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
Try our QuickAdminPanel generator!
2 Comments
Leave a Reply Cancel reply
Recent Posts
Try our QuickAdminPanel Generator!
How it works:
1. Generate panel online
No coding required, you just choose menu items.
2. Download code & install locally
Install with simple "composer install" and "php artisan migrate".
3. Customize anything!
We give all the code, so you can change anything after download.
It seems like the ‘approved_at’ checkbox in the edit photo form is fillable for a user without the ‘photo_review’ permission, even though it is not shown. So a registered user which is not a reviewer could easily add this to the form and get the image reviewed.
I would change this in app/Http/Controllers/Admin/PhotosController.php (not tested):
public function update(UpdatePhotoRequest $request, Photo $photo)
{
abort_unless(\Gate::allows(‘photo_edit’), 403);
if (\Gate::allows(‘photo_review’) {
$request[‘approved_at’] = $request->input(‘approved_at’, false)
? Carbon::now()->toDateTimeString()
: null;
}
$photo->update($request->all());
return redirect()->route(‘admin.photos.index’);
}
Great point, thanks Jasper! Probably Controller is the best place to restrict it, indeed.