Merge branch 'develop' into trunk

This commit is contained in:
Jérémy Munsch 2021-06-28 17:34:24 +02:00
commit 98d0edb024
19 changed files with 249 additions and 27 deletions

View file

@ -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'),

View file

@ -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
*

View file

@ -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) {

View file

@ -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 => []]);
}
}
}
}

View file

@ -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

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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');

View file

@ -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();
});
}
/**

View file

@ -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
*

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddSoftDeleteOnUsers extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropSoftDeletes();
});
}
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddSoftDeleteOnProjects extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('projects', function (Blueprint $table) {
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('projects', function (Blueprint $table) {
$table->dropSoftDeletes();
});
}
}

View file

@ -18,7 +18,7 @@
@endcan
</div>
<div class="card-body">
<form action="{{ route("$modelTable.store") }}" method="POST">
<form action="{{ route("$modelTable.store") }}" method="POST" @if($hasImage) enctype="multipart/form-data" @endif>
@csrf
<table class="table table-sm table-bordered border-primary align-middle table-striped text-center">
<thead class="thead-dark">

View file

@ -30,7 +30,7 @@
</div>
</div>
<div class="card-body">
<form action="{{ route("$modelTable.update", $model) }}" method="POST">
<form action="{{ route("$modelTable.update", $model) }}" method="POST" @if($hasImage) enctype="multipart/form-data" @endif>
@method('put')
@csrf
<table class="table table-sm table-bordered border-primary align-middle table-striped text-center">

View file

@ -23,6 +23,9 @@ $readonly = is_callable($prop['readonly']) ? $prop['readonly']() : $prop['readon
@case(CrudType::json())
@include('crud.formfields.json')
@break
@case(CrudType::image())
@include('crud.formfields.image')
@break
@case(CrudType::enum())
@include('crud.formfields.enum')
@break

View file

@ -0,0 +1,26 @@
<td>
<label for="{{ $fieldName }}" class="form-label">{{ $prop['label'] }}</label>
</td>
<td>
@if(strlen($prop['placeholder']))
<div class="form-floating">
@endif
<input
id="{{ $fieldName }}"
name="{{ $fieldName }}"
type="file"
class="form-control"
value="{{ old($fieldName) ?? $model->{$fieldName} ?? '' }}"
@if(isset($prop['rules']['max']))maxlength="{{ $prop['rules']['max'] }}"@endif
@if(isset($prop['rules']['min']))minlength="{{ $prop['rules']['min'] }}"@endif
@if($prop['disabled'])disabled @endif
@if($readonly)readonly @endif
@if(($prop['required'] or (!$prop['required'] and !$prop['nullable'])) and !(isset($model) and $model->{$fieldName}))required @endif>
@if(strlen($prop['placeholder']))
<label for="{{ $fieldName }}" class="form-label">{{ __('ex: :placeholder', ['placeholder' => $prop['placeholder']]) }}</label>
</div>
@endif
@if(isset($model) and $model->{$fieldName})
<img class="img-responsive" src="{{ sprintf('/storage/%s', $model->{$fieldName}) }}" alt="{{ sprintf('/storage/%s', $model->{$fieldName}) }}">
@endif
</td>

View file

@ -16,6 +16,9 @@
@case(CrudType::boolean())
@if($model->{$fieldName})OUI @else NON @endif
@break
@case(CrudType::image())
<img class="img-responsive" src="{{ sprintf('/storage/%s', $model->{$fieldName}) }}" alt="{{ sprintf('/storage/%s', $model->{$fieldName}) }}">
@break
@default
{{ $field }}
@endswitch
@ -29,9 +32,15 @@
@case(CrudType::enum())
{{ $model->{$fieldName}->label }}
@break
@case(CrudType::json())
@json($field)
@break
@case(CrudType::boolean())
@if($model->{$fieldName})OUI @else NON @endif
@break
@case(CrudType::image())
<img class="img-responsive" src="{{ sprintf('/storage/%s', $model->{$fieldName}) }}" alt="{{ sprintf('/storage/%s', $model->{$fieldName}) }}">
@break
@case(CrudType::belongsTo())
{{ $prop['belongsTo']::find($model->{$fieldName})->{$model->crudLabelColumn} }}
@break

View file

@ -1,10 +1,17 @@
@include('modules.error-messages')
@php
$userPr = Auth::user()->projects;
@endphp
<div class="form-row">
<div class="form-group col">
<label for="project" class="col-form-label">{{ __('Projet') }}</label>
<select class="form-select" name="project_id" id="project">
@foreach($projects as $project)
<option value="{{ $project->id }}" @if ($project->id == old('project_id') or (isset($ticket) and $ticket->project->id === $project->id))selected @endif>{{ $project->name }}</option>
@if (Auth::user()->role->equals(Role::admin()))
<option value="{{ $project->id }}" @if ($project->id == old('project_id') or (isset($ticket) and $ticket->project->id == $project->id))selected @endif>{{ $project->name }}</option>
@elseif ($userPr->filter(function($pr) use($project) { return $pr->id === $project->id; })->count())
<option value="{{ $project->id }}" selected>{{ $project->name }}</option>
@endif
@endforeach
</select>
</div>

View file

@ -16,16 +16,14 @@
@endif
<a href="{{ route('ticket.index', ['rst' => 1]) }}" class="btn btn-dark">{{ __('Réinitialiser le tri') }}</a>
</div>
@if(auth()->user()->role->equals(Role::admin()))
<div class="float-end me-2">
<select class="form-select" onchange="window.location='{{ route('ticket.index') }}?'+(this.value == -1 ? 'prs=-1' : 'pr='+this.value);">
<option value="-1">{{ __('Tous') }}</option>
@foreach($projects as $project)
@foreach(auth()->user()->projects as $project)
<option value="{{$project->id}}" @if(intval(session('tickets.pr')) == $project->id)selected="selected" @endif>{{$project->name}}</option>
@endforeach
</select>
</div>
@endif
</div>
</div>
<div class="row">