diff --git a/app/Enums/CrudType.php b/app/Enums/CrudType.php index b5f8301..f0b8a1f 100644 --- a/app/Enums/CrudType.php +++ b/app/Enums/CrudType.php @@ -10,6 +10,7 @@ use Kwaadpepper\Enum\BaseEnum; * @method static self string() * @method static self text() * @method static self json() + * @method static self image() * @method static self email() * @method static self password() * @method static self float() @@ -28,12 +29,13 @@ class CrudType extends BaseEnum 'string' => 2, 'text' => 3, 'json' => 4, - 'email' => 5, - 'password' => 6, - 'float' => 7, - 'enum' => 8, - 'belongsTo' => 9, - 'belongsToMany' => 10 + 'image' => 5, + 'email' => 6, + 'password' => 7, + 'float' => 8, + 'enum' => 9, + 'belongsTo' => 10, + 'belongsToMany' => 11 ]; } @@ -45,6 +47,7 @@ class CrudType extends BaseEnum 'string' => trans('Chaîne de charactères simple'), 'text' => trans('Texte'), 'json' => trans('Json'), + 'image' => trans('Image'), 'email' => trans('Chaîne de charactères email'), 'password' => trans('Mot de passe'), 'float' => trans('Nombre à virgule flotante'), diff --git a/app/Http/Controllers/CrudController.php b/app/Http/Controllers/CrudController.php index 238af0e..ec6e447 100644 --- a/app/Http/Controllers/CrudController.php +++ b/app/Http/Controllers/CrudController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use App\Enums\CrudType; use App\Exceptions\CrudException; use App\Http\Requests\CrudIndexRequest; use App\Http\Requests\StoreCrudModel; @@ -9,6 +10,7 @@ use App\Http\Requests\UpdateCrudModel; use App\Http\Resources\CrudResource; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Session; @@ -67,6 +69,7 @@ class CrudController extends Controller if (!request()->ajax() and !app()->runningInConsole()) { self::shareCrudModelsClassesToView(); $this->shareModelClassToView(); + $this->shareModelHasImageToView(); $this->shareModelTableToView(); $this->shareModelPropsToView(); } @@ -142,7 +145,7 @@ class CrudController extends Controller $model->saveRelations($request); $this->storedModel($model); return \redirect()->route(sprintf('%s.edit', $model->getTable()), $model) - ->with('success', trans(':model à bien été enregisté.', ['model' => $model->getModelName()])); + ->with('success', trans(':model a bien été enregisté.', ['model' => $model->getModelName()])); } // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass @@ -159,12 +162,15 @@ class CrudController extends Controller $model->saveRelations($request); $this->updatedModel($model); return \redirect()->route(sprintf('%s.edit', $model->getTable()), $model) - ->with('success', trans(':model à bien été enregisté.', ['model' => $model->getModelName()])); + ->with('success', trans(':model a bien été enregisté.', ['model' => $model->getModelName()])); } public function destroy(Model $model) { - if ($restrict = $model->gotHasManyRelationWithRestrictOnDelete()) { + if ( + !in_array(SoftDeletes::class, class_uses_recursive($model), true) and + $restrict = $model->gotHasManyRelationWithRestrictOnDelete() + ) { return redirect()->back() ->with('error', trans( ':model n\'as pas pu être supprimé car il est encore associé avec des :associated', @@ -176,7 +182,7 @@ class CrudController extends Controller } if ($model->delete()) { return \redirect()->route(sprintf('%s.index', $model->getTable())) - ->with('warning', trans(':model à bien été supprimé.', ['model' => $model->getModelName()])); + ->with('warning', trans(':model a bien été supprimé.', ['model' => $model->getModelName()])); } else { return redirect()->back() ->with('error', trans(':model n\'as pas pu être supprimé', ['model' => $model->getModelName()])); @@ -215,6 +221,21 @@ class CrudController extends Controller View::share('modelClass', static::$modelClass); } + /** + * Get the current model class + * + * @return void + */ + private function shareModelHasImageToView(): void + { + /** @var \Illuminate\Database\Eloquent\Model $model */ + $model = static::$modelClass; + + View::share('hasImage', (bool)collect((new $model())->getEditableProperties())->filter(function ($prop) { + return $prop['type']->equals(CrudType::image()); + })->count()); + } + /** * Get the current model table * diff --git a/app/Http/Controllers/TicketController.php b/app/Http/Controllers/TicketController.php index 329e216..d9fc380 100644 --- a/app/Http/Controllers/TicketController.php +++ b/app/Http/Controllers/TicketController.php @@ -39,7 +39,7 @@ class TicketController extends Controller { /** @var \App\Models\User */ $user = auth()->user(); - $tickets = Ticket::with('project'); + $tickets = Ticket::with('project')->has('project'); if (!$user->role->equals(Role::admin())) { $tickets = $tickets->whereHas('project', function ($q) use ($user) { $q->whereHas('users', function ($q) use ($user) { diff --git a/app/Http/Requests/CrudRequest.php b/app/Http/Requests/CrudRequest.php index f2d06f8..a7a3cbf 100644 --- a/app/Http/Requests/CrudRequest.php +++ b/app/Http/Requests/CrudRequest.php @@ -6,9 +6,8 @@ use App\Enums\CrudAction; use App\Enums\CrudType; use App\Exceptions\CrudException; use App\Http\Controllers\CrudController; -use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Str; use Illuminate\Foundation\Http\FormRequest; -use App\Traits\IsCrudModel; class CrudRequest extends FormRequest { @@ -24,6 +23,7 @@ class CrudRequest extends FormRequest */ public function getRulesForAction(CrudAction $action): array { + $model = $this->getCurrentModel(); $props = $this->getCurrentModel()::getEditableProperties(); $propsRules = []; foreach ($this->getCurrentModel()::getRules() as $k => $rules) { @@ -48,10 +48,21 @@ class CrudRequest extends FormRequest if ($props[$k]['type']->equals(CrudType::boolean())) { $this->merge([$k => $this->{$k} ? 1 : 0]); } + // handle image + if ($props[$k]['type']->equals(CrudType::image())) { + $tableName = Str::singular((new $model())->getTable()); + if ($this->{$tableName}) { + $i = array_search('required', $propsRules[$k]); + unset($propsRules[$k][$i]); + } + } // handle belongsToMany if ($props[$k]['type']->equals(CrudType::belongsToMany())) { $propsRules["$k.*"][0] = 'exists:' . (new $props[$k]['belongsToMany']())->getTable() . ',id'; + if (!isset($this->{$k})) { + $this->merge([$k => []]); + } } } } diff --git a/app/Http/Requests/StoreTicket.php b/app/Http/Requests/StoreTicket.php index 4f73e40..306b01a 100644 --- a/app/Http/Requests/StoreTicket.php +++ b/app/Http/Requests/StoreTicket.php @@ -2,6 +2,7 @@ namespace App\Http\Requests; +use App\Enums\Role; use App\Enums\TicketStatus; use App\Enums\TicketType; use Illuminate\Foundation\Http\FormRequest; @@ -15,6 +16,11 @@ class StoreTicket extends FormRequest */ public function authorize() { + if (!auth()->user()->role->equals(Role::admin())) { + $this->merge([ + 'status' => TicketStatus::new()->value, + ]); + } return auth()->check(); } @@ -26,7 +32,18 @@ class StoreTicket extends FormRequest public function rules() { return [ - 'project_id' => 'required|exists:projects,id', + 'project_id' => [ + 'required', + 'exists:projects,id', + // @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed + function ($attribute, $value, $fail) { + if (!in_array($value, auth()->user()->projects()->pluck('id')->all())) { + $fail(__('Veuillez choisir entre :values', [ + 'value' => auth()->user()->projects()->pluck('name')->implode(',') + ])); + } + } + ], 'type' => [ 'required', // @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed diff --git a/app/Http/Requests/UpdateTicket.php b/app/Http/Requests/UpdateTicket.php index 203416a..040bde7 100644 --- a/app/Http/Requests/UpdateTicket.php +++ b/app/Http/Requests/UpdateTicket.php @@ -9,6 +9,12 @@ class UpdateTicket extends StoreTicket { public function authorize() { - return auth()->check() and (auth()->user()->role->equals(Role::admin())); + if (!auth()->user()->role->equals(Role::admin())) { + $this->merge([ + 'status' => $this->project->status->value, + ]); + } + return auth()->check() and (auth()->user()->role->equals(Role::admin()) or + request()->route()->ticket->created_by === auth()->user()->id); } } diff --git a/app/Models/Project.php b/app/Models/Project.php index 28a236a..58bff7c 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -8,6 +8,7 @@ use App\Enums\Role; use App\Traits\IsCrudModel; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Facades\File; use Illuminate\Support\Str; @@ -15,6 +16,7 @@ class Project extends Model { use HasFactory; use IsCrudModel; + use SoftDeletes; protected $crudLabelColumn = 'name'; @@ -46,6 +48,7 @@ class Project extends Model 'type' => CrudType::belongsToMany(), 'belongsToMany' => User::class, 'belongsToManyLabel' => 'name', + 'nullable' => true, 'readonly' => function () { return auth()->user()->role->equals(Role::admin()) ? false : true; }, @@ -65,7 +68,7 @@ class Project extends Model static::created(function (Project $project) { $path = Project::ticketAssetsFullPath($project->slug_name); if (!File::exists($path)) { - File::makeDirectory($path); + File::makeDirectory($path, 0755, true); } }); static::updating(function (Project $project) { @@ -75,7 +78,7 @@ class Project extends Model if (File::exists($oldPath)) { File::move($oldPath, $newPath); } else { - File::makeDirectory($newPath); + File::makeDirectory($newPath, 0755, true); } }); static::deleted(function (Project $project) { diff --git a/app/Models/User.php b/app/Models/User.php index 76df3d3..cbf45df 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -7,8 +7,11 @@ use App\Enums\CrudType; use App\Enums\Role; use App\Traits\IsCrudModel; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Str; use Kwaadpepper\Enum\Traits\CastsEnums; class User extends Authenticatable @@ -17,6 +20,7 @@ class User extends Authenticatable use Notifiable; use CastsEnums; use IsCrudModel; + use SoftDeletes; protected $enumCasts = [ 'role' => Role::class @@ -81,7 +85,19 @@ class User extends Authenticatable 'required' => true, 'nullable' => false, 'validate' => [ - 'max:255' + 'max:255', + // @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed + function ($attribute, $value, $fail) { + $q = static::query(); + if (request()->user) { + $q = $q->where('id', '!=', $this->id); + } + if ($q->where('email', $value)->exists()) { + $fail(__(':attribute est déjà utilisé', [ + 'attribute' => $attribute + ])); + } + } ], 'actions' => [ CrudAction::viewAny(), @@ -108,6 +124,7 @@ class User extends Authenticatable 'type' => CrudType::belongsToMany(), 'belongsToMany' => Project::class, 'belongsToManyLabel' => 'name', + 'nullable' => true, 'readonly' => function () { return auth()->user()->role->equals(Role::admin()) ? false : true; }, @@ -122,6 +139,23 @@ class User extends Authenticatable parent::__construct($attributes); } + protected static function booted() + { + static::creating(function (User $user) { + if (\is_null($user->password)) { + $user->password = Str::random(10); + } + if (Hash::needsRehash($user->password)) { + $user->password = Hash::make($user->password); + } + }); + static::updating(function (User $user) { + if (Hash::needsRehash($user->password)) { + $user->password = Hash::make($user->password); + } + }); + } + public function tickets() { return $this->hasMany(Ticket::class, 'created_by'); diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index cd8fc15..2595412 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use App\Models\Project; +use App\Models\Ticket; use App\Models\User; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; @@ -51,6 +52,9 @@ class RouteServiceProvider extends ServiceProvider }); Route::model('user', User::class); Route::model('project', Project::class); + Route::bind('ticket', function ($value) { + return Ticket::where('id', $value)->has('project')->firstOrFail(); + }); } /** diff --git a/app/Traits/IsCrudModel.php b/app/Traits/IsCrudModel.php index 3dcf992..694d06e 100644 --- a/app/Traits/IsCrudModel.php +++ b/app/Traits/IsCrudModel.php @@ -7,6 +7,7 @@ use App\Enums\CrudType; use App\Exceptions\CrudException; use App\Http\Requests\CrudRequest; use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; use Kwaadpepper\Enum\BaseEnum; @@ -69,6 +70,9 @@ trait IsCrudModel ) { $this->{$k} = $attribute; } + if (\is_object($this->{$k}) and \get_class($this->{$k}) === UploadedFile::class) { + $this->moveUploadedFile($k, $this->{$k}); + } } } @@ -164,17 +168,20 @@ trait IsCrudModel return collect(static::$editableProperties)->mapWithKeys(function ($item, $key) { $item = \array_merge(self::$schemaCrud, $item); $defaultRules = []; - if (!$item['nullable']) { + if (!$item['nullable'] === true) { $defaultRules[] = 'required'; - } elseif ($item['required']) { + } elseif ($item['required'] === true) { $defaultRules[] = 'required'; - } elseif ($item['readonly']) { + } elseif ($item['readonly'] === true) { $defaultRules[] = 'prohibited'; } switch ($item['type']) { case CrudType::boolean(): $defaultRules[] = 'boolean'; break; + case CrudType::image(): + $defaultRules[] = 'file'; + break; case CrudType::email(): $defaultRules[] = 'email:rfc,dns'; break; @@ -212,6 +219,15 @@ trait IsCrudModel return class_basename(get_class($this)); } + private function moveUploadedFile(string $propName, UploadedFile &$file): void + { + $this->{$propName} = $file->storePubliclyAs( + $this->getTable(), + $file->hashName(), + ['disk' => 'public'] + ); + } + /** * Get the Editable Properties * diff --git a/database/migrations/2021_06_28_163754_add_soft_delete_on_users.php b/database/migrations/2021_06_28_163754_add_soft_delete_on_users.php new file mode 100644 index 0000000..cf19b99 --- /dev/null +++ b/database/migrations/2021_06_28_163754_add_soft_delete_on_users.php @@ -0,0 +1,32 @@ +softDeletes(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropSoftDeletes(); + }); + } +} diff --git a/database/migrations/2021_06_28_163755_add_soft_delete_on_projects.php b/database/migrations/2021_06_28_163755_add_soft_delete_on_projects.php new file mode 100644 index 0000000..b58a5fe --- /dev/null +++ b/database/migrations/2021_06_28_163755_add_soft_delete_on_projects.php @@ -0,0 +1,32 @@ +softDeletes(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('projects', function (Blueprint $table) { + $table->dropSoftDeletes(); + }); + } +} diff --git a/resources/views/crud/create.blade.php b/resources/views/crud/create.blade.php index ba950ff..9e17cea 100644 --- a/resources/views/crud/create.blade.php +++ b/resources/views/crud/create.blade.php @@ -18,7 +18,7 @@ @endcan