Merge branch 'develop' into trunk

This commit is contained in:
Noah Lancien 2025-07-03 16:19:14 +02:00
commit 2ec44a2584
18 changed files with 285 additions and 176 deletions

View file

@ -0,0 +1,34 @@
<?php
namespace App\Console\Commands;
use App\Jobs\UpdatePlanificatedStatus as UpdatePlanificatedStatusJob;
use Illuminate\Console\Command;
class UpdatePlanificatedStatus extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'uwil:update-planificated-status';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update models planificated status.';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
(new UpdatePlanificatedStatusJob())->handle();
$this->info('Models\' planificated status updated 👌');
}
}

View file

@ -2,6 +2,7 @@
namespace App\Console;
use App\Jobs\UpdatePlanificatedStatus;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@ -13,8 +14,9 @@ class Kernel extends ConsoleKernel
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
protected function schedule(Schedule $schedule): void
{
$schedule->job(UpdatePlanificatedStatus::class)->everyFiveMinutes();
}
/**
@ -22,7 +24,7 @@ class Kernel extends ConsoleKernel
*
* @return void
*/
protected function commands()
protected function commands(): void
{
$this->load(__DIR__ . '/Commands');

View file

@ -26,7 +26,7 @@ class Controller extends BaseController
*
* @param \Illuminate\Database\Eloquent\Builder $query The eloquent query builder.
* @param string $search The query string.
* @param callable $searchQuery The query string.
* @param callable|null $searchQuery The query string.
* @param string ...$fields Either a simple array or a string
* in case of a string the two fields
* would be concatenate to make the search.
@ -35,7 +35,7 @@ class Controller extends BaseController
protected function searchQuery(
Builder $query,
string $search,
callable $searchQuery = null,
callable|null $searchQuery = null,
string ...$fields
): \Illuminate\Database\Eloquent\Builder {
if (count($fields)) {
@ -200,7 +200,7 @@ class Controller extends BaseController
*/
protected function paginate(
Builder $query,
ItemsPerPaginationEnum $pagination = null
ItemsPerPaginationEnum|null $pagination = null
): \Illuminate\Contracts\Pagination\LengthAwarePaginator {
$currentRoutePath = Str::slug(request()->route()->getName());
$cacheKey = "pagination.{$currentRoutePath}";

View file

@ -22,7 +22,7 @@ class NewsController extends Controller
$sectionModels = Section::query()->where('page_id', $staticPageModel->id)->get();
$newsModels = News::query()
->where('published', true)
->orderBy('publish_at', 'DESC')
->orderBy('displayed_date', 'DESC')
->simplePaginate(4)
->fragment('corpo');
$lifeSection = $sectionModels->where('section', PageSectionEnum::move_life)->first();

View file

@ -20,6 +20,7 @@ use App\Models\Slide;
use App\Models\StaticPage;
use App\Models\Strip;
use App\Models\Testimonial;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
@ -34,13 +35,18 @@ class StaticPageController extends Controller
{
$staticPageModel = StaticPage::query()->where('type', StaticPageType::home)->first();
$sectionModels = Section::query()->where('page_id', $staticPageModel->id)->get();
$stripModels = Strip::query()->where('published', true)
$stripModels = Strip::query()
->where('published', true)
->orderBy('publish_at', 'DESC')
->limit(6)
->with('news')
->whereHas('news', function (Builder $query) {
$query->where('published', true);
})
->get();
$newsModels = News::query()->where('published', true)
->orderBy('publish_at', 'DESC')
$newsModels = News::query()
->where('published', true)
->orderBy('displayed_date', 'DESC')
->limit(4)
->get();
$slideModels = Slide::query()->where('type', SliderTypeEnum::home)->get();
@ -86,10 +92,10 @@ class StaticPageController extends Controller
$viewBox = null;
$initialZoom = null;
// * Calcul Viewbox leaflet map ().
// Carte  page “on se dévoile” : Point central = Laval + rayon = 100 km 
// Carte page “on se dévoile” : Point central = Laval + rayon = 100 km
/** @var \App\Models\Site|null $lavalSite */
// @phpstan-ignore-next-line
$lavalSite = $siteModelList->first(fn (Site $site) => Str::contains($site->name, 'Laval'));
$lavalSite = $siteModelList->first(fn(Site $site) => Str::contains($site->name, 'Laval'));
if ($lavalSite) {
$viewBox = (object)[
'lat' => $lavalSite->latitude,

View file

@ -53,6 +53,7 @@ class StoreNewsRequest extends FormRequest
'linkedin_url' => 'nullable|url',
'published' => 'required|boolean',
'publish_at' => 'nullable|date',
'displayed_date' => 'required|date',
'unpublish_at' => 'nullable|date|after:publish_at',
];
@ -84,6 +85,7 @@ class StoreNewsRequest extends FormRequest
'published' => trans('validation.custom.activated'),
'publish_at' => trans('validation.custom.publish_at'),
'unpublish_at' => trans('validation.custom.unpublish_at'),
'displayed_date' => trans('validation.custom.displayed_date'),
];
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace App\Jobs;
use App\Models\News;
use App\Models\Strip;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
/**
* Update published attribute reading its
* publish_at and unpublish_at attributes.
*/
class UpdatePlanificatedStatus
{
/**
* Called when the job will be executed.
*
* @return void
*/
public function handle(): void
{
// Set all models to apply status modifications.
collect([News::class, Strip::class])
->each(function (string $model) {
// Get all published model with an unpublish_at date passed.
/** @var \Illuminate\Database\Eloquent\Collection<array-key,\App\Models\News|\App\Models\Strip> $modelListToUnPublish */
$modelListToUnPublish = $model::query()
->whereNotNull('unpublish_at')
->where('unpublish_at', '<=', Carbon::now())
->where('published', true)
->get();
Log::debug(__CLASS__ . " model to unpublish " . $modelListToUnPublish->count());
// Unpublish all models with an unpublish_at date passed.
$modelListToUnPublish->each(function (News|Strip $modelToUnpublish) {
$modelToUnpublish->published = false;
$modelToUnpublish->saveOrFail();
});
// Get all unpublished model with a publish_at date passed.
/** @var \Illuminate\Database\Eloquent\Collection<array-key,\App\Models\News|\App\Models\Strip> $modelListToPublish */
$modelListToPublish = $model::query()
->where('publish_at', '<=', Carbon::now())
->whereNotNull('publish_at')
->where('published', false)
->get();
Log::debug(__CLASS__ . " model to publish " . $modelListToPublish->count());
// Publish all models with a publish_at date passed.
$modelListToPublish->each(function (News|Strip $modelToPublish) {
$modelToPublish->published = true;
$modelToPublish->saveOrFail();
});
});
}
}

View file

@ -105,7 +105,7 @@ class FileStorageHelper
Model $model,
$file,
bool $slugify = false,
string $filename = null,
string|null $filename = null,
bool $private = false
): string {
// phpcs:enable
@ -166,7 +166,7 @@ class FileStorageHelper
public static function storeModelFilesFromHtml(
Model $model,
string $htmlString,
string $oldHtmlString = null
string|null $oldHtmlString = null
): string {
\collect(self::onlyUploadedFilesWithFileManager(self::findFilesInHtml($htmlString)))
->each(function (string $path) use ($model, &$htmlString) {

View file

@ -5,6 +5,7 @@ namespace App\Models;
use App\Lib\Helpers\FileStorageHelper;
use App\Lib\Helpers\ToolboxHelper;
use App\Traits\Models\ActivityLog;
use App\Traits\Models\PlanificatedStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
@ -30,6 +31,7 @@ use Uwil\SitemapGenerator\Lib\SitemapHelper;
* @property boolean $published Publication status.
* @property \Carbon\Carbon|null $publish_at Publication date.
* @property \Carbon\Carbon|null $unpublish_at Unpublication status update date.
* @property \Carbon\Carbon $displayed_date News dipslayed date.
* @property-read \Carbon\Carbon $created_at Created date.
* @property-read \Carbon\Carbon $updated_at Updated date.
*
@ -39,6 +41,7 @@ class News extends Model implements Sitemapable
{
use ActivityLog;
use HasFactory;
use PlanificatedStatus;
/**
* The attributes that are mass assignable.
@ -63,31 +66,19 @@ class News extends Model implements Sitemapable
'published',
'publish_at',
'unpublish_at',
'displayed_date',
];
/**
* toSitemapTag method
*
* @return Url|string|array
*/
public function toSitemapTag(): Url | string | array
{
$model = $this;
$route = 'fo.news.show';
$params = ['publishedNews' => $model];
return SitemapHelper::generateUrl($route, $params, $model);
}
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'published' => 'bool',
'publish_at' => 'datetime',
'unpublish_at' => 'datetime',
'published' => 'bool',
'publish_at' => 'datetime',
'unpublish_at' => 'datetime',
'displayed_date' => 'datetime',
];
/**
@ -180,50 +171,15 @@ class News extends Model implements Sitemapable
}
/**
* Adjusts model status using current status and publish dates.
* toSitemapTag method
*
* @param self $model
* @return void
* @return Url|string|array
*/
private static function makeStatusSense(self $model): void
public function toSitemapTag(): Url | string | array
{
/** @var \Carbon\Carbon|null */
$publish_at = $model->publish_at;
/** @var \Carbon\Carbon|null */
$unpublish_at = $model->unpublish_at;
$wantedStatus = $model->published;
if ($publish_at or $unpublish_at) {
$model->published = $model->planificated_status;
}
if ($wantedStatus !== $model->published) {
$previousMessage = session()->get('bo.info', '');
session()->flash('bo.info', \trans(
'La publication est modifiée en concordance avec les dates enregistrées.
Pour modifier l\'état de publication, veuillez modifier ou enlever les dates correspondantes.'
) . " \n{$previousMessage}");
}
}
/**
* Getter the current status following planification dates planificated_status.
*
* @return boolean
*/
public function getPlanificatedStatusAttribute(): bool
{
/** @var \Carbon\Carbon|null */
$publish_at = $this->publish_at;
/** @var \Carbon\Carbon|null */
$unpublish_at = $this->unpublish_at;
if ($publish_at and $unpublish_at) {
return $unpublish_at->greaterThanOrEqualTo(now()) and $publish_at->lessThan(now());
}
if ($publish_at) {
return $publish_at->lessThan(now());
}
if ($unpublish_at) {
return $unpublish_at->greaterThanOrEqualTo(now());
}
return false;
$model = $this;
$route = 'fo.news.show';
$params = ['publishedNews' => $model];
return SitemapHelper::generateUrl($route, $params, $model);
}
}

View file

@ -3,7 +3,6 @@
namespace App\Models;
use App\Enums\StaticPageType;
use App\Lib\Helpers\FileStorageHelper;
use App\Lib\Helpers\ToolboxHelper;
use App\Traits\Models\ActivityLog;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -16,9 +15,6 @@ use Illuminate\Support\Str;
* @property string $seo_description Seo description about 160 chars as recommended.
* @property string $title Title for the page.
* @property string $slug Slug of the title for the page.
* @property string $picture Static page banner path.
* @property string $picture_alt Banner alt attribute.
* @property string $picture_title Banner title attribute.
* @property string $content WYSIWYG text content using ckeditor.
* @property-read \Illuminate\Support\Carbon $created_at Created date.
* @property-read \Illuminate\Support\Carbon $updated_at Updated date.
@ -39,9 +35,6 @@ class StaticPage extends Model
'seo_title',
'seo_description',
'title',
'picture',
'picture_alt',
'picture_title',
'content',
];
@ -67,12 +60,6 @@ class StaticPage extends Model
static::updating(function (self $model) {
self::manageFields($model);
});
static::updated(function (self $model) {
FileStorageHelper::removeOldFile($model, 'picture');
});
static::deleted(function (self $model) {
FileStorageHelper::removeFile($model, 'picture');
});
}
// * ROUTABLE
@ -120,7 +107,6 @@ class StaticPage extends Model
// * TRANSFORMATIONS
self::setSlug($model, $model->title);
$model->picture = FileStorageHelper::storeFile($model, $model->picture, true);
}
/**

View file

@ -4,6 +4,7 @@ namespace App\Models;
use App\Enums\Strips\PictoEnum;
use App\Traits\Models\ActivityLog;
use App\Traits\Models\PlanificatedStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@ -16,13 +17,14 @@ use Illuminate\Database\Eloquent\Model;
* @property-read \Carbon\Carbon $created_at Created date.
* @property-read \Carbon\Carbon $updated_at Updated date.
*
* @property-read \App\Models\News $news News Belongs to relationship.
* @property-read \App\Models\News $news News Belongs to relationship.
* @property-read \App\Models\ActivityLog $activityLogs Activities logs One-to-many relationship.
*/
class Strip extends Model
{
use ActivityLog;
use HasFactory;
use PlanificatedStatus;
/**
* The attributes that are mass assignable.
@ -78,54 +80,6 @@ class Strip extends Model
self::makeStatusSense($model);
}
/**
* Adjusts model status using current status and publish dates
*
* @param self $model
* @return void
*/
private static function makeStatusSense(self $model): void
{
/** @var \Carbon\Carbon|null */
$publish_at = $model->publish_at;
/** @var \Carbon\Carbon|null */
$unpublish_at = $model->unpublish_at;
$wantedStatus = $model->published;
if ($publish_at or $unpublish_at) {
$model->published = $model->planificated_status;
}
if ($wantedStatus !== $model->published) {
$previousMessage = session()->get('bo.info', '');
session()->flash('bo.info', \trans(
'La publication est modifiée en concordance avec les dates enregistrées.
Pour modifier l\'état de publication, veuillez modifier ou enlever les dates correspondantes.'
) . " \n{$previousMessage}");
}
}
/**
* Getter the current status following planification dates planificated_status.
*
* @return boolean
*/
public function getPlanificatedStatusAttribute(): bool
{
/** @var \Carbon\Carbon|null */
$publish_at = $this->publish_at;
/** @var \Carbon\Carbon|null */
$unpublish_at = $this->unpublish_at;
if ($publish_at and $unpublish_at) {
return $unpublish_at->greaterThanOrEqualTo(now()) and $publish_at->lessThan(now());
}
if ($publish_at) {
return $publish_at->lessThan(now());
}
if ($unpublish_at) {
return $unpublish_at->greaterThanOrEqualTo(now());
}
return false;
}
// * RELATIONS
/**

View file

@ -0,0 +1,62 @@
<?php
namespace App\Traits\Models;
use Carbon\Carbon;
/**
* If there is a static function in a trait (named boot[TraitName]),
* it will be executed as the boot() function would on an Eloquent model.
* Futhermore, event in the Model observer doesn't erase actual events in the model,
* but adds them.
*/
trait PlanificatedStatus
{
/**
* Adjusts model status using current status and publish dates.
*
* @param self $model
* @return void
*/
private static function makeStatusSense(self $model): void
{
/** @var \Carbon\Carbon|null */
$publish_at = $model->publish_at;
/** @var \Carbon\Carbon|null */
$unpublish_at = $model->unpublish_at;
$wantedStatus = $model->published;
if ($publish_at or $unpublish_at) {
$model->published = $model->planificated_status;
}
if ($wantedStatus !== $model->published) {
$previousMessage = session()->get('bo.info', '');
session()->flash('bo.info', \trans(
'La publication est modifiée en concordance avec les dates enregistrées.
Pour modifier l\'état de publication, veuillez modifier ou enlever les dates correspondantes.'
) . " \n{$previousMessage}");
}
}
/**
* Getter the current status following planification dates planificated_status.
*
* @return boolean
*/
public function getPlanificatedStatusAttribute(): bool
{
/** @var \Carbon\Carbon|null */
$publish_at = $this->publish_at;
/** @var \Carbon\Carbon|null */
$unpublish_at = $this->unpublish_at;
if ($publish_at and $unpublish_at) {
return $unpublish_at->greaterThanOrEqualTo(Carbon::now()) and $publish_at->lessThan(Carbon::now());
}
if ($publish_at) {
return $publish_at->lessThan(Carbon::now());
}
if ($unpublish_at) {
return $unpublish_at->greaterThanOrEqualTo(Carbon::now());
}
return false;
}
}

View file

@ -0,0 +1,39 @@
<?php
use App\Models\News;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::table('news', function (Blueprint $table) {
$table->timestamp('displayed_date')->useCurrent()->after('unpublish_at');
});
News::query()->get()->map(function (News $newsModel) {
if ($newsModel->publish_at) {
$newsModel->displayed_date = $newsModel->publish_at;
}
$newsModel->saveOrFail();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::table('news', function (Blueprint $table) {
$table->dropColumn('displayed_date');
});
}
};

View file

@ -63,19 +63,24 @@ return [
'paginate_list' => 'Nombre d\'éléments par page',
],
'other' => [
'up' => 'Modifier l\'ordre d\'apparition vers le haut',
'down' => 'Modifier l\'ordre d\'apparition vers le bas',
'no_model_found' => 'Aucun :model trouvé|Aucune :model trouvée',
'publish' => 'Publié',
'unpublish' => 'Dépublié',
'search_start' => 'Lancer la recherche',
'search_delete' => 'Supprimer la recherche',
'search_placeholder' => 'Vos mots clés ici...',
'sort_ascending' => 'Trier :name dans l\'ordre croissant',
'sort_descending' => 'Trier :name dans l\'ordre décroissant',
'sort_delete' => 'Supprimer le tri',
'sort_arrow' => 'Ici, les flèches sont utilisées pour trier et non pour filtrer',
'right' => 'Vous n\'avez pas les droits',
'up' => 'Modifier l\'ordre d\'apparition vers le haut',
'down' => 'Modifier l\'ordre d\'apparition vers le bas',
'no_model_found' => 'Aucun :model trouvé|Aucune :model trouvée',
'publish' => 'Publié',
'unpublish' => 'Dépublié',
'search_start' => 'Lancer la recherche',
'search_delete' => 'Supprimer la recherche',
'search_placeholder' => 'Vos mots clés ici...',
'sort_ascending' => 'Trier :name dans l\'ordre croissant',
'sort_descending' => 'Trier :name dans l\'ordre décroissant',
'sort_delete' => 'Supprimer le tri',
'sort_arrow' => 'Ici, les flèches sont utilisées pour trier et non pour filtrer',
'right' => 'Vous n\'avez pas les droits',
'visibility_with_dates' => 'Ce champ n\'aura d\'effet que si aucune date de publication n\'est renseignée.',
'displayed_date' => 'Cette date sera affichée à l\'utilisateur comme étant la date de publication.
Elle servira également à ordonner les :model.',
'publish_at' => ':model sera publié(e) à partir de cette date.',
'unpublish_at' => ':model sera dépublié(e) à partir de cette date.',
],
'meta' => [
'title' => [

View file

@ -247,6 +247,7 @@ return [
'alt_attribute' => 'attribut alt',
'description_2' => 'deuxième niveau de description',
'description' => 'description',
'displayed_date' => 'date affichée',
'director_name' => 'nom du directeur',
'director_speach' => 'mot du directeur',
'employee_function' => 'fonction de l\'employé',

View file

@ -332,51 +332,56 @@
</div>
</fieldset>
</div>
{{-- PUBLISHED --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ __('crud.minicrud.visibility_label') }}</legend>
<div class="row">
<div class="col-12">
@include('bo.partials.inputs.tinyint-boolean', [
'id' => 'newsPublished',
'name' => 'published',
'model' => $newsModel ?? null,
'label' => __('crud.minicrud.visibility_label'),
'helper' => sprintf('%s %s', __('crud.minicrud.visibility_helper'), __('crud.helpers.visibility_with_dates')),
])
</div>
</div>
</fieldset>
</div>
{{-- PUBLICATION --}}
<div class="col-12 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ __('Publication') }}</legend>
<div class="row mb-3">
<div class="col">
<div class="col-12 col-md-6 mb-3">
@include('bo.partials.inputs.tinyint-boolean', [
'id' => 'newsPublished',
'name' => 'published',
'model' => $newsModel ?? null,
'label' => __('crud.minicrud.visibility_label'),
'helper' => sprintf('%s %s', __('crud.minicrud.visibility_helper'), __('crud.other.visibility_with_dates')),
])
</div>
<div class="col-12 col-md-6 mb-3">
<div class="form-group">
@include('bo.partials.inputs.timestamp-datetime', [
'label' => Str::ucfirst(__('validation.custom.publish_at')) . __(' (Europe/Paris)'),
'label' => str(__('validation.custom.displayed_date'))->ucFirst() . __(' (Europe/Paris)'),
'id' => 'newsPublishedAt',
'name' => 'publish_at',
'name' => 'displayed_date',
'model' => $newsModel ?? null,
'helper' => __('validation.custom.publish_at', [
'model' => trans_choice('models.classes.news', 1),
'helper' => __('crud.other.displayed_date', [
'model' => str(trans('models.classes.news'))->plural(),
]),
])
</div>
</div>
<div class="col">
<div class="col-12 col-md-6 mb-3">
<div class="form-group">
@include('bo.partials.inputs.timestamp-datetime', [
'label' => Str::ucfirst(__('validation.custom.unpublish_at')) . __(' (Europe/Paris)'),
'label' => str(__('validation.custom.publish_at'))->ucFirst() . __(' (Europe/Paris)'),
'id' => 'newsPublishedAt',
'name' => 'publish_at',
'model' => $newsModel ?? null,
'helper' => str(__('crud.other.publish_at', [
'model' => trans_choice('models.classes.news', 1),
]))->ucFirst(),
])
</div>
</div>
<div class="col-12 col-md-6">
<div class="form-group">
@include('bo.partials.inputs.timestamp-datetime', [
'label' => str(__('validation.custom.unpublish_at'))->ucFirst() . __(' (Europe/Paris)'),
'id' => 'newsUnPublishedAt',
'name' => 'unpublish_at',
'model' => $newsModel ?? null,
'helper' => __('validation.custom.unpublish_at', [
'helper' => str(__('crud.other.unpublish_at', [
'model' => trans_choice('models.classes.news', 1),
]),
]))->ucFirst(),
])
</div>
</div>

View file

@ -133,7 +133,7 @@
'wysiwyg' => true,
])
</div>
<div class="col-12 d-none d-lg-block mt-1 text-center">
<div class="col-12 mt-1 text-center">
<img class="rounded-3 img-fluid" src="{{ asset($formationSection->data['bloc_image_n1']['value']) }}"
alt="{{ $formationSection->data['bloc_image_n1']['alt'] }}"
alt="{{ $formationSection->data['bloc_image_n1']['title'] }}">

View file

@ -201,7 +201,7 @@
title="{{ $news->thumbnail_title }}" alt="{{ $news->thumbnail_alt }}">
<div class="card-body d-flex flex-column align-items-center">
<p class="card-date small fw-bold text-quad">
{{ str($news->publish_at->locale('fr_FR')->isoFormat('D MMMM Y'))->upper() }}
{{ str($news->displayed_date->locale('fr_FR')->isoFormat('D MMMM Y'))->upper() }}
</p>
<p class="card-title fw-bold text-center">
{{ $news->title }}