Demo-Project: “Reviewer” Role to Manage Photos in Laravel
Founder of QuickAdminPanel
Today we will create a Laravel demo-project for reviewing photos, based on a real job from Upwork. Simple CRUD will be pre-generated with our QuickAdminPanel, and then I will provide step-by-step Laravel code instructions with roles-permissions, scopes etc. Let’s go.
Notice: at the end of the article, you will have a link to Github for the whole code.
We start with a simplified job description, taken from Upwork:
Types of users
1) guest: this user can only view the photos that has been approved for viewing
2) registered user: has guest user rights, plus can upload photos to the site
3) reviewer: All rights of a registered user but with rights to review only items placed in his queue for approval .
4) admin: all rights of a reviewer, plus ability to assign task to others for review. Ability to override reviewer, ability to pull image from general view, ability to delete user account
Website function: visitors come to browse images posted by a group of artists. The artist must create an account before he/she can post an image to the site. Images goes into a image repository and will be randomly displayed on the front page.
Let’s try to create something like this in Laravel.
Teaser – our final homepage result will look like this:
These are the steps we will take in this article:
These will be generated by QuickAdminPanel:
1. User management system
2. Photos CRUD menu
Then we will continue adding custom code:
3. Add reviewer role and permission called ‘photo_review’
4. Admin: Assigning a reviewer to each photo in Photo Edit form
5. Menu “Photos for Review” and list of photos to review for a reviewer (will use Eloquent Scopes here)
6. Photos approval – approved_at field and setting it in Photo Edit form
7. Front Homepage showing only approved photos
Step 1. Generate Users/Photos CRUDs with QuickAdminPanel
You don’t have to use our generator, and can create this foundation yourself, but hey – why not save time?
Won’t get into detail here, as it is straightforward, so just a few screenshots:
1. New panel
We generate a new project, we will choose CoreUI theme.
2. User management
It is generated for us in every default panel, with roles/permissions management, and with two default roles “Admin” and “User”, so we don’t need to change anything here. Later in the article, when we need to add “reviewer” role, I will explain how it works internally.
3. Photos CRUD
We create a new CRUD called “Photos” with only two fields – “title” (string field type) and “photo” (photo field type).
And that’s it for this phase: we download the project and install it locally with a few typical commands:
– .env file – credentials
– composer install
– php artisan key:generate
– php artisan migrate –seed
Here’s our visual result, after we login and add a few photos as administrator:
From here, we will continue with custom Laravel code.
Step 2. Roles/Permissions and new “Reviewer” role
As I mentioned above, default QuickAdminPanel comes with two roles: “Administrator” and “User”. The only actual difference between them, is that User cannot manage other Users. But both can access all other CRUDs.
This system is based on users/roles/permissions structure in DB tables, here are a few screenshots:
It’s pretty simple. You can read more about our roles/permissions system in this article or watch this popular video.
Now, we need to add a “Reviewer” role here with a permission to review photos, right?
We will do that by adding new entries in database/seeds seeder files, which were 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); } }
Next, for every CRUD we generate 5 permissions – access, create, edit, show and delete. We need to add 6th one to “photos” area. We also will do it in the 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 that in the seeds, but with a little more magic – looping through permissions and assigning them all to all role, except user management related ones. Here’s the full 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 re-seed the database, we should have correct permissions:
php artisan migrate:fresh --seed
Step 3. Assigning a Reviewer to each new photo
Next step – administrator can assign one reviewer while editing photo edit:
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 showing the dropdown 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’s the visual result:
And, of course, we need to add a new DB field: photos.reviewer_id which can be nullable, here’s 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, with relationship to users table.
app/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 $request->all() to save data, so we don’t need to change anything there – photo reviewer will be saved from the dropdown automatically.
Done here.
Step 4. Menu “Photos for Review” and list of photos
Ok, now we have roles/permissions and can assign reviewers to review photos. We probably need a new menu item for them to see that list of photos to review.
Here’s what we add 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 already are using @can(‘photo_review’) permission from the previous step, so simple users won’t see that menu item.
And now, let’s actually 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 new URL photos/review, keep in mind this extra URL should come before Route::resource statement, not after, otherwise it will conflict with photos/{photo} which is show() method in Controller.
Ok, let’s get to 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 the part of Photo::reviewersPhotos()? This is a way to filter out only photos to review by a logged-in reviewer. And for this, we will use Eloquent Query Scopes.
In app/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 for the photos list is pretty simple, but big, so won’t post it here, you will find it in the repository – link to Github at the end of the article.
Step 5. Approving the photo – approved_at field and checkbox
Now, we need to work on actually approving the photo. For that, we will visually add a checkbox in the edit form. But in the database, we will save it as datetime field approved_at – it’s much more informative to save WHEN the action was done, instead of just true/false value.
Migration file:
public function up() { Schema::table('photos', function (Blueprint $table) { $table->datetime('approved_at')->nullable(); }); }
New $fillable and $dates in app/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', ]; // ... }
One more checkbox field in Edit form, shown only if you have that 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. Viewing approved photos on the homepage
Finally, for guest visitors we need to show photos on the homepage, but only approved ones. Another scope!
app/Photo.php
public function scopeApproved($query) { return $query->whereNotNull('approved_at'); }
Then, route to homepage in routes/web.php:
Route::get('/', 'FrontController@index')->name('front.home');
Next, app/Http/Controllers/FrontController.php:
public function index() { $photos = Photo::approved()->get(); return view('front.index', compact('photos')); }
And finally, code for the front 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 some magic with getUrl() method for the photo, you will find all explanations to this 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
- Side goal: how easy it is to start admin project with QuickAdminPanel
Here’s the link to the repository that contains all the code from the above, plus Multi-Tenancy implementation, so every user would see only their photos.
Enjoy: https://github.com/LaravelDaily/Demo-Laravel-Image-Reviewers