├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── src ├── Moderatable.php ├── ModerationQueryBuilder.php ├── ModerationScope.php ├── ModerationServiceProvider.php ├── Status.php └── config │ └── moderation.php └── tests ├── BaseTestCase.php ├── ModerationScopeTest.php ├── ModerationTraitTest.php ├── Post.php └── migrations └── create_moderation_posts_table.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.1 5 | - 7.2 6 | 7 | sudo: false 8 | 9 | # cache vendor dirs 10 | cache: 11 | directories: 12 | - vendor 13 | - $HOME/.composer/cache 14 | 15 | before_install: 16 | - phpenv config-rm xdebug.ini 17 | 18 | install: 19 | - COMPOSER_DISCARD_CHANGES=1 composer install --dev --prefer-source --no-interaction 20 | 21 | before_script: 22 | - cp tests/migrations/create_moderation_posts_table.php vendor/laravel/laravel/database/migrations/2015_11_19_053825_create_moderation_posts_table.php 23 | - cp src/config/moderation.php vendor/laravel/laravel/config/moderation.php 24 | - touch vendor/laravel/laravel/database/database.sqlite 25 | - cd vendor/laravel/laravel 26 | - composer update --dev --prefer-source --no-interaction 27 | - yes | php artisan migrate 28 | - cd - 29 | 30 | script: 31 | - vendor/bin/phpunit tests 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Alex Kyriakidis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Moderation [![Build Status](https://travis-ci.org/hootlex/laravel-moderation.svg?branch=v1.0.11)](https://travis-ci.org/hootlex/laravel-moderation) [![Version](https://img.shields.io/packagist/v/hootlex/laravel-moderation.svg?style=flat)](https://packagist.org/packages/hootlex/laravel-moderation) [![Total Downloads](https://img.shields.io/packagist/dt/hootlex/laravel-moderation.svg?style=flat)](https://packagist.org/packages/hootlex/laravel-moderation) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat)](LICENSE) 2 | A simple Moderation System for Laravel 5.* that allows you to Approve or Reject resources like posts, comments, users, etc. 3 | 4 | Keep your application pure by preventing offensive, irrelevant, or insulting content. 5 | 6 | ## Possible Use Case 7 | 8 | 1. User creates a resource (a post, a comment or any Eloquent Model). 9 | 2. The resource is pending and invisible in website (ex. `Post::all()` returns only approved posts). 10 | 3. Moderator decides if the resource will be approved, rejected or postponed. 11 | 12 | 1. **Approved**: Resource is now public and queryable. 13 | 2. **Rejected**: Resource will be excluded from all queries. Rejected resources will be returned only if you scope a query to include them. (scope: `withRejected`) 14 | 3. **Postponed**: Resource will be excluded from all queries until Moderator decides to approve it. 15 | 16 | 4. You application is clean. 17 | 18 | ## Installation 19 | 20 | First, install the package through Composer. 21 | 22 | ```php 23 | composer require hootlex/laravel-moderation 24 | ``` 25 | 26 | If you are using Laravel < 5.5, you need to add Hootlex\Moderation\ModerationServiceProvider to your `config/app.php` providers array: 27 | ```php 28 | 'providers' => [ 29 | ... 30 | Hootlex\Moderation\ModerationServiceProvider::class, 31 | ... 32 | ]; 33 | ``` 34 | Lastly you publish the config file. 35 | 36 | ``` 37 | php artisan vendor:publish --provider="Hootlex\Moderation\ModerationServiceProvider" --tag=config 38 | ``` 39 | 40 | 41 | ## Prepare Model 42 | 43 | To enable moderation for a model, use the `Hootlex\Moderation\Moderatable` trait on the model and add the `status`, `moderated_by` and `moderated_at` columns to your model's table. 44 | ```php 45 | use Hootlex\Moderation\Moderatable; 46 | class Post extends Model 47 | { 48 | use Moderatable; 49 | ... 50 | } 51 | ``` 52 | 53 | Create a migration to add the new columns. [(You can use custom names for the moderation columns)](#configuration) 54 | 55 | Example Migration: 56 | ```php 57 | class AddModerationColumnsToPostsTable extends Migration 58 | { 59 | /** 60 | * Run the migrations. 61 | * 62 | * @return void 63 | */ 64 | public function up() 65 | { 66 | Schema::table('posts', function (Blueprint $table) { 67 | $table->smallInteger('status')->default(0); 68 | $table->dateTime('moderated_at')->nullable(); 69 | //To track who moderated the Model, add 'moderated_by' and set the column name in the config file. 70 | //$table->integer('moderated_by')->nullable()->unsigned(); 71 | }); 72 | } 73 | 74 | /** 75 | * Reverse the migrations. 76 | * 77 | * @return void 78 | */ 79 | public function down() 80 | { 81 | Schema::table('posts', function(Blueprint $table) 82 | { 83 | $table->dropColumn('status'); 84 | $table->dropColumn('moderated_at'); 85 | //$table->dropColumn('moderated_by'); 86 | }); 87 | } 88 | } 89 | ``` 90 | 91 | **You are ready to go!** 92 | 93 | ## Usage 94 | > **Note:** In next examples I will use Post model to demonstrate how the query builder works. You can Moderate any Eloquent Model, even User. 95 | 96 | ### Moderate Models 97 | You can moderate a model Instance: 98 | ```php 99 | $post->markApproved(); 100 | 101 | $post->markRejected(); 102 | 103 | $post->markPostponed(); 104 | 105 | $post->markPending(); 106 | ``` 107 | 108 | or by referencing it's id 109 | ```php 110 | Post::approve($post->id); 111 | 112 | Post::reject($post->id); 113 | 114 | Post::postpone($post->id); 115 | ``` 116 | 117 | or by making a query. 118 | ```php 119 | Post::where('title', 'Horse')->approve(); 120 | 121 | Post::where('title', 'Horse')->reject(); 122 | 123 | Post::where('title', 'Horse')->postpone(); 124 | ``` 125 | 126 | ### Query Models 127 | By default only Approved models will be returned on queries. To change this behavior check the [configuration](#configuration). 128 | 129 | ##### To query the Approved Posts, run your queries as always. 130 | ```php 131 | //it will return all Approved Posts (strict mode) 132 | Post::all(); 133 | 134 | // when not in strict mode 135 | Post::approved()->get(); 136 | 137 | //it will return Approved Posts where title is Horse 138 | Post::where('title', 'Horse')->get(); 139 | 140 | ``` 141 | ##### Query pending or rejected models. 142 | ```php 143 | //it will return all Pending Posts 144 | Post::pending()->get(); 145 | 146 | //it will return all Rejected Posts 147 | Post::rejected()->get(); 148 | 149 | //it will return all Postponed Posts 150 | Post::postponed()->get(); 151 | 152 | //it will return Approved and Pending Posts 153 | Post::withPending()->get(); 154 | 155 | //it will return Approved and Rejected Posts 156 | Post::withRejected()->get(); 157 | 158 | //it will return Approved and Postponed Posts 159 | Post::withPostponed()->get(); 160 | ``` 161 | ##### Query ALL models 162 | ```php 163 | //it will return all Posts 164 | Post::withAnyStatus()->get(); 165 | 166 | //it will return all Posts where title is Horse 167 | Post::withAnyStatus()->where('title', 'Horse')->get(); 168 | ``` 169 | 170 | ### Model Status 171 | To check the status of a model there are 3 helper methods which return a boolean value. 172 | ```php 173 | //check if a model is pending 174 | $post->isPending(); 175 | 176 | //check if a model is approved 177 | $post->isApproved(); 178 | 179 | //check if a model is rejected 180 | $post->isRejected(); 181 | 182 | //check if a model is rejected 183 | $post->isPostponed(); 184 | ``` 185 | 186 | ## Strict Moderation 187 | Strict Moderation means that only Approved resource will be queried. To query Pending resources along with Approved you have to disable Strict Moderation. See how you can do this in the [configuration](#configuration). 188 | 189 | ## Configuration 190 | 191 | ### Global Configuration 192 | To configuration Moderation package globally you have to edit `config/moderation.php`. 193 | Inside `moderation.php` you can configure the following: 194 | 195 | 1. `status_column` represents the default column 'status' in the database. 196 | 2. `moderated_at_column` represents the default column 'moderated_at' in the database. 197 | 2. `moderated_by_column` represents the default column 'moderated_by' in the database. 198 | 3. `strict` represents [*Strict Moderation*](#strict-moderation). 199 | 200 | ### Model Configuration 201 | Inside your Model you can define some variables to overwrite **Global Settings**. 202 | 203 | To overwrite `status` column define: 204 | ```php 205 | const MODERATION_STATUS = 'moderation_status'; 206 | ``` 207 | 208 | To overwrite `moderated_at` column define: 209 | ```php 210 | const MODERATED_AT = 'mod_at'; 211 | ``` 212 | 213 | To overwrite `moderated_by` column define: 214 | ```php 215 | const MODERATED_BY = 'mod_by'; 216 | ``` 217 | 218 | To enable or disable [Strict Moderation](#strict-moderation): 219 | ```php 220 | public static $strictModeration = true; 221 | ``` 222 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hootlex/laravel-moderation", 3 | "description": "A simple Content Moderation System for Laravel 5.* that allows you to Approve or Reject resources like posts, comments, users, etc.", 4 | "keywords": [ 5 | "laravel", 6 | "moderation", 7 | "moderation-system", 8 | "content-moderation" 9 | ], 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Alex Kyriakidis", 14 | "email": "hootlex@icloud.com" 15 | } 16 | ], 17 | "autoload-dev": { 18 | "classmap": [ 19 | "tests", 20 | "vendor/laravel/laravel/tests" 21 | ] 22 | }, 23 | "require": { 24 | "php": ">=5.4.0" 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "7.0", 28 | "laravel/laravel": "5.*", 29 | "nunomaduro/collision": "^2.0", 30 | "beyondcode/laravel-dump-server": "~1.0" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Hootlex\\Moderation\\": "src/" 35 | } 36 | }, 37 | "extra": { 38 | "laravel": { 39 | "providers": [ 40 | "Hootlex\\Moderation\\ModerationServiceProvider" 41 | ] 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Moderatable.php: -------------------------------------------------------------------------------- 1 | newQueryWithoutScope(new ModerationScope())->approve($id); 31 | } 32 | 33 | /** 34 | * Change resource status to rejected. 35 | * 36 | * @param null $id 37 | * 38 | * @return mixed 39 | */ 40 | public static function reject($id) 41 | { 42 | return (new static)->newQueryWithoutScope(new ModerationScope())->reject($id); 43 | } 44 | 45 | /** 46 | * Change resource status to postponed. 47 | * 48 | * @param null $id 49 | * 50 | * @return mixed 51 | */ 52 | public static function postpone($id) 53 | { 54 | return (new static)->newQueryWithoutScope(new ModerationScope())->postpone($id); 55 | } 56 | 57 | /** 58 | * Change instance's status to approved. 59 | * 60 | * @return mixed 61 | */ 62 | public function markApproved() 63 | { 64 | $new = (new static)->newQueryWithoutScope(new ModerationScope())->approve($this->id); 65 | return $this->setRawAttributes($new->attributesToArray()); 66 | } 67 | 68 | /** 69 | * Change instance's status to rejected. 70 | * 71 | * @return mixed 72 | */ 73 | public function markRejected() 74 | { 75 | $new = (new static)->newQueryWithoutScope(new ModerationScope())->reject($this->id); 76 | return $this->setRawAttributes($new->attributesToArray()); 77 | } 78 | 79 | /** 80 | * Change instance's status to postponed. 81 | * 82 | * @return mixed 83 | */ 84 | public function markPostponed() 85 | { 86 | $new = (new static)->newQueryWithoutScope(new ModerationScope())->postpone($this->id); 87 | return $this->setRawAttributes($new->attributesToArray()); 88 | } 89 | 90 | /** 91 | * Change instance's status to pending. 92 | * 93 | * @return mixed 94 | */ 95 | public function markPending() 96 | { 97 | $new = (new static)->newQueryWithoutScope(new ModerationScope())->pend($this->id); 98 | return $this->setRawAttributes($new->attributesToArray()); 99 | } 100 | 101 | /** 102 | * Determine if the model instance is approved. 103 | * 104 | * @return bool 105 | */ 106 | public function isApproved() 107 | { 108 | return $this->{$this->getStatusColumn()} == Status::APPROVED; 109 | } 110 | 111 | /** 112 | * Determine if the model instance is rejected. 113 | * 114 | * @return bool 115 | */ 116 | public function isRejected() 117 | { 118 | return $this->{$this->getStatusColumn()} == Status::REJECTED; 119 | } 120 | 121 | /** 122 | * Determine if the model instance is postponed. 123 | * 124 | * @return bool 125 | */ 126 | public function isPostponed() 127 | { 128 | return $this->{$this->getStatusColumn()} == Status::POSTPONED; 129 | } 130 | 131 | /** 132 | * Determine if the model instance is pending. 133 | * 134 | * @return bool 135 | */ 136 | public function isPending() 137 | { 138 | return $this->{$this->getStatusColumn()} == Status::PENDING; 139 | } 140 | 141 | /** 142 | * Get the name of the "status" column. 143 | * 144 | * @return string 145 | */ 146 | public function getStatusColumn() 147 | { 148 | return defined('static::MODERATION_STATUS') ? static::MODERATION_STATUS : config('moderation.status_column'); 149 | } 150 | 151 | /** 152 | * Get the fully qualified "status" column. 153 | * 154 | * @return string 155 | */ 156 | public function getQualifiedStatusColumn() 157 | { 158 | return $this->getTable() . '.' . $this->getStatusColumn(); 159 | } 160 | 161 | /** 162 | * Get the fully qualified "moderated at" column. 163 | * 164 | * @return string 165 | */ 166 | public function getQualifiedModeratedAtColumn() 167 | { 168 | return $this->getTable() . '.' . $this->getModeratedAtColumn(); 169 | } 170 | 171 | /** 172 | * Get the fully qualified "moderated by" column. 173 | * 174 | * @return string 175 | */ 176 | public function getQualifiedModeratedByColumn() 177 | { 178 | return $this->getTable() . '.' . $this->getModeratedByColumn(); 179 | } 180 | 181 | /** 182 | * Get the name of the "moderated at" column. 183 | * 184 | * @return string 185 | */ 186 | public function getModeratedAtColumn() 187 | { 188 | return defined('static::MODERATED_AT') ? static::MODERATED_AT : config('moderation.moderated_at_column'); 189 | } 190 | 191 | /** 192 | * Get the name of the "moderated by" column. 193 | * 194 | * @return string 195 | */ 196 | public function getModeratedByColumn() 197 | { 198 | return defined('static::MODERATED_BY') ? static::MODERATED_BY : config('moderation.moderated_by_column'); 199 | } 200 | 201 | /** 202 | * Get the name of the "moderated at" column. 203 | * Append "moderated at" column to the attributes that should be converted to dates. 204 | * 205 | * @return string 206 | */ 207 | public function getDates(){ 208 | return array_merge(parent::getDates(), [$this->getModeratedAtColumn()]); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/ModerationQueryBuilder.php: -------------------------------------------------------------------------------- 1 | newQueryWithoutScope(new ModerationScope())->pending(); 17 | } 18 | 19 | /** 20 | * Get a new query builder that only includes rejected resources. 21 | * 22 | * @return \Illuminate\Database\Eloquent\Builder|static 23 | */ 24 | public static function rejected() 25 | { 26 | return (new static)->newQueryWithoutScope(new ModerationScope())->rejected(); 27 | } 28 | 29 | /** 30 | * Get a new query builder that only includes postponed resources. 31 | * 32 | * @return \Illuminate\Database\Eloquent\Builder|static 33 | */ 34 | public static function postponed() 35 | { 36 | return (new static)->newQueryWithoutScope(new ModerationScope())->postponed(); 37 | } 38 | 39 | /** 40 | * Get a new query builder that includes pending resources. 41 | * 42 | * @return \Illuminate\Database\Eloquent\Builder|static 43 | */ 44 | public static function withPending() 45 | { 46 | return (new static)->newQueryWithoutScope(new ModerationScope())->withPending(); 47 | } 48 | 49 | /** 50 | * Get a new query builder that includes rejected resources. 51 | * 52 | * @return \Illuminate\Database\Eloquent\Builder|static 53 | */ 54 | public static function withRejected() 55 | { 56 | return (new static)->newQueryWithoutScope(new ModerationScope())->withRejected(); 57 | } 58 | 59 | /** 60 | * Get a new query builder that includes postponed resources. 61 | * 62 | * @return \Illuminate\Database\Eloquent\Builder|static 63 | */ 64 | public static function withPostponed() 65 | { 66 | return (new static)->newQueryWithoutScope(new ModerationScope())->withPostponed(); 67 | } 68 | 69 | /** 70 | * Get a new query builder that includes all resources. 71 | * 72 | * @return \Illuminate\Database\Eloquent\Builder|static 73 | */ 74 | public static function withAnyStatus() 75 | { 76 | return (new static)->newQueryWithoutScope(new ModerationScope()); 77 | } 78 | } -------------------------------------------------------------------------------- /src/ModerationScope.php: -------------------------------------------------------------------------------- 1 | where($model->getQualifiedStatusColumn(), '=', Status::APPROVED); 48 | } else { 49 | $builder->where($model->getQualifiedStatusColumn(), '!=', Status::REJECTED); 50 | } 51 | 52 | $this->extend($builder); 53 | } 54 | 55 | /** 56 | * Remove the scope from the given Eloquent query builder. 57 | * 58 | * (This method exists in order to achieve compatibility with laravel 5.1.*) 59 | * 60 | * @param \Illuminate\Database\Eloquent\Builder $builder 61 | * @param \Illuminate\Database\Eloquent\Model $model 62 | * @return void 63 | */ 64 | public function remove(Builder $builder, Model $model) 65 | { 66 | $builder->withoutGlobalScope($this); 67 | 68 | $column = $model->getQualifiedStatusColumn(); 69 | $query = $builder->getQuery(); 70 | 71 | $bindingKey = 0; 72 | 73 | foreach ((array)$query->wheres as $key => $where) { 74 | if ($this->isModerationConstraint($where, $column)) { 75 | $this->removeWhere($query, $key); 76 | 77 | // Here SoftDeletingScope simply removes the where 78 | // but since we use Basic where (not Null type) 79 | // we need to get rid of the binding as well 80 | $this->removeBinding($query, $bindingKey); 81 | } 82 | 83 | // Check if where is either NULL or NOT NULL type, 84 | // if that's the case, don't increment the key 85 | // since there is no binding for these types 86 | if (!in_array($where['type'], ['Null', 'NotNull'])) $bindingKey++; 87 | } 88 | 89 | } 90 | 91 | /** 92 | * Extend the query builder with the needed functions. 93 | * 94 | * @param \Illuminate\Database\Eloquent\Builder $builder 95 | * 96 | * @return void 97 | */ 98 | public function extend(Builder $builder) 99 | { 100 | foreach ($this->extensions as $extension) { 101 | $this->{"add{$extension}"}($builder); 102 | } 103 | } 104 | 105 | /** 106 | * Add the with-pending extension to the builder. 107 | * 108 | * @param \Illuminate\Database\Eloquent\Builder $builder 109 | * 110 | * @return void 111 | */ 112 | protected function addWithPending(Builder $builder) 113 | { 114 | $builder->macro('withPending', function (Builder $builder) { 115 | $this->remove($builder, $builder->getModel()); 116 | 117 | return $builder->whereIN($this->getStatusColumn($builder), [Status::APPROVED, Status::PENDING]); 118 | }); 119 | } 120 | 121 | /** 122 | * Add the with-rejected extension to the builder. 123 | * 124 | * @param \Illuminate\Database\Eloquent\Builder $builder 125 | * 126 | * @return void 127 | */ 128 | protected function addWithRejected(Builder $builder) 129 | { 130 | $builder->macro('withRejected', function (Builder $builder) { 131 | $this->remove($builder, $builder->getModel()); 132 | 133 | return $builder->whereIN($this->getStatusColumn($builder), 134 | [Status::APPROVED, Status::REJECTED]); 135 | }); 136 | } 137 | 138 | /** 139 | * Add the with-postpone extension to the builder. 140 | * 141 | * @param \Illuminate\Database\Eloquent\Builder $builder 142 | * 143 | * @return void 144 | */ 145 | protected function addWithPostponed(Builder $builder) 146 | { 147 | $builder->macro('withPostponed', function (Builder $builder) { 148 | $this->remove($builder, $builder->getModel()); 149 | 150 | return $builder->whereIN($this->getStatusColumn($builder), 151 | [Status::APPROVED, Status::POSTPONED]); 152 | }); 153 | } 154 | 155 | /** 156 | * Add the with-any-status extension to the builder. 157 | * 158 | * @param \Illuminate\Database\Eloquent\Builder $builder 159 | * 160 | * @return void 161 | */ 162 | protected function addWithAnyStatus(Builder $builder) 163 | { 164 | $builder->macro('withAnyStatus', function (Builder $builder) { 165 | $this->remove($builder, $builder->getModel()); 166 | return $builder; 167 | }); 168 | } 169 | 170 | /** 171 | * Add the Approved extension to the builder. 172 | * 173 | * @param \Illuminate\Database\Eloquent\Builder $builder 174 | * 175 | * @return void 176 | */ 177 | protected function addApproved(Builder $builder) 178 | { 179 | $builder->macro('approved', function (Builder $builder) { 180 | $model = $builder->getModel(); 181 | 182 | $this->remove($builder, $model); 183 | 184 | $builder->where($model->getQualifiedStatusColumn(), '=', Status::APPROVED); 185 | 186 | return $builder; 187 | }); 188 | } 189 | 190 | /** 191 | * Add the Pending extension to the builder. 192 | * 193 | * @param \Illuminate\Database\Eloquent\Builder $builder 194 | * 195 | * @return void 196 | */ 197 | protected function addPending(Builder $builder) 198 | { 199 | $builder->macro('pending', function (Builder $builder) { 200 | $model = $builder->getModel(); 201 | 202 | $this->remove($builder, $model); 203 | 204 | $builder->where($model->getQualifiedStatusColumn(), '=', Status::PENDING); 205 | 206 | return $builder; 207 | }); 208 | } 209 | 210 | /** 211 | * Add the Rejected extension to the builder. 212 | * 213 | * @param \Illuminate\Database\Eloquent\Builder $builder 214 | * 215 | * @return void 216 | */ 217 | protected function addRejected(Builder $builder) 218 | { 219 | $builder->macro('rejected', function (Builder $builder) { 220 | $model = $builder->getModel(); 221 | 222 | $this->remove($builder, $model); 223 | 224 | $builder->where($model->getQualifiedStatusColumn(), '=', Status::REJECTED); 225 | 226 | return $builder; 227 | }); 228 | } 229 | 230 | /** 231 | * Add the Postponed extension to the builder. 232 | * 233 | * @param \Illuminate\Database\Eloquent\Builder $builder 234 | * 235 | * @return void 236 | */ 237 | protected function addPostponed(Builder $builder) 238 | { 239 | $builder->macro('postponed', function (Builder $builder) { 240 | $model = $builder->getModel(); 241 | 242 | $this->remove($builder, $model); 243 | 244 | $builder->where($model->getQualifiedStatusColumn(), '=', Status::POSTPONED); 245 | 246 | return $builder; 247 | }); 248 | } 249 | 250 | /** 251 | * Add the Approve extension to the builder. 252 | * 253 | * @param \Illuminate\Database\Eloquent\Builder $builder 254 | * 255 | * @return void 256 | */ 257 | protected function addApprove(Builder $builder) 258 | { 259 | $builder->macro('approve', function (Builder $builder, $id = null) { 260 | $builder->withAnyStatus(); 261 | return $this->updateModerationStatus($builder, $id, Status::APPROVED); 262 | }); 263 | } 264 | 265 | /** 266 | * Add the Reject extension to the builder. 267 | * 268 | * @param \Illuminate\Database\Eloquent\Builder $builder 269 | * 270 | * @return void 271 | */ 272 | protected function addReject(Builder $builder) 273 | { 274 | $builder->macro('reject', function (Builder $builder, $id = null) { 275 | $builder->withAnyStatus(); 276 | return $this->updateModerationStatus($builder, $id, Status::REJECTED); 277 | 278 | }); 279 | } 280 | 281 | /** 282 | * Add the Postpone extension to the builder. 283 | * 284 | * @param \Illuminate\Database\Eloquent\Builder $builder 285 | * 286 | * @return void 287 | */ 288 | protected function addPostpone(Builder $builder) 289 | { 290 | $builder->macro('postpone', function (Builder $builder, $id = null) { 291 | $builder->withAnyStatus(); 292 | return $this->updateModerationStatus($builder, $id, Status::POSTPONED); 293 | }); 294 | } 295 | 296 | /** 297 | * Add the Postpone extension to the builder. 298 | * 299 | * @param \Illuminate\Database\Eloquent\Builder $builder 300 | * 301 | * @return void 302 | */ 303 | protected function addPend(Builder $builder) 304 | { 305 | $builder->macro('pend', function (Builder $builder, $id = null) { 306 | $builder->withAnyStatus(); 307 | return $this->updateModerationStatus($builder, $id, Status::PENDING); 308 | }); 309 | } 310 | 311 | /** 312 | * Get the "deleted at" column for the builder. 313 | * 314 | * @param \Illuminate\Database\Eloquent\Builder $builder 315 | * 316 | * @return string 317 | */ 318 | protected function getStatusColumn(Builder $builder) 319 | { 320 | if ($builder->getQuery()->joins && count($builder->getQuery()->joins) > 0) { 321 | return $builder->getModel()->getQualifiedStatusColumn(); 322 | } else { 323 | return $builder->getModel()->getStatusColumn(); 324 | } 325 | } 326 | 327 | /** 328 | * Remove scope constraint from the query. 329 | * 330 | * @param $query 331 | * @param int $key 332 | * 333 | * @internal param \Illuminate\Database\Query\Builder $builder 334 | */ 335 | protected function removeWhere($query, $key) 336 | { 337 | unset($query->wheres[$key]); 338 | 339 | $query->wheres = array_values($query->wheres); 340 | } 341 | 342 | /** 343 | * Remove scope constraint from the query. 344 | * 345 | * @param $query 346 | * @param int $key 347 | * 348 | * @internal param \Illuminate\Database\Query\Builder $builder 349 | */ 350 | protected function removeBinding($query, $key) 351 | { 352 | $bindings = $query->getRawBindings()['where']; 353 | 354 | unset($bindings[$key]); 355 | 356 | $query->setBindings($bindings); 357 | } 358 | 359 | /** 360 | * @param \Illuminate\Database\Eloquent\Builder $builder 361 | * @param $id 362 | * @param $status 363 | * 364 | * @return bool|int 365 | */ 366 | private function updateModerationStatus(Builder $builder, $id, $status) 367 | { 368 | 369 | //If $id parameter is passed then update the specified model 370 | if ($id) { 371 | $model = $builder->find($id); 372 | $model->{$model->getStatusColumn()} = $status; 373 | $model->{$model->getModeratedAtColumn()} = Carbon::now(); 374 | //if moderated_by in enabled then append it to the update 375 | if ($moderated_by = $model->getModeratedByColumn()) { 376 | $model->{$moderated_by} = \Auth::user()->getKey(); 377 | } 378 | 379 | $model->save(); 380 | return $model; 381 | } 382 | 383 | $update = [ 384 | $builder->getModel()->getStatusColumn() => $status, 385 | $builder->getModel()->getModeratedAtColumn() => Carbon::now() 386 | ]; 387 | //if moderated_by in enabled then append it to the update 388 | if ($moderated_by = $builder->getModel()->getModeratedByColumn()) { 389 | $update[$builder->getModel()->getModeratedByColumn()] = \Auth::user()->getKey(); 390 | } 391 | return $builder->update($update); 392 | } 393 | 394 | /** 395 | * Determine if the given where clause is a moderation constraint. 396 | * 397 | * @param array $where 398 | * @param string $column 399 | * @return bool 400 | */ 401 | protected function isModerationConstraint(array $where, $column) 402 | { 403 | return $where['column'] == $column; 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /src/ModerationServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 17 | __DIR__.'/config/moderation.php' => config_path('moderation.php') 18 | ], 'config'); 19 | } 20 | 21 | /** 22 | * Register the application services. 23 | * 24 | * @return void 25 | */ 26 | public function register() 27 | { 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Status.php: -------------------------------------------------------------------------------- 1 | 'status', 9 | 10 | /* 11 | |-------------------------------------------------------------------------- 12 | | Moderated At column 13 | |-------------------------------------------------------------------------- 14 | */ 15 | 'moderated_at_column' => 'moderated_at', 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Moderated By column 20 | |-------------------------------------------------------------------------- 21 | | Moderated by column is disabled by default. 22 | | If you want to include the id of the user who moderated a resource set 23 | | here the name of the column. 24 | | REMEMBER to migrate the database to add this column. 25 | */ 26 | 'moderated_by_column' => null, 27 | 28 | /* 29 | |-------------------------------------------------------------------------- 30 | | Strict Moderation 31 | |-------------------------------------------------------------------------- 32 | | If Strict Moderation is set to true then the default query will return 33 | | only approved resources. 34 | | In other case, all resources except Rejected ones, will returned as well. 35 | */ 36 | 'strict' => true, 37 | ); -------------------------------------------------------------------------------- /tests/BaseTestCase.php: -------------------------------------------------------------------------------- 1 | \Carbon\Carbon::now()], $overrides)); 20 | $posts->push($post); 21 | } 22 | 23 | return (count($posts) > 1) ? $posts : $posts[0]; 24 | } 25 | 26 | 27 | function actingAsUser() 28 | { 29 | $userModel = config('auth.providers.users.model', config('auth.model', 'App\User')); 30 | return $this->actingAs($userModel::create(['name' => 'tester', 'email' => mt_rand(1,9999).'tester@test.com', 'password' => 'password'])); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/ModerationScopeTest.php: -------------------------------------------------------------------------------- 1 | status_column = 'status'; 23 | $this->moderated_at_column = 'moderated_at'; 24 | $this->moderated_by_column = 'moderated_by'; 25 | 26 | //create a user and login 27 | $this->actingAsUser(); 28 | 29 | Post::$strictModeration = true; 30 | } 31 | 32 | /** @test */ 33 | public function it_returns_only_approved_stories() 34 | { 35 | $this->createPost([$this->status_column => Status::APPROVED], 5); 36 | $posts = Post::all(); 37 | $this->assertNotEmpty($posts); 38 | foreach ($posts as $post) { 39 | $this->assertEquals(Status::APPROVED, $post->{$this->status_column}); 40 | } 41 | } 42 | 43 | /** @test */ 44 | public function it_returns_only_rejected_stories() 45 | { 46 | $this->createPost([$this->status_column => Status::REJECTED], 5); 47 | 48 | $posts = (new Post)->newQueryWithoutScope(new ModerationScope)->rejected()->get(); 49 | 50 | $this->assertNotEmpty($posts); 51 | 52 | foreach ($posts as $post) { 53 | $this->assertEquals(Status::REJECTED, $post->{$this->status_column}); 54 | } 55 | } 56 | 57 | /** @test */ 58 | public function it_returns_only_pending_stories() 59 | { 60 | $this->createPost([$this->status_column => Status::PENDING], 5); 61 | 62 | $posts = (new Post)->newQueryWithoutScope(new ModerationScope)->pending()->get(); 63 | 64 | $this->assertNotEmpty($posts); 65 | 66 | foreach ($posts as $post) { 67 | $this->assertEquals(Status::PENDING, $post->{$this->status_column}); 68 | } 69 | } 70 | 71 | /** @test */ 72 | public function it_returns_stories_including_pending_ones() 73 | { 74 | $this->createPost([$this->status_column => Status::PENDING], 5); 75 | 76 | $posts = (new Post)->newQueryWithoutScope(new ModerationScope)->withPending()->get(); 77 | 78 | $this->assertNotEmpty($posts); 79 | 80 | //with pending will return more stories than only approved 81 | $this->assertTrue($posts > Post::all()); 82 | 83 | foreach ($posts as $post) { 84 | $this->assertTrue(($post->{$this->status_column} == Status::APPROVED || $post->{$this->status_column} == Status::PENDING)); 85 | } 86 | } 87 | 88 | /** @test */ 89 | public function it_returns_stories_including_rejected_ones() 90 | { 91 | $this->createPost([$this->status_column => Status::REJECTED], 5); 92 | 93 | $posts = (new Post)->newQueryWithoutScope(new ModerationScope)->withRejected()->get(); 94 | 95 | $this->assertNotEmpty($posts); 96 | 97 | //with rejected will return more stories than only approved 98 | $this->assertTrue($posts > Post::all()); 99 | 100 | foreach ($posts as $post) { 101 | $this->assertTrue(($post->{$this->status_column} == Status::APPROVED || $post->{$this->status_column} == Status::REJECTED)); 102 | } 103 | } 104 | 105 | /** @test */ 106 | public function it_returns_stories_including_postponed_ones() 107 | { 108 | $this->createPost([$this->status_column => Status::POSTPONED], 5); 109 | 110 | $posts = (new Post)->newQueryWithoutScope(new ModerationScope)->withPostponed()->get(); 111 | 112 | $this->assertNotEmpty($posts); 113 | 114 | //with rejected will return more stories than only approved 115 | $this->assertTrue($posts > Post::all()); 116 | 117 | foreach ($posts as $post) { 118 | $this->assertTrue(($post->{$this->status_column} == Status::APPROVED || $post->{$this->status_column} == Status::POSTPONED)); 119 | } 120 | } 121 | 122 | /** @test */ 123 | public function it_returns_all_stories() 124 | { 125 | $this->createPost([], 5); 126 | $posts = (new Post)->newQueryWithoutScope(new ModerationScope)->withAnyStatus()->get(); 127 | $allStories = Post::all() 128 | ->merge(Post::pending()->get()) 129 | ->merge(Post::rejected()->get()); 130 | 131 | $this->assertNotEmpty($posts); 132 | 133 | //with rejected will return more stories than only approved 134 | $this->assertCount(count($posts), $allStories); 135 | } 136 | 137 | /** @test */ 138 | public function it_approves_stories() 139 | { 140 | $posts = $this->createPost([$this->status_column => Status::PENDING], 4); 141 | $postsIds = $posts->pluck('id')->all(); 142 | 143 | (new Post)->newQueryWithoutScope(new ModerationScope)->whereIn('id', $postsIds)->approve(); 144 | 145 | foreach ($postsIds as $postId) { 146 | $this->assertDatabaseHas('posts', ['id' => $postId, $this->status_column => Status::APPROVED]); 147 | } 148 | } 149 | 150 | /** @test */ 151 | public function it_rejects_stories() 152 | { 153 | $posts = $this->createPost([$this->status_column => Status::PENDING], 4); 154 | $postsIds = $posts->pluck('id')->all(); 155 | 156 | (new Post)->newQueryWithoutScope(new ModerationScope)->whereIn('id', $postsIds)->reject(); 157 | 158 | foreach ($postsIds as $postId) { 159 | $this->assertDatabaseHas('posts', ['id' => $postId, $this->status_column => Status::REJECTED]); 160 | } 161 | } 162 | 163 | /** @test */ 164 | public function it_postpones_stories() 165 | { 166 | $posts = $this->createPost([$this->status_column => Status::PENDING], 4); 167 | $postsIds = $posts->pluck('id')->all(); 168 | 169 | (new Post)->newQueryWithoutScope(new ModerationScope)->whereIn('id', $postsIds)->postpone(); 170 | 171 | foreach ($postsIds as $postId) { 172 | $this->assertDatabaseHas('posts', ['id' => $postId, $this->status_column => Status::POSTPONED]); 173 | } 174 | } 175 | 176 | /** @test */ 177 | public function it_approves_a_story_by_id() 178 | { 179 | $post = $this->createPost([$this->status_column => Status::PENDING]); 180 | 181 | (new Post)->newQueryWithoutScope(new ModerationScope)->approve($post->id); 182 | 183 | $this->assertDatabaseHas('posts', 184 | [ 185 | 'id' => $post->id, 186 | $this->status_column => Status::APPROVED, 187 | $this->moderated_at_column => \Carbon\Carbon::now() 188 | ]); 189 | } 190 | 191 | /** @test */ 192 | public function it_rejects_a_story_by_id() 193 | { 194 | $post = $this->createPost([$this->status_column => Status::PENDING]); 195 | 196 | (new Post)->newQueryWithoutScope(new ModerationScope)->reject($post->id); 197 | 198 | $this->assertDatabaseHas('posts', 199 | [ 200 | 'id' => $post->id, 201 | $this->status_column => Status::REJECTED, 202 | $this->moderated_at_column => \Carbon\Carbon::now() 203 | ]); 204 | } 205 | 206 | /** @test */ 207 | public function it_postpones_a_story_by_id() 208 | { 209 | $post = $this->createPost([$this->status_column => Status::PENDING]); 210 | 211 | (new Post)->newQueryWithoutScope(new ModerationScope)->postpone($post->id); 212 | 213 | $this->assertDatabaseHas('posts', 214 | [ 215 | 'id' => $post->id, 216 | $this->status_column => Status::POSTPONED, 217 | $this->moderated_at_column => \Carbon\Carbon::now() 218 | ]); 219 | } 220 | 221 | /** @test */ 222 | public function it_updates_moderated_by_column_on_status_update() 223 | { 224 | //set moderated by column globally 225 | \Illuminate\Support\Facades\Config::set('moderation.moderated_by_column', 'moderated_by'); 226 | 227 | $posts = $this->createPost([$this->status_column => Status::PENDING], 3); 228 | 229 | (new Post)->newQueryWithoutScope(new ModerationScope)->where('id', '=', $posts[0]->id)->postpone(); 230 | (new Post)->newQueryWithoutScope(new ModerationScope)->where('id', '=', $posts[1]->id)->approve(); 231 | (new Post)->newQueryWithoutScope(new ModerationScope)->where('id', '=', $posts[2]->id)->reject(); 232 | 233 | foreach ($posts as $post) { 234 | $this->assertDatabaseHas('posts', 235 | [ 236 | 'id' => $post->id, 237 | $this->moderated_by_column => \Auth::user()->id 238 | ]); 239 | } 240 | } 241 | 242 | /** @test */ 243 | public function it_updates_moderated_by_column_on_status_update_by_id() 244 | { 245 | //set moderated by column globally 246 | \Illuminate\Support\Facades\Config::set('moderation.moderated_by_column', 'moderated_by'); 247 | 248 | $posts = $this->createPost([$this->status_column => Status::PENDING], 3); 249 | 250 | (new Post)->newQueryWithoutScope(new ModerationScope)->postpone($posts[0]->id); 251 | (new Post)->newQueryWithoutScope(new ModerationScope)->approve($posts[1]->id); 252 | (new Post)->newQueryWithoutScope(new ModerationScope)->reject($posts[2]->id); 253 | 254 | foreach ($posts as $post) { 255 | $this->assertDatabaseHas('posts', 256 | [ 257 | 'id' => $post->id, 258 | $this->moderated_by_column => \Auth::user()->id 259 | ]); 260 | } 261 | } 262 | 263 | /** @test */ 264 | public function it_returns_approved_and_pending_stories_when_not_in_strict_mode() 265 | { 266 | Post::$strictModeration = false; 267 | 268 | $this->createPost([$this->status_column => Status::PENDING], 4); 269 | $this->createPost([$this->status_column => Status::APPROVED], 2); 270 | 271 | $posts = Post::all(); 272 | 273 | $pendingCount = count(Post::pending()->get()); 274 | $this->assertTrue($posts->count() > $pendingCount); 275 | 276 | $this->assertNotEmpty($posts); 277 | 278 | foreach ($posts as $post) { 279 | $this->assertTrue(($post->{$this->status_column} == Status::APPROVED || $post->{$this->status_column} == Status::PENDING)); 280 | } 281 | } 282 | 283 | /** @test */ 284 | public function it_queries_pending_stories_by_default_when_not_in_strict_mode() 285 | { 286 | Post::$strictModeration = false; 287 | 288 | $posts = $this->createPost([$this->status_column => Status::PENDING], 5); 289 | $postsIds = $posts->pluck('id')->all(); 290 | 291 | $postsReturned = Post::whereIn('id', $postsIds)->get(); 292 | 293 | $this->assertCount(5, $postsReturned); 294 | 295 | foreach ($posts as $post) { 296 | $this->assertTrue(($post->{$this->status_column} == Status::PENDING)); 297 | } 298 | } 299 | 300 | /** @test */ 301 | public function it_queries_approved_stories_when_not_in_strict_mode() 302 | { 303 | $this->createPost([$this->status_column => Status::APPROVED], 5); 304 | 305 | $posts = (new Post)->newQueryWithoutScope(new ModerationScope)->approved()->get(); 306 | 307 | $this->assertNotEmpty($posts); 308 | 309 | foreach ($posts as $post) { 310 | $this->assertEquals(Status::APPROVED, $post->{$this->status_column}); 311 | } 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /tests/ModerationTraitTest.php: -------------------------------------------------------------------------------- 1 | status_column = 'status'; 22 | $this->moderated_at_column = 'moderated_at'; 23 | 24 | Post::$strictModeration = true; 25 | } 26 | 27 | /** @test */ 28 | public function it_returns_only_rejected_stories() 29 | { 30 | $this->createPost([$this->status_column => Status::REJECTED], 5); 31 | 32 | $posts = Post::rejected()->get(); 33 | 34 | $this->assertNotEmpty($posts); 35 | 36 | foreach ($posts as $post) { 37 | $this->assertEquals(Status::REJECTED, $post->{$this->status_column}); 38 | } 39 | } 40 | 41 | /** @test */ 42 | public function it_returns_only_pending_stories() 43 | { 44 | $this->createPost([$this->status_column => Status::PENDING], 5); 45 | 46 | $posts = Post::pending()->get(); 47 | 48 | $this->assertNotEmpty($posts); 49 | 50 | foreach ($posts as $post) { 51 | $this->assertEquals(Status::PENDING, $post->status); 52 | } 53 | } 54 | 55 | /** @test */ 56 | public function it_returns_only_postponed_stories() 57 | { 58 | $this->createPost([$this->status_column => Status::POSTPONED], 5); 59 | 60 | $posts = Post::postponed()->get(); 61 | 62 | $this->assertNotEmpty($posts); 63 | 64 | foreach ($posts as $post) { 65 | $this->assertEquals(Status::POSTPONED, $post->status); 66 | } 67 | } 68 | 69 | /** @test */ 70 | public function it_approves_a_story_by_id() 71 | { 72 | $post = $this->createPost([$this->status_column => Status::PENDING]); 73 | 74 | Post::approve($post->id); 75 | 76 | $this->assertDatabaseHas('posts', 77 | ['id' => $post->id, $this->status_column => Status::APPROVED, $this->moderated_at_column => \Carbon\Carbon::now()]); 78 | } 79 | 80 | /** @test */ 81 | public function it_rejects_a_story_by_id() 82 | { 83 | $post = $this->createPost([$this->status_column => Status::PENDING]); 84 | 85 | Post::reject($post->id); 86 | 87 | $this->assertDatabaseHas('posts', 88 | ['id' => $post->id, $this->status_column => Status::REJECTED, $this->moderated_at_column => \Carbon\Carbon::now()]); 89 | } 90 | 91 | /** @test */ 92 | public function it_postpones_a_story_by_id() 93 | { 94 | $post = $this->createPost([$this->status_column => Status::PENDING]); 95 | 96 | Post::postpone($post->id); 97 | 98 | $this->assertDatabaseHas('posts', 99 | ['id' => $post->id, $this->status_column => Status::POSTPONED, $this->moderated_at_column => \Carbon\Carbon::now()]); 100 | } 101 | 102 | /** @test */ 103 | public function it_pendings_a_story_by_id() 104 | { 105 | $post = $this->createPost([$this->status_column => Status::APPROVED]); 106 | 107 | Post::pend($post->id); 108 | 109 | $this->assertDatabaseHas('posts', 110 | ['id' => $post->id, $this->status_column => Status::PENDING, $this->moderated_at_column => \Carbon\Carbon::now()]); 111 | } 112 | 113 | /** @test */ 114 | public function it_determines_if_story_is_approved() 115 | { 116 | $postApproved = $this->createPost([$this->status_column => Status::APPROVED]); 117 | $postPending = $this->createPost([$this->status_column => Status::PENDING]); 118 | $postRejected = $this->createPost([$this->status_column => Status::REJECTED]); 119 | 120 | $this->assertTrue($postApproved->isApproved()); 121 | $this->assertFalse($postPending->isApproved()); 122 | $this->assertFalse($postRejected->isApproved()); 123 | } 124 | 125 | /** @test */ 126 | public function it_determines_if_story_is_rejected() 127 | { 128 | $postApproved = $this->createPost([$this->status_column => Status::APPROVED]); 129 | $postPending = $this->createPost([$this->status_column => Status::PENDING]); 130 | $postRejected = $this->createPost([$this->status_column => Status::REJECTED]); 131 | 132 | $this->assertFalse($postApproved->isRejected()); 133 | $this->assertFalse($postPending->isRejected()); 134 | $this->assertTrue($postRejected->isRejected()); 135 | } 136 | 137 | /** @test */ 138 | public function it_determines_if_story_is_pending() 139 | { 140 | $postApproved = $this->createPost([$this->status_column => Status::APPROVED]); 141 | $postPending = $this->createPost([$this->status_column => Status::PENDING]); 142 | $postRejected = $this->createPost([$this->status_column => Status::REJECTED]); 143 | 144 | $this->assertFalse($postApproved->isPending()); 145 | $this->assertTrue($postPending->isPending()); 146 | $this->assertFalse($postRejected->isPending()); 147 | } 148 | 149 | /** @test */ 150 | public function it_determines_if_story_is_postponed() 151 | { 152 | $postApproved = $this->createPost([$this->status_column => Status::APPROVED]); 153 | $postPending = $this->createPost([$this->status_column => Status::PENDING]); 154 | $postRejected = $this->createPost([$this->status_column => Status::REJECTED]); 155 | $postPostponed = $this->createPost([$this->status_column => Status::POSTPONED]); 156 | 157 | $this->assertFalse($postApproved->isPostponed()); 158 | $this->assertFalse($postPending->isPostponed()); 159 | $this->assertFalse($postRejected->isPostponed()); 160 | $this->assertTrue($postPostponed->isPostponed()); 161 | } 162 | 163 | /** @test */ 164 | public function it_casts_moderated_at_attribute_as_a_date(){ 165 | $post = $this->createPost(); 166 | Post::approve($post->id); 167 | 168 | //reload the instance 169 | $post = Post::find($post->id); 170 | 171 | $this->assertInstanceOf(\Carbon\Carbon::class, $post->{$this->moderated_at_column}); 172 | } 173 | 174 | /** @test */ 175 | public function it_deletes_rejected_resources(){ 176 | $post = $this->createPost([$this->status_column => Status::REJECTED]); 177 | 178 | $postDel = Post::withRejected()->where('id', $post->id)->first(); 179 | $postDel->delete(); 180 | 181 | $this->assertDatabaseMissing('posts',['id' => $post->id]); 182 | } 183 | 184 | /** @test */ 185 | public function it_deletes_resources_of_any_status(){ 186 | $posts = $this->createPost([], 4); 187 | Post::approve($posts[0]->id); 188 | Post::reject($posts[1]->id); 189 | Post::postpone($posts[2]->id); 190 | 191 | foreach ($posts as $post) { 192 | $post->delete(); 193 | } 194 | 195 | $this->assertDatabaseMissing('posts',['id' => $posts[0]->id]); 196 | $this->assertDatabaseMissing('posts',['id' => $posts[1]->id]); 197 | $this->assertDatabaseMissing('posts',['id' => $posts[2]->id]); 198 | } 199 | 200 | /** @test */ 201 | public function it_marks_as_approved_an_instance() 202 | { 203 | $post = $this->createPost([$this->status_column => Status::PENDING]); 204 | 205 | $post->markApproved(); 206 | 207 | $this->assertEquals(Status::APPROVED, $post->status); 208 | 209 | $this->assertDatabaseHas('posts', 210 | ['id' => $post->id, $this->status_column => Status::APPROVED, $this->moderated_at_column => \Carbon\Carbon::now()]); 211 | } 212 | 213 | /** @test */ 214 | public function it_marks_as_rejected_an_instance() 215 | { 216 | $post = $this->createPost([$this->status_column => Status::PENDING]); 217 | 218 | $post->markRejected(); 219 | 220 | $this->assertEquals(Status::REJECTED, $post->status); 221 | 222 | $this->assertDatabaseHas('posts', 223 | ['id' => $post->id, $this->status_column => Status::REJECTED, $this->moderated_at_column => \Carbon\Carbon::now()]); 224 | } 225 | 226 | /** @test */ 227 | public function it_marks_as_postponed_an_instance() 228 | { 229 | $post = $this->createPost([$this->status_column => Status::PENDING]); 230 | 231 | $post->markPostponed(); 232 | 233 | $this->assertEquals(Status::POSTPONED, $post->status); 234 | 235 | $this->assertDatabaseHas('posts', 236 | ['id' => $post->id, $this->status_column => Status::POSTPONED, $this->moderated_at_column => \Carbon\Carbon::now()]); 237 | } 238 | 239 | /** @test */ 240 | public function it_marks_as_pending_an_instance() 241 | { 242 | $post = $this->createPost([$this->status_column => Status::PENDING]); 243 | 244 | $post->markPending(); 245 | 246 | $this->assertEquals(Status::PENDING, $post->status); 247 | 248 | $this->assertDatabaseHas('posts', 249 | ['id' => $post->id, $this->status_column => Status::PENDING, $this->moderated_at_column => \Carbon\Carbon::now()]); 250 | } 251 | 252 | } 253 | -------------------------------------------------------------------------------- /tests/Post.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->smallInteger('status')->default(0); 19 | $table->dateTime('moderated_at'); 20 | $table->integer('moderated_by')->nullable()->unsigned(); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::drop('posts'); 33 | } 34 | } 35 | --------------------------------------------------------------------------------