Today we will create a Laravel demo project for reviewing photos, based on a real Upwork job. A simple CRUD will be pre-generated with our QuickAdminPanel, then I will provide step-by-step Laravel code instructions with roles, permissions, scopes, etc.

Notice: At the end of the article you will have a link to Github for the entire code.

We start with a simplified job description, taken from Upwork:

You are looking to create a website where users can upload images to an “image” bank. Uploaded images/photos must be reviewed by a reviewer before being made available for general viewing by website visitors.

User Types
1) guest: this user can only see photos that have been approved for viewing

2) registered user: has guest user rights and can upload photos to the site

3) reviewer: all the rights of a registered user but with the right to only review items placed in their queue for approval.

4) admin: all the rights of a reviewer, plus the ability to assign tasks to others for review. Possibility to replace the reviewer, possibility to extract the image from the general view, possibility to delete a user account

Function of the website: visitors come to browse images posted by a group of artists. The artist must create an account before being able to publish an image on the site. Images are placed in an image repository and will be displayed randomly on the first page.

Let’s try to create something like this in Laravel.

Teaser – the final result of our homepage will look like this:

Here are the steps we will follow in this article:

These will be generated by QuickAdminPanel:
1. User management system
2. CRUD Photos Menu
Next, we’ll continue adding custom code:
3. Add a reviewer role and permission called “photo_review”
4. Admin: assign a reviewer to each photo in the photo editing form
5. “Photos to review” menu and list of photos to review for an evaluator (will use Eloquent Scopes here)
6. Photo approval – approved_at field and definition in the Photo Edit form
7. Front homepage showing only approved photos


Step 1. Generate User/Photo CRUDs with QuickAdminPanel

You don’t have to use our generator and can create this foundation yourself, but hey, why not save some time?
I won’t go into detail here, because it’s simple, so just a few screenshots:

1. New panel

We generate a new project, we will choose the CoreUI theme.

2. User management

It is generated for us in each panel by default, with role/permission management, and with two default roles “Administrator” and “User”, so we don’t need to change anything here. Later in the article, when we need to add the “reviewer” role, I’ll explain how this works internally.

3. CRUD Photos

We create a new CRUD called “Photos” with only two fields – “title” (string field type) and “photo” (photo field type).

And that’s all for this phase: we download the project and install it locally with a few standard commands:

– .env file – identifiers
– composer installation
– php craft key: generate
– php artisan migrates – seed

Here is our visual result, after logging in and adding a few photos as an administrator:

From here we will continue with custom Laravel code.


Step 2. Roles/Permissions and new role “Reviewer”

As I mentioned above, QuickAdminPanel by default comes with two roles: “Administrator” and “User”. The only real difference between them is that the user cannot manage other users. But both can access all other CRUDs.

This system is based on the users/roles/permissions structure in the database tables, here are some screenshots:

It’s quite simple. You can learn more about our roles/permissions system in this article or watch this popular video.

Now we need to add a “Reviewer” role here with permission to review photos, right?

We will do this by adding new entries in database/seeds seeder files, generated by QuickAdminPanel.

database/seeds/RolesTableSeeder.php:


class RolesTableSeeder extends Seeder
{
    public function run()
    {
        $roles = [
            [
                'id'         => 1,
                'title'      => 'Admin',
                'created_at' => '2019-06-30 14:24:02',
                'updated_at' => '2019-06-30 14:24:02',
                'deleted_at' => null,
            ],
            [
                'id'         => 2,
                'title'      => 'User',
                'created_at' => '2019-06-30 14:24:02',
                'updated_at' => '2019-06-30 14:24:02',
                'deleted_at' => null,
            ],
            [
                'id'         => 3,
                'title'      => 'Reviewer',
                'created_at' => '2019-06-30 14:24:02',
                'updated_at' => '2019-06-30 14:24:02',
                'deleted_at' => null,
            ],
        ];

        Role::insert($roles);
    }
}

Then, for each CRUD, we generate 5 permissions: access, create, modify, view and delete. We need to add the 6th to the “photos” area. We will also do this in seed.

database/seeds/PermissionsTableSeeder.php:


class PermissionsTableSeeder extends Seeder
{
    public function run()
    {
        $permissions = [
            [
                'id'         => '1',
                'title'      => 'user_management_access',
                'created_at' => '2019-06-30 14:24:02',
                'updated_at' => '2019-06-30 14:24:02',
            ],

            // ... all other permissions

            [
                'id'         => '17',
                'title'      => 'photo_create',
                'created_at' => '2019-06-30 14:24:02',
                'updated_at' => '2019-06-30 14:24:02',
            ],
            [
                'id'         => '18',
                'title'      => 'photo_edit',
                'created_at' => '2019-06-30 14:24:02',
                'updated_at' => '2019-06-30 14:24:02',
            ],
            [
                'id'         => '19',
                'title'      => 'photo_show',
                'created_at' => '2019-06-30 14:24:02',
                'updated_at' => '2019-06-30 14:24:02',
            ],
            [
                'id'         => '20',
                'title'      => 'photo_delete',
                'created_at' => '2019-06-30 14:24:02',
                'updated_at' => '2019-06-30 14:24:02',
            ],
            [
                'id'         => '21',
                'title'      => 'photo_access',
                'created_at' => '2019-06-30 14:24:02',
                'updated_at' => '2019-06-30 14:24:02',
            ],
            [
                'id'         => '22',
                'title'      => 'photo_review',
                'created_at' => '2019-06-30 14:24:02',
                'updated_at' => '2019-06-30 14:24:02',
            ],
        ];

        Permission::insert($permissions);
    }
}

Finally, we need to assign all “photos” permissions to our new “Reviewer” role. We also do this in seeds, but with a little more magic: go through the permissions and assign them all to all roles except those related to user management. Here is the complete code.

database/seeds/PermissionRoleTableSeeder.php:


class PermissionRoleTableSeeder extends Seeder
{
    public function run()
    {
        // Assign all permissions to administrator - role ID 1
        $admin_permissions = Permission::all();
        Role::findOrFail(1)->permissions()->sync($admin_permissions->pluck('id'));

        // Reviewer permissions are same as administrator except user management
        $reviewer_permissions = $admin_permissions->filter(function ($permission) {
            return substr($permission->title, 0, 5) != 'user_' 
                && substr($permission->title, 0, 5) != 'role_' 
                && substr($permission->title, 0, 11) != 'permission_';
        });
        Role::findOrFail(3)->permissions()->sync($reviewer_permissions);

        // Finally, simple user permission is same as reviewer but cannot review
        $user_permissions = $reviewer_permissions->filter(function ($permission) {
            return $permission->title != 'photo_review';
        });
        Role::findOrFail(2)->permissions()->sync($user_permissions);
    }
}

And now if we reseed the database we should have the correct permissions:

php artisan migrate:fresh --seed

Step 3. Assign a reviewer to each new photo

Next step: Admin can appoint a reviewer when editing photos:

app/Http/Controllers/Admin/PhotosController.php:


public function edit(Photo $photo)
{
    abort_unless(\Gate::allows('photo_edit'), 403);
    $reviewers = Role::findOrFail(3)->users()->get();

    return view('admin.photos.edit', compact('photo', 'reviewers'));
}

And then displaying the drop-down list of potential reviewers.
resources/views/admin/photos/edit.blade.php:


<form action=" route("admin.photos.update", [$photo->id]) }}" 
    method="POST" enctype="multipart/form-data">
    @csrf
    @method('PUT')

    <div class="form-group {{ $errors->has('title') ? 'has-error' : '' }}">
        <label for="title">{{ trans('cruds.photo.fields.title') }}*</label>
        <input type="text" id="title" name="title" class="form-control" 
            value="{{ old('title', isset($photo) ? $photo->title : '') }}" required>
        @if($errors->has('title'))
            <em class="invalid-feedback">
                {{ $errors->first('title') }}
            </em>
        @endif
        <p class="helper-block">
            {{ trans('cruds.photo.fields.title_helper') }}
        </p>
    </div>

    <div class="form-group {{ $errors->has('photo') ? 'has-error' : '' }}">
        <label for="photo">{{ trans('cruds.photo.fields.photo') }}*</label>
        <div class="needsclick dropzone" id="photo-dropzone">

        </div>
        @if($errors->has('photo'))
            <em class="invalid-feedback">
                {{ $errors->first('photo') }}
            </em>
        @endif
        <p class="helper-block">
            {{ trans('cruds.photo.fields.photo_helper') }}
        </p>
    </div>

    @can('user_management_access')
        <div class="form-group">
            <label for="reviewer">{{ trans('cruds.photo.fields.reviewer') }}</label>
            <select class="form-control 
                {{ $errors->has('reviewer_id') ? 'has-error' : '' }}" id="reviewer" name="reviewer_id">
                <option value="">-</option>
                @foreach($reviewers as $reviewer)
                    <option value="{{ $reviewer->id }}" 
                    @if(isset($photo) && $photo->reviewer_id == $reviewer->id) selected @endif>
                        {{ $reviewer->name }}</option>
                @endforeach
            </select>
            @if($errors->has('reviewer_id'))
                <em class="invalid-feedback">
                    {{ $errors->first('reviewer_id') }}
                </em>
            @endif
            <p class="helper-block">
                {{ trans('cruds.photo.fields.reviewer_helper') }}
            </p>
        </div>
    @endcan
    

    <div>
        <input class="btn btn-danger" type="submit" value="{{ trans('global.save') }}">
    </div>
</form>

Here is the visual result:

And of course, we need to add a new DB field: photos.reviewer_id which can be nullable, here is the migration:


public function up()
{
    Schema::table('photos', function (Blueprint $table) {
        $table->unsignedInteger('reviewer_id')->nullable();
        $table->foreign('reviewer_id')->references('id')->on('users');
    });
}

Finally, we add it as $fillable in the model, in relation to users painting.

application/Photo.php:


class Photo extends Model implements HasMedia
{

    protected $fillable = [
        'title',
        'created_at',
        'updated_at',
        'deleted_at',
        'reviewer_id',
    ];

    public function reviewer()
    {
        return $this->belongsTo(User::class, 'reviewer_id');
    }

}

Our controller uses $query->all() to save the data, so we don’t need to change anything here – the photo reviewer will be automatically saved in the drop-down list.

Done here.


Step 4. “Photos to view” menu and list of photos

Ok, now we have roles/permissions and can assign reviewers to review photos. We probably need a new menu item so they can see this list of photos to review.

Here’s what we’re adding to our sidebar menu:

resources/views/partials/menu.blade.php


<ul class="nav">

    <!-- ... other menus ... -->

    @can('photo_access')
        <li class="nav-item">
            <a href=" route("admin.photos.index") }}" class="nav-link 
                {{ request()->is('admin/photos') || request()->is('admin/photos/*') 
                    && !request()->is('admin/photos/review') ? 'active' : '' }}">
                <i class="fa-fw fas fa-cogs nav-icon">

                </i>
                {{ trans('cruds.photo.title') }}
            </a>
        </li>
    @endcan

    @can('photo_review')
        <li class="nav-item">
            <a href=" route("admin.photos.indexReview") }}" class="nav-link 
                {{ request()->is('admin/photos/review') ? 'active' : '' }}">
                <i class="fa-fw fas fa-search nav-icon">

                </i>
                {{ trans('cruds.photo.review') }}
            </a>
        </li>
    @endcan

    <!-- ... other menus ... -->

</ul>

As you can see, we are already using @can(‘photo_review’) permission from the previous step, so simple users will not see this menu item.

And now let’s implement it.

routes/web.php:


Route::group([
  'prefix' => 'admin', 
  'as' => 'admin.', 
  'namespace' => 'Admin', 
  'middleware' => ['auth']
], function () {
    // ... other admin routes
    Route::get('photos/review', 'PhotosController@indexReview')
      ->name('photos.indexReview');
    Route::resource('photos', 'PhotosController');
});

So we have a new URL photos/reviewskeep in mind that this additional URL should appear Before Route::resource statement, not after, otherwise it will conflict with photo/{photo} what is to show() method in Controller.

Ok, let’s move on to the controller:

app/Http/Controllers/Admin/PhotosController.php:


public function indexReview()
{
    abort_unless(\Gate::allows('photo_review'), 403);
    $photos = Photo::reviewersPhotos()->get();

    return view('admin.photos.index', compact('photos'));
}

See part of Photo::reviewsPhotos()? This is a way to filter only photos for review by a connected reviewer. And for this we will use Eloquent Query Scopes.

In application/Photo.php we add this:


public function scopeReviewersPhotos($query)
{
    return $query->where('reviewer_id', auth()->id());
}

And that’s it. Blade file resources/views/admin/photos/index.blade.php because the list of photos is quite simple, but large, so I will not post it here, you will find it in the repository – link to Github at the end of the article.


Step 5. Photo approval – approved_at field and checkbox

Now we need to work on approving the photo. To do this, we will visually add a checkbox in the edit form. But in database we will save it under datetime field approved_to – it is much more informative to save WHEN the action was performed, instead of just saving the true/false value.

Migration file:


public function up()
{
    Schema::table('photos', function (Blueprint $table) {
        $table->datetime('approved_at')->nullable();
    });
}

New $fillable And $dates In application/Photo.php model:


class Photo extends Model implements HasMedia
{
    protected $dates = [
        'created_at',
        'updated_at',
        'deleted_at',
        'approved_at',
    ];

    protected $fillable = [
        'title',
        'created_at',
        'updated_at',
        'deleted_at',
        'approved_at',
        'reviewer_id',
    ];

    // ...
}

An additional checkbox field in the Edit form, displayed only if you have this permission:
resource/views/admin/photos/edit.blade.php:


@can('photo_review')
    <div class="form-group">
        <div class="form-check">
            <input type="checkbox" class="form-check-input" 
              id="approved" name="approved_at" 
              @if(isset($photo->approved_at)) checked @endif>
            <label for="approved" 
              class="form-check-label">{{ trans('cruds.photo.fields.approved') }}</label>
        </div>
        <p class="helper-block">
            {{ trans('cruds.photo.fields.approved_helper') }}
        </p>
    </div>
@endcan

And we process it in app/Http/Controllers/Admin/PhotosController.php:


public function update(UpdatePhotoRequest $request, Photo $photo)
{
    abort_unless(\Gate::allows('photo_edit'), 403);
    $request['approved_at'] = $request->input('approved_at', false) 
      ? Carbon::now()->toDateTimeString() 
      : null;

    $photo->update($request->all());

    return redirect()->route('admin.photos.index');
}

Step 6. Showing approved photos on the homepage

Finally, for guest visitors, we need to display photos on the home page, but only approved ones. Another litter!

application/Photo.php


public function scopeApproved($query)
{
    return $query->whereNotNull('approved_at');
}

Then go to the home page in routes/web.php:


Route::get('/', 'FrontController@index')->name('front.home');

Following, app/Http/Controllers/FrontController.php:


public function index()
{
    $photos = Photo::approved()->get();
    return view('front.index', compact('photos'));
}

And finally, the code for the first page.
resources/views/front/index.blade.php:


@extends('layouts.front')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-10">
            <div class="card">
                <div class="card-header">{{ trans('cruds.photo.title') }}</div>

                <div class="card-body">
                    @if (session('status'))
                        <div class="alert alert-success" role="alert">
                            {{ session('status') }}
                        </div>
                    @endif
                    <div class="container-fluid">
                        <div class="row">
                            @forelse ($photos as $photo)
                                <div class="col-md-3 mb-2">
                                    @if($photo->photo)
                                        <a href=" $photo->photo->getUrl() }}" target="_blank">
                                            <img src=" $photo->photo->getUrl() }}" class="img-thumbnail" width="150px">
                                        </a>
                                    @endif
                                </div>
                            @empty
                                <div class="w-100">
                                    <p class="text-center">{{ trans('panel.empty') }}</p>
                                </div>
                            @endforelse
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

Visual result:

There is magic with getUrl() method for the photo, you will find all the explanations on this subject in the repository (see below).


So, with this project I wanted to show you:

  • How to implement and customize roles/permissions in Laravel
  • How to use Eloquent Scopes to filter data
  • Secondary objective: How easy it is to start an administration project with QuickAdminPanel

Here is the link to the repository which contains all the code above, as well as the multi-tenant implementation, so each user only sees their photos.

Appreciate:



Technology

Another Tech Information

Similar Posts