From 96f15d32a12e39d68742ba310061396d0c6a9b85 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 6 Feb 2024 14:49:35 +0100 Subject: [PATCH 01/33] WIP --- config/jocko-client.php | 2 + database/factories/MediaFactory.php | 46 +++++++++++ database/factories/ModelFactory.php | 19 ----- .../create_jocko_client_table.php.stub | 19 ----- database/migrations/create_jocko_tables.php | 35 +++++++++ src/Client.php | 20 +++-- src/Console/ConfigureCmsCommand.php | 56 ++++++++++++++ src/Eloquent/JockoModel.php | 34 +++++++++ src/Eloquent/Media.php | 26 +++++++ src/JockoCms/JockoCollectionConfig.php | 46 +++++++++++ src/JockoCms/JockoCollectionFormConfig.php | 8 ++ src/JockoCms/JockoCollectionListConfig.php | 62 +++++++++++++++ src/JockoCms/List/JockoCheckColumn.php | 11 +++ src/JockoCms/List/JockoColumn.php | 76 +++++++++++++++++++ src/JockoCms/List/JockoDateColumn.php | 22 ++++++ src/JockoCms/List/JockoTextColumn.php | 11 +++ src/JockoCms/List/JockoThumbnailColumn.php | 11 +++ src/JockoServiceProvider.php | 5 ++ 18 files changed, 466 insertions(+), 43 deletions(-) create mode 100644 database/factories/MediaFactory.php delete mode 100644 database/factories/ModelFactory.php delete mode 100644 database/migrations/create_jocko_client_table.php.stub create mode 100644 database/migrations/create_jocko_tables.php create mode 100644 src/Console/ConfigureCmsCommand.php create mode 100644 src/Eloquent/JockoModel.php create mode 100644 src/Eloquent/Media.php create mode 100644 src/JockoCms/JockoCollectionConfig.php create mode 100644 src/JockoCms/JockoCollectionFormConfig.php create mode 100644 src/JockoCms/JockoCollectionListConfig.php create mode 100644 src/JockoCms/List/JockoCheckColumn.php create mode 100644 src/JockoCms/List/JockoColumn.php create mode 100644 src/JockoCms/List/JockoDateColumn.php create mode 100644 src/JockoCms/List/JockoTextColumn.php create mode 100644 src/JockoCms/List/JockoThumbnailColumn.php diff --git a/config/jocko-client.php b/config/jocko-client.php index b827bee..449f74b 100644 --- a/config/jocko-client.php +++ b/config/jocko-client.php @@ -4,7 +4,9 @@ return [ 'api_host' => env('JOCKO_API_HOST', 'http://jocko.test/'), 'api_key' => env('JOCKO_API_KEY'), + 'api_version' => env('JOCKO_API_VERSION', 'v3'), 'website_key' => env('JOCKO_WEBSITE_KEY'), 'should_cache' => env('JOCKO_SHOULD_CACHE', false), 'preview' => env('JOCKO_PREVIEW', false), + 'collections' => [], ]; diff --git a/database/factories/MediaFactory.php b/database/factories/MediaFactory.php new file mode 100644 index 0000000..76327dc --- /dev/null +++ b/database/factories/MediaFactory.php @@ -0,0 +1,46 @@ + sprintf('%s/%s/%s.jpg', $this->faker->word, $this->faker->word, $this->faker->word), + 'mime_type' => 'image/jpeg', + 'disk' => 'local', + 'size' => $this->faker->numberBetween(100, 100000), + ]; + } + + public function withFile(string $path, bool $randomFileName = true) + { + return $this->state(function (array $attributes) use ($path, $randomFileName) { + $type = class_basename($attributes['model_type']); + $modelId = $attributes['model_id']; + $filename = basename($path); + if ($randomFileName) { + $filename = sprintf( + '%s-%s.%s', + $this->faker->word, + $this->faker->word, + \Str::afterLast($filename, '.') + ); + } + + Storage::disk('local') + ->put("/data/$type/$modelId/$filename", file_get_contents($path)); + + return [ + 'file_name' => "data/$type/$modelId/$filename", + ]; + }); + } +} diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php deleted file mode 100644 index 635b4ea..0000000 --- a/database/factories/ModelFactory.php +++ /dev/null @@ -1,19 +0,0 @@ -id(); - - // add fields - - $table->timestamps(); - }); - } -}; diff --git a/database/migrations/create_jocko_tables.php b/database/migrations/create_jocko_tables.php new file mode 100644 index 0000000..a5935c8 --- /dev/null +++ b/database/migrations/create_jocko_tables.php @@ -0,0 +1,35 @@ +id(); + + $table->text('title')->nullable(); + $table->unsignedInteger('order')->default(1000); + $table->text('content')->nullable(); + $table->json('custom_properties')->nullable(); + + $table->timestamps(); + }); + + Schema::create('medias', function (Blueprint $table) { + $table->id(); + $table->morphs('model'); + $table->string('model_key')->nullable(); + $table->string('file_name')->nullable(); + $table->string('mime_type')->nullable(); + $table->string('disk')->default('local')->nullable(); + $table->unsignedInteger('size')->nullable(); + $table->text('custom_properties')->nullable(); + $table->unsignedInteger('order')->nullable(); + $table->timestamps(); + }); + } +}; diff --git a/src/Client.php b/src/Client.php index 2cacc0b..bc12a9b 100755 --- a/src/Client.php +++ b/src/Client.php @@ -3,6 +3,7 @@ namespace Code16\JockoClient; use Illuminate\Http\Client\PendingRequest; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Http; class Client @@ -10,6 +11,7 @@ class Client public function __construct( protected string $apiHost, protected ?string $apiKey, + protected string $apiVersion, protected string $websiteKey, protected bool $shouldCache, protected bool $isPreview, @@ -30,6 +32,12 @@ public function getSettings(): array ->json('data'); } + public function updateCollectionSharpConfiguration(string $collectionKey, array $collectionData): void + { + $this->http() + ->post($this->url(sprintf('/collections/%s/configure', $collectionKey)), $collectionData); + } + public function searchUrl(string $collectionKey): string { return $this->url("/collections/$collectionKey/search"); @@ -60,11 +68,13 @@ public function apiKey(): ?string protected function url(string $endpoint = ''): string { - $host = rtrim($this->apiHost, '/'); - $websiteKey = $this->websiteKey; - $endpoint = ltrim($endpoint, '/'); - - return "$host/api/v2/$websiteKey/$endpoint"; + return sprintf( + '%s/api/%s/%s/%s', + rtrim($this->apiHost, '/'), + $this->apiVersion, + $this->websiteKey, + ltrim($endpoint, '/'), + ); } protected function http(): PendingRequest diff --git a/src/Console/ConfigureCmsCommand.php b/src/Console/ConfigureCmsCommand.php new file mode 100644 index 0000000..77ad4fa --- /dev/null +++ b/src/Console/ConfigureCmsCommand.php @@ -0,0 +1,56 @@ +map(function ($collectionClassName, $k) { + $model = new $collectionClassName; + $collection = $collectionClassName::configureJockoCollection(new JockoCollectionConfig()); + $list = $collectionClassName::configureJockoCollectionList(new JockoCollectionListConfig()); + + return [ + 'key' => $model->jockoCollectionKey(), + 'label' => $collection->label(), + 'icon' => $collection->icon(), + 'hasPublicationState' => $collection->hasPublicationState(), + 'order' => $k+1, + 'list' => [ + 'isReorderable' => $list->isReorderable(), + 'isSearchable' => $list->isSearchable(), + 'isPaginated' => $list->isPaginated(), + 'columns' => $list + ->columns() + ->map(fn (JockoColumn $column) => [ + 'type' => $column->type(), + 'key' => $column->key(), + 'label' => $column->label(), + 'size' => $column->size(), + ]) + ] + ]; + }) + ->each(function (array $collection) { + $this->info('Update CMS configuration for [' . $collection['key'] . '].'); + Jocko::updateCollectionSharpConfiguration( + $collection['key'], + $collection + ); + }); + + $this->info('CMS configuration sent to Jocko.'); + } +} diff --git a/src/Eloquent/JockoModel.php b/src/Eloquent/JockoModel.php new file mode 100644 index 0000000..7905762 --- /dev/null +++ b/src/Eloquent/JockoModel.php @@ -0,0 +1,34 @@ +snake() + ->plural(); + } + + public function cover(): MorphOne + { + return $this->morphOne(Media::class, 'model') + ->where('model_key', 'cover'); + } + + abstract public static function configureJockoCollection(JockoCollectionConfig $config): JockoCollectionConfig; + + abstract public static function configureJockoCollectionList(JockoCollectionListConfig $config): JockoCollectionListConfig; + + abstract public static function configureJockoCollectionForm(JockoCollectionFormConfig $config): JockoCollectionFormConfig; +} diff --git a/src/Eloquent/Media.php b/src/Eloquent/Media.php new file mode 100644 index 0000000..b6437b0 --- /dev/null +++ b/src/Eloquent/Media.php @@ -0,0 +1,26 @@ +morphTo('model'); + } +} diff --git a/src/JockoCms/JockoCollectionConfig.php b/src/JockoCms/JockoCollectionConfig.php new file mode 100644 index 0000000..62dee2c --- /dev/null +++ b/src/JockoCms/JockoCollectionConfig.php @@ -0,0 +1,46 @@ +label = $label; + + return $this; + } + + public function setIcon(string $icon): self + { + $this->icon = $icon; + + return $this; + } + + public function setHasPublicationState(bool $hasState = true): self + { + $this->hasPublicationState = $hasState; + + return $this; + } + + public function label(): string + { + return $this->label; + } + + public function icon(): string + { + return $this->icon; + } + + public function hasPublicationState(): bool + { + return $this->hasPublicationState; + } +} diff --git a/src/JockoCms/JockoCollectionFormConfig.php b/src/JockoCms/JockoCollectionFormConfig.php new file mode 100644 index 0000000..74b0bdd --- /dev/null +++ b/src/JockoCms/JockoCollectionFormConfig.php @@ -0,0 +1,8 @@ +isReorderable = $isReorderable; + + return $this; + } + + public function setIsSearchable(bool $isSearchable = true): self + { + $this->isSearchable = $isSearchable; + + return $this; + } + + public function setIsPaginated(bool $isPaginated = true): self + { + $this->isPaginated = $isPaginated; + + return $this; + } + + public function addColumn(JockoColumn $column): self + { + $this->columns[] = $column; + + return $this; + } + + public function isReorderable(): bool + { + return $this->isReorderable; + } + + public function isSearchable(): bool + { + return $this->isSearchable; + } + + public function isPaginated(): bool + { + return $this->isPaginated; + } + + public function columns(): Collection + { + return collect($this->columns); + } +} diff --git a/src/JockoCms/List/JockoCheckColumn.php b/src/JockoCms/List/JockoCheckColumn.php new file mode 100644 index 0000000..ff8d211 --- /dev/null +++ b/src/JockoCms/List/JockoCheckColumn.php @@ -0,0 +1,11 @@ +key; + } + + public function size(): int + { + return $this->size; + } + + abstract public function type(): string; + + public function label(): ?string + { + return $this->label; + } + + public function setLabel(string $label): self + { + $this->label = $label; + + return $this; + } + + public function setDefaultSort(string $direction = 'asc'): self + { + $this->isDefaultSort = true; + $this->sortDirection = $direction; + + return $this; + } + + public function isDefaultSort(): bool + { + return $this->isDefaultSort; + } + + public function getDefaultSortDirection(): string + { + return $this->sortDirection; + } +} diff --git a/src/JockoCms/List/JockoDateColumn.php b/src/JockoCms/List/JockoDateColumn.php new file mode 100644 index 0000000..7980e2e --- /dev/null +++ b/src/JockoCms/List/JockoDateColumn.php @@ -0,0 +1,22 @@ +format = $format; + + return $this; + } + + public function type(): string + { + return 'date'; + } +} diff --git a/src/JockoCms/List/JockoTextColumn.php b/src/JockoCms/List/JockoTextColumn.php new file mode 100644 index 0000000..5ac4718 --- /dev/null +++ b/src/JockoCms/List/JockoTextColumn.php @@ -0,0 +1,11 @@ +name('jocko-client') ->hasRoute('web') + ->hasMigrations(['create_jocko_tables'])->runsMigrations() + ->hasCommands([ + Console\ConfigureCmsCommand::class, + ]) ->hasConfigFile(); } @@ -37,6 +41,7 @@ public function register() return new Client( apiHost: config('jocko-client.api_host'), apiKey: config('jocko-client.api_key'), + apiVersion: config('jocko-client.api_version'), websiteKey: config('jocko-client.website_key'), shouldCache: config('jocko-client.should_cache'), isPreview: config('jocko-client.preview'), From 25ef283ce3d30d799485001849398936778f10b7 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 6 Feb 2024 18:03:26 +0100 Subject: [PATCH 02/33] WIP --- src/Console/ConfigureCmsCommand.php | 16 +++- src/JockoCms/Form/JockoCheckField.php | 18 ++++ src/JockoCms/Form/JockoDateField.php | 20 +++++ src/JockoCms/Form/JockoEditorField.php | 48 ++++++++++ src/JockoCms/Form/JockoField.php | 100 +++++++++++++++++++++ src/JockoCms/Form/JockoFileListField.php | 41 +++++++++ src/JockoCms/Form/JockoImageField.php | 41 +++++++++ src/JockoCms/Form/JockoImageListField.php | 21 +++++ src/JockoCms/Form/JockoSelectField.php | 51 +++++++++++ src/JockoCms/Form/JockoTextField.php | 11 +++ src/JockoCms/JockoCollectionFormConfig.php | 15 ++++ 11 files changed, 380 insertions(+), 2 deletions(-) create mode 100644 src/JockoCms/Form/JockoCheckField.php create mode 100644 src/JockoCms/Form/JockoDateField.php create mode 100644 src/JockoCms/Form/JockoEditorField.php create mode 100644 src/JockoCms/Form/JockoField.php create mode 100644 src/JockoCms/Form/JockoFileListField.php create mode 100644 src/JockoCms/Form/JockoImageField.php create mode 100644 src/JockoCms/Form/JockoImageListField.php create mode 100644 src/JockoCms/Form/JockoSelectField.php create mode 100644 src/JockoCms/Form/JockoTextField.php diff --git a/src/Console/ConfigureCmsCommand.php b/src/Console/ConfigureCmsCommand.php index 77ad4fa..dfc9900 100644 --- a/src/Console/ConfigureCmsCommand.php +++ b/src/Console/ConfigureCmsCommand.php @@ -3,6 +3,8 @@ namespace Code16\JockoClient\Console; use Code16\JockoClient\Facades\Jocko; +use Code16\JockoClient\JockoCms\Form\JockoField; +use Code16\JockoClient\JockoCms\JockoCollectionFormConfig; use Code16\JockoClient\JockoCms\JockoCollectionListConfig; use Code16\JockoClient\JockoCms\JockoCollectionConfig; use Code16\JockoClient\JockoCms\List\JockoColumn; @@ -11,7 +13,6 @@ class ConfigureCmsCommand extends Command { protected $signature = 'jocko:configure-cms'; - protected $description = 'Send CMS configuration to Jocko.'; public function handle(): void @@ -21,6 +22,7 @@ public function handle(): void $model = new $collectionClassName; $collection = $collectionClassName::configureJockoCollection(new JockoCollectionConfig()); $list = $collectionClassName::configureJockoCollectionList(new JockoCollectionListConfig()); + $form = $collectionClassName::configureJockoCollectionForm(new JockoCollectionFormConfig()); return [ 'key' => $model->jockoCollectionKey(), @@ -40,7 +42,17 @@ public function handle(): void 'label' => $column->label(), 'size' => $column->size(), ]) - ] + ], + 'form' => [ + 'fields' => $form + ->customFields() + ->map(fn (JockoField $field) => [ + 'type' => $field->type(), + 'key' => $field->key(), + 'label' => $field->label(), + 'validationRules' => $field->validationRules(), + ]) + ], ]; }) ->each(function (array $collection) { diff --git a/src/JockoCms/Form/JockoCheckField.php b/src/JockoCms/Form/JockoCheckField.php new file mode 100644 index 0000000..8545974 --- /dev/null +++ b/src/JockoCms/Form/JockoCheckField.php @@ -0,0 +1,18 @@ +hasTime = $hasTime; + + return $this; + } + + public function type(): string + { + return 'date'; + } +} diff --git a/src/JockoCms/Form/JockoEditorField.php b/src/JockoCms/Form/JockoEditorField.php new file mode 100644 index 0000000..b823279 --- /dev/null +++ b/src/JockoCms/Form/JockoEditorField.php @@ -0,0 +1,48 @@ +withoutParagraphs = true; + + return $this; + } + + public function setToolbar(array $toolbar): self + { + $this->toolbar = $toolbar; + + return $this; + } + + public function hideToolbar(): self + { + $this->hideToolbar = true; + + return $this; + } + + public function setHeight(int $height, int|null $maxHeight = null): self + { + $this->height = $height; + $this->maxHeight = $maxHeight; + + return $this; + } + + public function type(): string + { + return 'editor'; + } +} diff --git a/src/JockoCms/Form/JockoField.php b/src/JockoCms/Form/JockoField.php new file mode 100644 index 0000000..feba915 --- /dev/null +++ b/src/JockoCms/Form/JockoField.php @@ -0,0 +1,100 @@ +key; + } + + public function label(): string + { + return $this->label; + } + + public function validationRules(): array + { + return $this->validationRules; + } + + public function setLabel(string $label): self + { + $this->label = $label; + + return $this; + } + + public function setHelpMessage(string $helpMessage) + { + $this->helpMessage = $helpMessage; + + return $this; + } + + public function setValidationRules(array $rules): self + { + $this->validationRules = $rules; + + return $this; + } + + public function setIsUpdatable(bool $isUpdatable = true): self + { + $this->isUpdatable = $isUpdatable; + + return $this; + } +} diff --git a/src/JockoCms/Form/JockoFileListField.php b/src/JockoCms/Form/JockoFileListField.php new file mode 100644 index 0000000..150ae48 --- /dev/null +++ b/src/JockoCms/Form/JockoFileListField.php @@ -0,0 +1,41 @@ +maxItems = $maxItems; + + return $this; + } + + public function setHasLegend(bool $hasLegend = true): self + { + $this->hasLegend = $hasLegend; + + return $this; + } + + public function hasLegend(): bool + { + return $this->hasLegend; + } + + private function buildUploadField(): SharpFormUploadField + { + return SharpFormUploadField::make('file') + ->setMaxFileSize(5) + ->setStorageDisk('local') + ->setStorageBasePath(sprintf('data/%s/Posts/{id}', CurrentSharpWebsite::getKey())); + } + + public function type(): string + { + return 'fileList'; + } +} diff --git a/src/JockoCms/Form/JockoImageField.php b/src/JockoCms/Form/JockoImageField.php new file mode 100644 index 0000000..ac7de08 --- /dev/null +++ b/src/JockoCms/Form/JockoImageField.php @@ -0,0 +1,41 @@ +hasLegend = $hasLegend; + + return $this; + } + + public function setFileFiler(string|array|null $fileFilter): self + { + $this->fileFilter = $fileFilter; + + return $this; + } + + public function setMaxFileSize(int $maxFileSize): self + { + $this->maxFileSize = $maxFileSize; + + return $this; + } + + public function hasLegend(): bool + { + return $this->hasLegend; + } + + public function type(): string + { + return 'image'; + } +} diff --git a/src/JockoCms/Form/JockoImageListField.php b/src/JockoCms/Form/JockoImageListField.php new file mode 100644 index 0000000..228d1a4 --- /dev/null +++ b/src/JockoCms/Form/JockoImageListField.php @@ -0,0 +1,21 @@ +setFileFilterImages() + ->setMaxFileSize(5) + ->shouldOptimizeImage() + ->setStorageDisk('local') + ->setStorageBasePath(sprintf('data/%s/Posts/{id}', CurrentSharpWebsite::getKey())); + } + + public function type(): string + { + return 'imageList'; + } +} diff --git a/src/JockoCms/Form/JockoSelectField.php b/src/JockoCms/Form/JockoSelectField.php new file mode 100644 index 0000000..c257e33 --- /dev/null +++ b/src/JockoCms/Form/JockoSelectField.php @@ -0,0 +1,51 @@ +options = $options; + + return $this; + } + + public function setDisplayAsDropdown(): self + { + $this->display = 'dropdown'; + + return $this; + } + + public function setDisplayAsList(): self + { + $this->display = 'list'; + + return $this; + } + + public function setMultiple(bool $multiple = true): self + { + $this->multiple = $multiple; + + return $this; + } + + public function setClearable(bool $clearable = true): self + { + $this->clearable = $clearable; + + return $this; + } + + public function type(): string + { + return 'select'; + } +} diff --git a/src/JockoCms/Form/JockoTextField.php b/src/JockoCms/Form/JockoTextField.php new file mode 100644 index 0000000..cc491b4 --- /dev/null +++ b/src/JockoCms/Form/JockoTextField.php @@ -0,0 +1,11 @@ +fields[] = $field; + + return $this; + } + public function customFields(): Collection + { + return collect($this->fields); + } } From ae6fcbd90bdbcafb30b7acd4f8e9bc8d493b7cfc Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 6 Feb 2024 18:40:43 +0100 Subject: [PATCH 03/33] WIP --- src/Console/ConfigureCmsCommand.php | 7 +---- src/JockoCms/Form/JockoCheckField.php | 7 +++++ src/JockoCms/Form/JockoDateField.php | 7 +++++ src/JockoCms/Form/JockoEditorField.php | 11 ++++++++ src/JockoCms/Form/JockoField.php | 31 ++++++++++------------- src/JockoCms/Form/JockoFileListField.php | 22 +++++++++------- src/JockoCms/Form/JockoImageField.php | 18 ++++++++----- src/JockoCms/Form/JockoImageListField.php | 10 -------- src/JockoCms/Form/JockoSelectField.php | 10 ++++++++ 9 files changed, 74 insertions(+), 49 deletions(-) diff --git a/src/Console/ConfigureCmsCommand.php b/src/Console/ConfigureCmsCommand.php index dfc9900..4b37b03 100644 --- a/src/Console/ConfigureCmsCommand.php +++ b/src/Console/ConfigureCmsCommand.php @@ -46,12 +46,7 @@ public function handle(): void 'form' => [ 'fields' => $form ->customFields() - ->map(fn (JockoField $field) => [ - 'type' => $field->type(), - 'key' => $field->key(), - 'label' => $field->label(), - 'validationRules' => $field->validationRules(), - ]) + ->map(fn (JockoField $field) => $field->toArray()) ], ]; }) diff --git a/src/JockoCms/Form/JockoCheckField.php b/src/JockoCms/Form/JockoCheckField.php index 8545974..4c68bed 100644 --- a/src/JockoCms/Form/JockoCheckField.php +++ b/src/JockoCms/Form/JockoCheckField.php @@ -15,4 +15,11 @@ public function type(): string { return 'check'; } + + public function toArray(): array + { + return array_merge(parent::toArray(), [ + 'text' => $this->text, + ]); + } } diff --git a/src/JockoCms/Form/JockoDateField.php b/src/JockoCms/Form/JockoDateField.php index 2b90549..0b4c548 100644 --- a/src/JockoCms/Form/JockoDateField.php +++ b/src/JockoCms/Form/JockoDateField.php @@ -17,4 +17,11 @@ public function type(): string { return 'date'; } + + public function toArray(): array + { + return array_merge(parent::toArray(), [ + 'hasTime' => $this->hasTime, + ]); + } } diff --git a/src/JockoCms/Form/JockoEditorField.php b/src/JockoCms/Form/JockoEditorField.php index b823279..6b304c3 100644 --- a/src/JockoCms/Form/JockoEditorField.php +++ b/src/JockoCms/Form/JockoEditorField.php @@ -45,4 +45,15 @@ public function type(): string { return 'editor'; } + + public function toArray(): array + { + return array_merge(parent::toArray(), [ + 'withoutParagraphs' => $this->withoutParagraphs, + 'hideToolbar' => $this->hideToolbar, + 'toolbar' => $this->toolbar, + 'height' => $this->height, + 'maxHeight' => $this->maxHeight, + ]); + } } diff --git a/src/JockoCms/Form/JockoField.php b/src/JockoCms/Form/JockoField.php index feba915..0442195 100644 --- a/src/JockoCms/Form/JockoField.php +++ b/src/JockoCms/Form/JockoField.php @@ -53,23 +53,6 @@ public static function makeEditor(string $key): JockoEditorField return new JockoEditorField($key); } - abstract public function type(): string; - - public function key(): string - { - return $this->key; - } - - public function label(): string - { - return $this->label; - } - - public function validationRules(): array - { - return $this->validationRules; - } - public function setLabel(string $label): self { $this->label = $label; @@ -97,4 +80,18 @@ public function setIsUpdatable(bool $isUpdatable = true): self return $this; } + + abstract public function type(): string; + + public function toArray(): array + { + return [ + 'type' => $this->type(), + 'key' => $this->key, + 'label' => $this->label, + 'validationRules' => $this->validationRules, + 'helpMessage' => $this->helpMessage, + 'isUpdatable' => $this->isUpdatable, + ]; + } } diff --git a/src/JockoCms/Form/JockoFileListField.php b/src/JockoCms/Form/JockoFileListField.php index 150ae48..e52cdfd 100644 --- a/src/JockoCms/Form/JockoFileListField.php +++ b/src/JockoCms/Form/JockoFileListField.php @@ -6,6 +6,7 @@ class JockoFileListField extends JockoField { protected int $maxItems = 10; protected bool $hasLegend = false; + private int $maxFileSizeInMB = 5; public function setMaxItems(int $maxItems): self { @@ -21,21 +22,24 @@ public function setHasLegend(bool $hasLegend = true): self return $this; } - public function hasLegend(): bool + public function setMaxFileSize(int $maxFileSizeInMB): self { - return $this->hasLegend; - } + $this->maxFileSizeInMB = $maxFileSizeInMB; - private function buildUploadField(): SharpFormUploadField - { - return SharpFormUploadField::make('file') - ->setMaxFileSize(5) - ->setStorageDisk('local') - ->setStorageBasePath(sprintf('data/%s/Posts/{id}', CurrentSharpWebsite::getKey())); + return $this; } public function type(): string { return 'fileList'; } + + public function toArray(): array + { + return array_merge(parent::toArray(), [ + 'maxItems' => $this->maxItems, + 'hasLegend' => $this->hasLegend, + 'maxFileSize' => $this->maxFileSizeInMB, + ]); + } } diff --git a/src/JockoCms/Form/JockoImageField.php b/src/JockoCms/Form/JockoImageField.php index ac7de08..8a7925c 100644 --- a/src/JockoCms/Form/JockoImageField.php +++ b/src/JockoCms/Form/JockoImageField.php @@ -5,7 +5,7 @@ class JockoImageField extends JockoField { private string|array|null $fileFilter = null; - private int $maxFileSize = 5; + private int $maxFileSizeInMB = 5; private bool $hasLegend = false; public function setHasLegend(bool $hasLegend = true): self @@ -22,20 +22,24 @@ public function setFileFiler(string|array|null $fileFilter): self return $this; } - public function setMaxFileSize(int $maxFileSize): self + public function setMaxFileSizeInMB(int $maxFileSizeInMB): self { - $this->maxFileSize = $maxFileSize; + $this->maxFileSizeInMB = $maxFileSizeInMB; return $this; } - public function hasLegend(): bool + public function type(): string { - return $this->hasLegend; + return 'image'; } - public function type(): string + public function toArray(): array { - return 'image'; + return array_merge(parent::toArray(), [ + 'hasLegend' => $this->hasLegend, + 'fileFilter' => $this->fileFilter, + 'maxFileSize' => $this->maxFileSizeInMB, + ]); } } diff --git a/src/JockoCms/Form/JockoImageListField.php b/src/JockoCms/Form/JockoImageListField.php index 228d1a4..c98de5a 100644 --- a/src/JockoCms/Form/JockoImageListField.php +++ b/src/JockoCms/Form/JockoImageListField.php @@ -4,16 +4,6 @@ class JockoImageListField extends JockoFileListField { - private function buildUploadField(): SharpFormUploadField - { - return SharpFormUploadField::make('file') - ->setFileFilterImages() - ->setMaxFileSize(5) - ->shouldOptimizeImage() - ->setStorageDisk('local') - ->setStorageBasePath(sprintf('data/%s/Posts/{id}', CurrentSharpWebsite::getKey())); - } - public function type(): string { return 'imageList'; diff --git a/src/JockoCms/Form/JockoSelectField.php b/src/JockoCms/Form/JockoSelectField.php index c257e33..5d146ac 100644 --- a/src/JockoCms/Form/JockoSelectField.php +++ b/src/JockoCms/Form/JockoSelectField.php @@ -48,4 +48,14 @@ public function type(): string { return 'select'; } + + public function toArray(): array + { + return array_merge(parent::toArray(), [ + 'multiple' => $this->multiple, + 'display' => $this->display, + 'clearable' => $this->clearable, + 'options' => $this->options, + ]); + } } From d041b04418b1aa5b133cb0843cd3fd7322089e21 Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 7 Feb 2024 14:44:44 +0100 Subject: [PATCH 04/33] Work on Media and Seeder --- composer.json | 1 + database/factories/MediaFactory.php | 39 ++++---- database/fixtures/images/1.jpg | Bin 0 -> 244798 bytes src/Eloquent/Casts/JockoCustomAttribute.php | 25 +++++ src/Eloquent/Concerns/CastsCollection.php | 23 ----- src/Eloquent/JockoModel.php | 23 +++++ src/Eloquent/Media.php | 8 ++ src/Support/Database/JockoSeeder.php | 19 ++++ src/Support/Thumbnail.php | 103 ++++++++++++++++++++ 9 files changed, 199 insertions(+), 42 deletions(-) create mode 100644 database/fixtures/images/1.jpg create mode 100644 src/Eloquent/Casts/JockoCustomAttribute.php delete mode 100644 src/Eloquent/Concerns/CastsCollection.php create mode 100644 src/Support/Database/JockoSeeder.php create mode 100644 src/Support/Thumbnail.php diff --git a/composer.json b/composer.json index e873233..e1acc1f 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "php": "^8.1", "code16/laravel-content-renderer": "^1.1.0", "illuminate/contracts": "^10.0", + "intervention/image-laravel": "^1.0", "spatie/laravel-package-tools": "^1.14.0" }, "require-dev": { diff --git a/database/factories/MediaFactory.php b/database/factories/MediaFactory.php index 76327dc..0f92adb 100644 --- a/database/factories/MediaFactory.php +++ b/database/factories/MediaFactory.php @@ -13,33 +13,34 @@ class MediaFactory extends Factory public function definition() { return [ - 'file_name' => sprintf('%s/%s/%s.jpg', $this->faker->word, $this->faker->word, $this->faker->word), - 'mime_type' => 'image/jpeg', - 'disk' => 'local', - 'size' => $this->faker->numberBetween(100, 100000), ]; } - public function withFile(string $path, bool $randomFileName = true) + public function image(string $key): Factory { - return $this->state(function (array $attributes) use ($path, $randomFileName) { - $type = class_basename($attributes['model_type']); - $modelId = $attributes['model_id']; - $filename = basename($path); - if ($randomFileName) { - $filename = sprintf( - '%s-%s.%s', - $this->faker->word, - $this->faker->word, - \Str::afterLast($filename, '.') - ); - } + return $this + ->state(function (array $attributes) use ($key) { + return [ + 'model_key' => $key, + 'file_name' => sprintf('data/medias/%s.jpg', $this->faker->unique()->slug()), + 'mime_type' => 'image/jpeg', + 'disk' => 'local', + 'size' => $this->faker->numberBetween(100, 100000), + ]; + }); + } + + public function withFile(?string $fileName = null) + { + return $this->state(function (array $attributes) use ($fileName) { + $fileName = $fileName ?: fake()->slug() . '.jpg'; + $path = base_path('vendor/code16/jocko-client/database/fixtures/images/1.jpg'); Storage::disk('local') - ->put("/data/$type/$modelId/$filename", file_get_contents($path)); + ->put("/data/medias/$fileName", file_get_contents($path)); return [ - 'file_name' => "data/$type/$modelId/$filename", + 'file_name' => "data/medias/$fileName", ]; }); } diff --git a/database/fixtures/images/1.jpg b/database/fixtures/images/1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a6f46bd6f3a274208dd6cc51d776ddfc55e3eb31 GIT binary patch literal 244798 zcmce-2V4|O)9^dXE-X1nmIaoeL}3>YmYkzV1`#DLS@M!|5HOHIMT7+;34(x<6cJFO zN{}ofAZbBFa!z;g=sBJze9!aVd%t)50W;HG-BmMP{qL%2k4KIdMZRe19RhzGBE!x; zt~U5{HvC^(w1|f_Km$;R13(FYdf*BEaRUHEDu32e|D%@c-)s4Jxk>r>IN3^x+1rX+ zBSq2YL?mqN&WW8ziJh|%7qvYnA!?68N}@%PHnvE7Ul69B&GG%woHgd3Iu)|UVC=lS zo!s3pcmXi{r-0%Q0ULL(-vww-v=nl*#yASuxcl3k2mnlI5safkpSHLDwGifbYx@ie zDd7bGAQ(pDg&(4=T^#H&j@BaQ#qEBG#OK+$S-aX{BG87qnzkoRnZi9yq(%U1h0yU# zcch~@?}dI(QUh|c00E0pSS{Fzm>%t6ZL9E0&Q@7OPw|JKBJlSDMLj*l&lU^nb5f4K zGS^jH+H>r@SW2=2yQUg4H|J0=e&*}*D^;ieNf7x{Fpsjs^2XGWePPTanX1+1w&4{~ zcGZc(^TARg!Fz4pxl)-}k&mF8)o<)=*dNmxaprIo5C0 zyg@3pXPhq*$gHK+W@mLjdmGHKu4)n5iZlM3AX-J^m!h{rCbeverN|-9EtOOa8mtz3 zBlLZ+vnFTc-sKS0OnCe|Iew>Fbhi{_Rrmb>Z?e;woeYo1hoOzmIt_#DX!Y2o>3=7; ze#w`LZy??^1qkpE>a4m!(Jn@Nc2%fob8*XO`9tove^*%1{1~!w>iNpZ+2D z@BPCX{mYyFopS&YC(e=U@Uvz_{xw@W4BE@d<3!L)Q4_9i=Vs?+?QLfZ*L1aZu(SW8 zqRS8WLwnhsl(=Ht?Y;f1z3dbn?A*M)l$_ig{-_LBKB?gDW$WjNFAzb9h@C?q&xwc| zA;qMSC@Cb;4DX#@Kh5C&VTK6W<##v#F&PP=U96p4@fH)PohV!MnQu$MhiS@?f6Z3+Ld^3T?so!l_q)^2D! zO%*g!2;u%iEla%3ClAQo%O7^p3GL;6@-Qq7>@XhqY0SyS#m-VuQy5>{`yXmsI=R`q zM?ADWnS%WJ{*;9OCJDzs{vQt=uIJ_Mi=V9UT|4;Nd13Ic=0pMKzvj!xyx$%Q+Q-Yq z-~9gv%l~Tsk0Pk-V5kUJRz$EdQizBmPzX^Z3VB{c z%nW}ih7c7+{=7tZ|Gi&Y1kFji#I&TB@Yuuqt2e$gY6RuUO(-qY0PoZ8ZnpnXy`SoX z|Bd>HNYKOI9{`9D07rrn0sQ@21d*T!pb!-Qc`DRWd|tKr^5-#E7?m072-dR+- zUwM$kQOskyszx$1;obUGV!`r$cbc5C#2CZ%U^!Zb(PHNw+O48@O_y2c{4{{AvLLtO z-OJ?jWE8hd!Us*+ZMcQ^UxtJ_&_NFFijp_%JJp`ri)SYo!r!cEqmk64O@f@Do z9=rFHWTOQZC;7=rp_pXs*^_=dhtr?qmn{fESKi;m$!g2a!eKG&51?0HE2`Hw8oj%y z{^|Pjq-S=f*#VcQuvou~r4tW!yRKD!ysG5C%ZZK}F|U34V%*I8sR)E`I{sPNTBna2 z)r!zVmCw)mnn61hK6;&ZgcVrAY1|ylCO`y&fFdQdr9mV=1_mqDNf88005l<3PpVQv z7$9^^pOv%a?g%s5GDjeINj~CjDtj4#)nLh`3mx(zsgs~03TwLH zcwxrAxB?g8C4*|WPTL)tnQ{^EW2acbEY%pApz-`0%WaxQ_X;oM3m~!2t|d|@VF~WX zTlw4@D0xcuRYg|O{5ai`V)EsvZIJ+3MT*9@)AN^$Mh|28LdITpa6NfAKu;Bt*7@p* z3HCZVz;ompF2juf*;W$G?Xt^RH=llFR6Dgh6yK*x_cSat@WT#8HpeXeX!?87YR$*b zLe7^*ClM>|S6>gR+Td5CFF8nlChZbAS!lPQ@~Z#Ts|}aON3&TQ;r`Fu@m{gE81zwB zJmXSp=r!~``tmOnRUOKLjiADt7tnT042p|#Bd%k8RGdb0J@47SVB{22ub19zbmFwI ze~~+LCTZBhe?qhR^L+3^6*%S7d>rNy;wu|{3ufbqdPUGt=mP(Dqc5coHDhy+ogF7p z-j9i#Gj*(R82jKa{oDl&97Og4=Z0?zp_am41xw_tB)g6I?K7ViE!d^4NOtFtOb z+(ov_lYuv(3#lMwV`U9wG`ZCcFo)vv~R=oI)6_5`@Ujj)vZK!^*0`d<72Vwd5 zIr~4c0)Y_14?{dFc>eSgxS@}Shf4rl`2<)$P+{Qi?v3XK9KRM5`2h|52~q-q5IZj- zj)#WW&&%^Cm;cvb6-mJPyRAR4KtP4Z0s$$(iMI@&nfkPzAmK6!X)P?<@wZDNW6+YbDni`oepT3hH ze66G=F7gWUvNu)T;=^HfXnGqKQ4yv+7i-+jDN=pmmKJ0i%Ea72zvc8gpT>+zrQs-Z z{_FdYw)=ijhSkEVX?;q_LqYS*7TLf|HOjuLiPSl^p6_vatalyGNS~b<5u^O_CSlFY zn^HbHxD4x1VZJx<@#;B(K_+6RhqcMh=Pn6sWL;2}#XRXdp!J*>OP(%Mn?#3K4$FyX zAHV3oY91j`iECQag}ZrSxy@SF(_YovOYZ1Mmt8eGDDZgQ<#e;X2lCCWZ>!>}hNL-M z)A}ZX$y}qgo)+OfflCYzncpe#ox5Qdao%{-=@*s~P8JG(XAdX_K}|&R6HW9WJdHrW zZ#4RsusK=ku)-mv2x0^g5d?n<;8FLh4LSk{IU6Zp2>1XtfHI(np!#zzCq#hpQ~KXv zM)u56x_q?!yGMcVM~?0HDh$xx9XxA(z|0PTMp)-q=9tG^`Hz74N0|L^(-V4~IO?A) zJ8{+@NW=d+ao8VZ3;0DcJlR|k*APy>uJL3u|BoI2Gui&@uDyTv*JhYW-N%m4B^B#Mr(5)bpkHp5fUB65AO*hSj=FmCkw~M=7#{z1fHjKt1Bp+X9Nl zq#UcEIH;s@pBB3mv3I5>Sf{twg#-r3q_5<&^xq_JqV*hX4@$c>PJWl(3T(FxqTW_% zol7buxzza{e22v}KT)biiodJz5cL=pV@ViX&Yj@iU_Kk^D|jY-Mf+ZE7W>{gS!QSt zi%H#;E!*wEinwoJ9eHJ9djoqJsR}G_%bTehl;hXANUpF|27vI`xpc1>zWaXXf6}^HKv`$+MdH?dgKzkPP_Blp^ z@(?s-j;{U1W2>8yewQ3kbGD?C_hz_0AyD80aP+!yPO?dJQOE^d39`}squg?7+!$!&j9 zAAp?J)nZnP%%+4nsixLa`qgzPG2c%cPo<0n6dFSXuVAM3=v6Kj)(-` z`vYv$j=Y1~p;t#6lqH{#H=FUF|A$fYze+Wvi0BWhN&Eph5hOy=?Ek(3{|Rr{Ga!3r zc3ZLLEJ-dKR?pieU(fVCU|;3|MK=14q=`l`X*|%w2HmxN-TTH;^;jfxo{_P^f5|7^ zQVQ?tFo_3hT1li8Tn|Hb*^@|KDzz0rr(cBK} zevx3QP3NGLx%$+3C;FR=7xNsyy0GzOkgRtHaTYpbfd| z+%OEPr)#!@sJik+j?Iql#(#W6Wo>rzz0}O(8FFp{Cr?l!((@_r@J_6V+E`#qI=- z8Y4IE^r0*TUnCa9>m!|S8auiODA_tkbLZ%T+dqHsGF@_EiyKL9#c9%HF$YSRviYg( zza~ysYPmT5;1*?%f~qb{xX0zV)QEYi!H%Sa>;`0- z4um#(nH#DJxaHlq8j6my_haxapK4{gGG$IIK4dS!XtyHNlRNuzE}p`!%EZt-yQ5sk zO~2>HtCX*ZeLu#iO5C!TDx3kJdcQEL?V8y zsv;SA|IA%M334JCnek=x1RyZ-Bt1Z46{7*N149V#t9OJy5!ufq7YQ{8lH&xy4{ibw z=#NSkKT^3?e=HQy~Z8?;luY36y)MyY#?X?lq; z_}BIfY2mx}(@r?2qbqNQ1mLjddude#dJ$_q@1TVU$G1^RqX%Im+-dodr$?P%j7YyV zA7^HY-dFufVeOK2s@#8lv$uUqdzqN=YozVPW3K79G2g55Icz-^2WN9e(a(ey z2)~zXac{D|d;oh|YdNv<-8Tez-%i~)ON(2$RN&sL?3u$LjGV}K1F`*q`o)19L9qwZ z<8HRhPzg{n6|7pnt3=yA>V~6D{Yym_e`eR2k5rSUiu)s#f?xE@tIFt!=)T$z)Al>D z1>0WCqMGRbU=dgHSdYH*+1r=HEz*uEc?Hr+hnSG>pGl7c#Hd3HuacT-19H($KJ8Jq zLq1)Jr+iHcxc7!zu5rAWXBSHsmil@PRI$C!AM-8909Bk$lI!`@#j{+l&i(G-7i!ug zYjx7Nqm`QW!S-<%?v+NR$hSG3vY9EVkSZq+9tz#sYVi-9mc;oUdEZD^X@qPi-*F1O zUFD_a(9{nlvhSCEwYQjz8qtToO{<;K%Uzf1zZLaPA?Q~ubsHZ`#r_>Q(C~jqTrr#k zGCvZ4qVb;jBOmz75q>(Oy4X)w{KFG}cf_Bmz=)(@rr;x{h$uuvID-4v9Rw(b>1R*w zXpG0-dP11~S&JY>`AK=S#1-6p;AotcgIydQpP} z`AE92uE}$Q*ELzF;s~6p;?+2(56=Z!+JbsA_xz_)Py0S}^6A%`Q91PrPJt~}>D`&t z^dmOoXszl=zjNB?#q*cVPRuWQ7}laB#M~+r&wd^^yQeD}KM?O1;-i{dV0zXWtUCTt zuUapz+@wLf^^007Z+{t18Sof?pN?g|3H~~eEU|piV7=3dqs2shX_V$doXDFSbEZ=@ zpX7EeE0l&3rx}w!lr$Mn$26Am2rBkwh-y6&A9(I{H}qZrO2hK)>oHl837;Wx2eX?y zTSe`&y60tphF6*VE&-siQI$EVag;RFLeg|27=tv<38#5SM;`j*5a(Oz z#f-@Fmv|Z8IjxCgJnb8is;g9sx%Ptbu*J`X-sQ>Ta*WixrdsV_*!vn()5tg2W=(FL zvfzY{b_koJN0T4v0Kw!{t9ssJ)U1KoP$?rPm(Iqwe#U*hYsXMOcF%h%ik)?8{VpQG zros;DcC##)n)|!g1c}s^71>r+EGIvGEK3SkDG#SsyoIOO6FkN4{-hXGT%wlSD0oD@ z^pMw{-{Q(YQq1P>0HH_Fo^XPcmi*UPJb@Sz@3AMh$!Muh^6J045t8FaUIULeravWq z;q6C`P+jb2dJxa7zaS=o5ET*m1+o7XlKv;YwZ)t#(tOq|>!qo0+ayqU?IKWM?sVU3V@$A_zHD zuDU)-vx=#F_FW+S`YleTiN#Ub7RIp0%mQ*sV-5*l^<`IvOa^eg=N9`bt9@|nM0W9M zeElE4IZb>_id6%GKB*|)91C{76u2T0kZoT2ZvMJ$mBE^?`nGix^JmK{BQg$0Hwlh< zg;L9><60rzo06v-mMII+E;EI#tX<+1R|n@<9yQx~D-<<(wZ70{udFt`7;rS4h)FFK zl$_^jVljj-YShTX@t><_4H{!UVb2ZL|JFczvsitt+=GrNSJA_X`r{E zzb>wM@ix3q@Y4AJfk*4ARxi2YO}EFa$5W3an|7Zf3bcn8(zQ(G`oTAjo+b8MT<|@m zpV}EmE`8+|=cDGc#JhCVr@$eWT=llo=*hw_a$B#xpUSh5_KHoN7?RR`>fp@1wkSpS(}Bs;lxB-^}rRyNoz>!jT^zV(_^>eDdz^*};EF zrr*)^ZFoAe&(qvidqT)T9`_>s35-hj@65#KA5T1u^$g_ii1ho#3Or-*Il2FmFp9sV z@VUG5{{zCDc&NGAKe+wdi5i8_! zLQ`sudwQ`7Cn(f@XoN%QO9hh<^+nnqgkg6h(|)_2`P2ozeUDodC45c?PR%{6&;>cG zjuEoit5=3yWB0DZ&xj!%x@u0{b#KwrwqYeFj(o(>P;AmekBJ9yp6R~hB(Ltb@8wN5 zeEeAVmSEuH=q_c;C99ee-DG>LAa?Rr>FiZ*F$>{MbetPSR47-4z?lU9X#``P|PwdDSa(;R1m{kwj>!VW~7L zu_jeomt^4Q<1^^{Jc6?F*NRr{-zawJ)+jvS;85tTrRDA#WA&Gs!IsU6d8Q1Xy*7iw zJT3(3Hpol+muZ*nx7-O@BDEBij8*gxL@i`~`OJ05%G=+*JHMK4xi2shx~7)pBgn=h zc=nS!eGnV-ZSlI)fw?ut`%M}KY&G-_W{kUT1t5nDr zOQJ3?7j`;7n(83^?v}BU0DJge(;B6~?9hI(unN5vj+T=3lLYE?uE5sPQ1Agh?-!3J zmd{UOQ9fWw*zG-sTmGasMXbPN_B*joc0Snsev`9V?brV-iDkc6FMRGhV?@$EP^(Z2O+>Ckug`f)yLwKYBWL*zAMAv)qWR8s zf3wi(NZsMEejv6;*kXiajuLQ=pR|Mr^7mTG4v6qq`GWI!Tch%n^FW~3Z- zvd3fNNJWw1PY?{hTQVq^Lwn%m6Be+Kxxf8KDWpn$&0_htK&uFLQMz zt7%Lbx4EyXCE9K`JKgY2_m#bDuH+kg^PW?n#>(WwQ!Mrw-fzb*u;D1tPv!=eQ&Oa` zvSYxE>Y1B087j;Ip-pL3eb%mvq90o)E?KBcLx4_h0I^AJ?DDyc*> zsoXj3YZJN^Ua>!KTPXVBCiF`~?VNHxQ(d7S=nGXEIS1=gBfc{?U1li_^wqszoh_|V z^cfEqMT<>Q#!y03>+EwkVU zxooXXi4QA7NzdNjoN*FPXFa$1vG^(LEXyxuN#U6#{?8wr{!04&OCJ3WEY-z_wcE&W?M{c|%eKQ=4A6bJ-(Vt zonHR&y>li#yBpi3h`@~7Pb~&|J}9d^Q07>?>y@ifb^l$KZV?ff){jSJ02elY`O!+F zFj?WknwZSDZE$<+qdsi~=JRUKPDT}Vxh4tU@&{$-E5{CDBO88ZSB>J_mvi-G4}3`+ zuV=fz{REFcZv`U&zk^qln&wa{pj*>9o1b3ZG``lu^Vr3=+nwv&MyRJV*EP=LQ*}n? zctS^{KCaZn-aWkX;c6+&IU&(V3Q2(oJ z+i#h{oQMyI(|=|WNx(?_zM|hUh4|fXzjnuw5F)95Y+Cy(xrY$=tug^Q5{5X9z;7vn z(AlpDh`fH?Nfl|{U2?0lO_w>dM*$)HS5puqCj|cGlAIJ{!y3o!@4L5qQKv;C)*&u1nsW?>`JETyj(tcaOX^YMk*zl%f3M z>5Kt#f8O80hEIM%A_)Ml{PMhjFO+opi*=kjrL+TPU73eQPGLsjHqvvc6_|6zq4La4 zTd#QcScD#4@K9U1DV_9DgzaH#oG$N3McEc(+nOz*2k;o%PFRfYU6haXDaFCS7OiLJjOXwJKx3+kuN=cBJ!B?k)`=h zPm_&XE>XCL2{XGCz^|1E$mY68wz74b8S+}+g^H!Rv(0+@G`F*K>s-RF3~&@T=N^O-@?*c>7fvpo z=N`QIx*-CIoqb}frmSKHQ04mdoxX7IWj&{u-GC6)I<@;{Da)OqH;?RHdb@XGF5Q>d z=%DVr3BJXu!vtPX&^03CM54RQBTGY}$|2Z1>t#^b_=T*^Dw|D&^MRf$iZqe+QXGnR z$>qk&85^Y7H_g-IPgy+_v-@&_W~V-oTFcbWOY-;ZvC8j-rd9T=ywh1Td+t5i7W~cq zuvDtg2bUohG9EM>m~>m?;G02dO$miz63p0oyygYXt28AOyZ#`Kkfk9_WAAk&U(Lsu zEwz@Ym-X!74pU>Nw=RRi8L_yLw>pF~sOivdkkl z+w(zujEwI#mcDTAXipehZ;>O)Fl8Yd4&Egj+a(Tl&SSBWvseee`&l|_dciltE28)p z6H@Xz-|H2@7+S9!(a?~ey6(=@U@l(xX>93Pqvv^f0y5Cy1>+4Vk(H8X}j zcf?M^K&($Ev1EBJO+{wVJ%x4V;utx(BA3!`>IM%zjQz2!T;KdWpWy~Iotdbd;F;~h zjtk?_>@0 z9wcm-H7s5|oo!xY|LTkPqL<-e^M4~vF4-f$uLP!K9CLtxmSE!~2@SkeH1Q0MGAp~zGC)Kxhb0aUd0mX!Wh3 zoSt+d5y`p4TuPPA^adYRxJ7NeB1uRY7@3$)pE=9J%f~NvUR(ktDW$BUs-~`?sby$n zY;wu;vKiXe&fdY%$rlLJlO+DMfv#wI?m7NCYvRQgUu(GYOZd!3w>N z*9TGtZm}a(VcCCYSmggJ%lD*0r<{>+^2Hoxgj!n+I)eE`fE!SS4_dGG_!=}AF z93HiC3~U-de=8|`9|Yq?XXPy#AH}wPol}M3Bp0&_;8zncJI3pd zm!e`OVH^ub0mUXG#P3~h*Gu30v>xZcwa~4xMdwwy&Ux!fy-N3UtDI8`lbEq%0DF<^ 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