diff --git a/.gitignore b/.gitignore index c88b6cd6..7f0d5283 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ docker-compose.override.yml phpunit.xml _ide_helper_models.php _ide_helper.php +/.scannerwork/ diff --git a/app/Concerns/Publishable.php b/app/Concerns/Publishable.php new file mode 100644 index 00000000..da026451 --- /dev/null +++ b/app/Concerns/Publishable.php @@ -0,0 +1,87 @@ +fillable[] = 'published_at'; + + $this->casts['published_at'] = 'datetime'; + } + + public static function bootPublishable(): void + { + static::addGlobalScope('published', function (Builder $query) { + $query->onlyPublished(); + }); + } + + public function scopeWithDrafted(Builder $query): Builder + { + return $query->withoutGlobalScope('published'); + } + + public function scopeOnlyDrafted(Builder $query): Builder + { + return $query + ->withDrafted() + ->whereNull('published_at'); + } + + public function scopeOnlyScheduled(Builder $query): Builder + { + return $query + ->withDrafted() + ->whereNotNull('published_at') + ->where('published_at', '>', Carbon::now()); + } + + public function scopeOnlyPublished(Builder $query): Builder + { + return $query + ->whereNotNull('published_at') + ->where('published_at', '<=', Carbon::now()); + } + + public function isDraft(): bool + { + return \is_null($this->published_at); + } + + public function isPublished(): bool + { + return ! $this->isDraft() && $this->published_at->isPast(); + } + + public function isScheduled(): bool + { + return ! $this->isDraft() && $this->published_at->isFuture(); + } + + /** + * Determine the publish status of the model instance. + * + * @return string + */ + public function status(): string + { + if ($this->isDraft()) { + return 'draft'; + } + + if ($this->isPublished()) { + return 'published'; + } + + if ($this->isScheduled()) { + return 'scheduled'; + } + } +} diff --git a/app/Console/Commands/Import/Command.php b/app/Console/Commands/Import/Command.php new file mode 100644 index 00000000..fad63f9a --- /dev/null +++ b/app/Console/Commands/Import/Command.php @@ -0,0 +1,138 @@ +db = DB::connection('import'); + } + + public function createProgressBar(string $message, int $max): void + { + $this->progressBar = $this->output->createProgressBar($max); + $this->progressBar->setFormat("\n%message%\n[%bar%] %current%/%max%\n"); + $this->progressBar->setMessage('⏳ ' . $message); + $this->progressBar->setMessage('', 'status'); + $this->progressBar->setBarWidth(48); + $this->progressBar->setBarCharacter('='); + $this->progressBar->setEmptyBarCharacter('-'); + $this->progressBar->setProgressCharacter('>'); + $this->progressBar->start(); + } + + public function finishProgressBar(string $message): void + { + if ($this->hasErrors()) { + $this->progressBar->setMessage('🚨 ' . $message . ' with ' . $this->errorsCount . ' errors'); + } else { + $this->progressBar->setMessage('✅ ' . $message . ''); + } + + $this->progressBar->finish(); + $this->resetErrors(); + } + + public function logError(string $message, array $context = []): void + { + logger()->error($message, $context); + + $this->errorsCount++; + } + + public function hasErrors(): bool + { + return $this->errorsCount > 0; + } + + public function resetErrors(): void + { + $this->errorsCount = 0; + } + + public function getRejectedOrganizations(): Collection + { + return Cache::driver('array') + ->rememberForever( + 'import-rejected-organizations', + fn () => $this->db + ->table('dbo.ONGs') + ->orderBy('dbo.ONGs.Id') + ->select([ + 'dbo.ONGs.Id', + 'dbo.ONGs.CIF', + 'dbo.ONGs.ONGStatusId', + 'ProjectsCount' => $this->db + ->table('dbo.ONGProjects') + ->whereColumn('dbo.ONGs.Id', 'dbo.ONGProjects.ONGId') + ->selectRaw('count(*)'), + ]) + ->get() + ->groupBy('CIF') + ->reject(fn (Collection $collection) => $collection->count() < 2) + ->flatMap( + fn (Collection $collection) => $collection + ->sortBy([ + ['ONGStatusId', 'asc'], + ['ProjectsCount', 'desc'], + ]) + ->skip(1) + ) + ->pluck('Id') + ); + } + + public function addFilesToCollection(Model $model, int|array|null $fileIds, string $collection = 'default'): void + { + $this->db + ->table('dbo.Files') + ->join('dbo.FilesData', 'dbo.FilesData.Id', 'dbo.Files.Id') + ->whereIn( + 'dbo.Files.Id', + collect($fileIds) + ->filter() + ->all() + ) + ->get() + ->each(function (object $file) use ($model, $collection) { + $filename = rtrim($file->FileName, '.') . '.' . ltrim($file->FileExtension, '.'); + + $model->addMediaFromString($file->Data) + ->usingFileName($filename) + ->usingName($filename) + ->toMediaCollection($collection); + }); + } + + public function parseDate(?string $input): ?Carbon + { + if ($input === null) { + return null; + } + + return Carbon::createFromFormat('M d Y H:i:s:A', $input); + } +} diff --git a/app/Console/Commands/Import/ImportArticlesCommand.php b/app/Console/Commands/Import/ImportArticlesCommand.php new file mode 100644 index 00000000..30512943 --- /dev/null +++ b/app/Console/Commands/Import/ImportArticlesCommand.php @@ -0,0 +1,143 @@ +confirmToProceed()) { + return static::FAILURE; + } + + $this->importArticleCategories(); + $this->importArticles(); + + return static::SUCCESS; + } + + protected function importArticleCategories(): void + { + $articleCategories = $this->db + ->table('lkp.ArticleCategories') + ->orderBy('lkp.ArticleCategories.Id') + ->get(); + + $this->createProgressBar('Importing article categories...', $articleCategories->count()); + + foreach ($this->progressBar->iterate($articleCategories) as $row) { + ArticleCategory::forceCreate([ + 'id' => (int) $row->Id, + 'name' => Sanitize::text($row->Name), + 'slug' => Sanitize::slug($row->Name), + ]); + } + + $this->finishProgressBar('Imported article categories'); + } + + protected function importArticles(): void + { + $query = $this->db + ->table('dbo.Articles') + ->addSelect([ + 'dbo.Articles.*', + 'MainImageId' => $this->db + ->table('dbo.ArticleImages') + ->select('ImageId') + ->whereColumn('dbo.ArticleImages.ArticleId', 'dbo.Articles.Id') + ->where('dbo.ArticleImages.IsMainImage', 1), + 'GalleryImageIds' => $this->db + ->table('dbo.ArticleImages') + ->selectRaw("STRING_AGG(ImageId,',')") + ->whereColumn('dbo.ArticleImages.ArticleId', 'dbo.Articles.Id') + ->where('dbo.ArticleImages.IsMainImage', 0), + 'AttachmentIds' => $this->db + ->table('dbo.ArticleAttachments') + ->selectRaw("STRING_AGG(GenericFileId,',')") + ->whereColumn('dbo.ArticleAttachments.ArticleId', 'dbo.Articles.Id'), + ]) + ->orderBy('dbo.Articles.Id'); + + $this->createProgressBar( + $this->option('skip-files') + ? 'Importing articles [skip-files]...' + : 'Importing articles...', + $query->count() + ); + + $query->chunk((int) $this->option('chunk'), function (Collection $items) { + $items->each(function (object $row) { + $created_at = Carbon::parse($row->CreationDate); + + try { + $article = Article::forceCreate([ + 'id' => (int) $row->Id, + 'title' => Sanitize::text($row->Title), + 'slug' => Sanitize::text($row->DynamicUrl), + 'author' => Sanitize::text($row->Author), + 'content' => $row->Content, + 'article_category_id' => (int) $row->ArticleCategoryId, + 'created_at' => $created_at, + 'updated_at' => $created_at, + 'published_at' => $this->parseDate($row->PublishDate), + ]); + + if (! $this->option('skip-files')) { + // Add main image + $this->addFilesToCollection($article, $row->MainImageId, 'preview'); + + // Add gallery images + if ($row->GalleryImageIds) { + $this->addFilesToCollection($article, explode(',', $row->GalleryImageIds), 'gallery'); + } + + // Add attachments + if ($row->AttachmentIds) { + $this->addFilesToCollection($article, explode(',', $row->AttachmentIds)); + } + } + } catch (Throwable $th) { + $this->logError('Error importing article #' . $row->Id, [$th->getMessage()]); + } + + $this->progressBar->advance(); + }); + }); + + $this->finishProgressBar( + $this->option('skip-files') + ? 'Imported article [skip-files]' + : 'Imported article' + ); + } +} diff --git a/app/Console/Commands/Import/ImportCommand.php b/app/Console/Commands/Import/ImportCommand.php new file mode 100644 index 00000000..077d0842 --- /dev/null +++ b/app/Console/Commands/Import/ImportCommand.php @@ -0,0 +1,66 @@ +confirmToProceed()) { + return static::FAILURE; + } + + $this->call(ImportPrepareCommand::class, [ + '--force' => $this->option('force'), + ]); + + $this->call(ImportOrganizationsCommand::class, [ + '--skip-files' => $this->option('skip-files'), + '--force' => $this->option('force'), + ]); + + $this->call(ImportUsersCommand::class, [ + '--force' => $this->option('force'), + ]); + + $this->call(ImportProjectsCommand::class, [ + '--skip-files' => $this->option('skip-files'), + '--force' => $this->option('force'), + ]); + + $this->call(ImportDonationsCommand::class, [ + '--force' => $this->option('force'), + ]); + + $this->call(ImportArticlesCommand::class, [ + '--skip-files' => $this->option('skip-files'), + '--force' => $this->option('force'), + ]); + + return static::SUCCESS; + } +} diff --git a/app/Console/Commands/Import/ImportDonationsCommand.php b/app/Console/Commands/Import/ImportDonationsCommand.php new file mode 100644 index 00000000..6b1df5d3 --- /dev/null +++ b/app/Console/Commands/Import/ImportDonationsCommand.php @@ -0,0 +1,61 @@ +confirmToProceed()) { + return static::FAILURE; + } + + $query = $this->db + ->table('dbo.Donations') + ->orderBy('dbo.Donations.Id'); + + $this->createProgressBar('Importing donations...', $query->count()); + + $query->chunk((int) $this->option('chunk'), function (Collection $items) { + $items + ->reject(fn (object $row) => $this->getRejectedOrganizations()->contains($row->ONGId)) + ->each(function (object $row) { + try { + // TODO: import donations + } catch (Throwable $th) { + $this->logError('Error importing donation #' . $row->Id, [$th->getMessage()]); + } + + $this->progressBar->advance(); + }); + }); + + $this->finishProgressBar('Imported donations'); + + return static::SUCCESS; + } +} diff --git a/app/Console/Commands/Import/ImportOrganizationsCommand.php b/app/Console/Commands/Import/ImportOrganizationsCommand.php new file mode 100644 index 00000000..6766be86 --- /dev/null +++ b/app/Console/Commands/Import/ImportOrganizationsCommand.php @@ -0,0 +1,161 @@ +confirmToProceed()) { + return static::FAILURE; + } + + $this->importOrganizations(); + $this->importActivityDomains(); + + return static::SUCCESS; + } + + protected function importOrganizations(): void + { + $query = $this->db + ->table('dbo.ONGs') + ->orderBy('dbo.ONGs.Id'); + + $this->createProgressBar( + $this->option('skip-files') + ? 'Importing organizations [skip-files]...' + : 'Importing organizations...', + $query->count() + ); + + $query->chunk((int) $this->option('chunk'), function (Collection $items) { + $items + ->reject(fn (object $row) => $this->getRejectedOrganizations()->contains($row->Id)) + ->each(function (object $row) { + try { + $created_at = Carbon::parse($row->CreationDate); + + $organization = Organization::forceCreate([ + 'id' => (int) $row->Id, + 'cif' => Sanitize::text($row->CIF), + 'name' => Sanitize::text($row->Name), + 'slug' => Sanitize::text($row->DynamicUrl), + 'description' => Sanitize::text($row->Description), + 'address' => Sanitize::text($row->Address, 255), + 'contact_phone' => Sanitize::text($row->PhoneNb), + 'contact_email' => Sanitize::email($row->Email), + 'contact_person' => Sanitize::text($row->ContactPerson), + 'website' => Sanitize::url($row->WebSite), + 'facebook' => Sanitize::url($row->FacebookPageLink), + 'accepts_volunteers' => Sanitize::truthy($row->HasVolunteering), + 'why_volunteer' => Sanitize::text($row->WhyVolunteer), + 'status' => match ($row->ONGStatusId) { + 1 => OrganizationStatus::pending, + 2 => OrganizationStatus::approved, + 3 => OrganizationStatus::rejected, + }, + 'status_updated_at' => $created_at, + 'created_at' => $created_at, + 'updated_at' => $created_at, + 'eu_platesc_merchant_id' => Sanitize::text($row->MerchantId), + 'eu_platesc_private_key' => Sanitize::text($row->MerchantKey), + ]); + + if (! $this->option('skip-files')) { + // Add logo + $this->addFilesToCollection($organization, $row->LogoImageId, 'logo'); + + // Add statute + $this->addFilesToCollection($organization, $row->OrganizationalStatusId, 'statute'); + + // Add annual report + $this->addFilesToCollection($organization, $row->AnualReportFileId); + } + } catch (Throwable $th) { + $this->logError('Error importing organization #' . $row->Id, [$th->getMessage()]); + } + + $this->progressBar->advance(); + }); + }); + + $this->finishProgressBar( + $this->option('skip-files') + ? 'Imported organizations [skip-files]' + : 'Imported organizations', + ); + } + + protected function importActivityDomains(): void + { + $activityDomains = $this->db + ->table('lkp.ActivityDomains') + ->addSelect([ + 'ONGsIds' => $this->db + ->table('dbo.ONGActivityDomains') + ->selectRaw("STRING_AGG(ONGId,',')") + ->whereColumn('dbo.ONGActivityDomains.ActivityDomainId', 'lkp.ActivityDomains.Id'), + ]) + ->orderBy('lkp.ActivityDomains.Id') + ->get(); + + $this->createProgressBar('Importing activity domains...', $activityDomains->count()); + + $organizations = Organization::query() + ->orderBy('id') + ->pluck('id'); + + $activityDomains->each(function (object $row) use ($organizations) { + try { + $activityDomain = ActivityDomain::forceCreate([ + 'id' => (int) $row->Id, + 'name' => Sanitize::text($row->Name), + 'slug' => Sanitize::slug($row->Name), + ]); + + if ($row->ONGsIds) { + $activityDomain->organizations()->sync( + $organizations->intersect(explode(',', $row->ONGsIds)) + ); + } + } catch (Throwable $th) { + $this->logError('Error importing activity domain #' . $row->Id, [$th->getMessage()]); + } + + $this->progressBar->advance(); + }); + + $this->finishProgressBar('Imported activity domains'); + } +} diff --git a/app/Console/Commands/Import/ImportPrepareCommand.php b/app/Console/Commands/Import/ImportPrepareCommand.php new file mode 100644 index 00000000..9ed06a34 --- /dev/null +++ b/app/Console/Commands/Import/ImportPrepareCommand.php @@ -0,0 +1,46 @@ +confirmToProceed()) { + return static::FAILURE; + } + + $this->createProgressBar('Removing old data...', 2); + + $this->callSilent(FreshCommand::class); + $this->progressBar->advance(); + + $this->callSilent(CleanCommand::class); + + $this->finishProgressBar('Removed old data'); + + return static::SUCCESS; + } +} diff --git a/app/Console/Commands/Import/ImportProjectsCommand.php b/app/Console/Commands/Import/ImportProjectsCommand.php new file mode 100644 index 00000000..eb9c6d62 --- /dev/null +++ b/app/Console/Commands/Import/ImportProjectsCommand.php @@ -0,0 +1,154 @@ +confirmToProceed()) { + return static::FAILURE; + } + + $this->importProjectCategories(); + $this->importProjects(); + + return static::SUCCESS; + } + + protected function importProjectCategories(): void + { + $projectCategories = $this->db + ->table('lkp.ProjectCategories') + ->orderBy('lkp.ProjectCategories.Id') + ->get(); + + $this->createProgressBar('Importing project categories...', $projectCategories->count()); + + ProjectCategory::upsert( + $projectCategories->map(fn (object $row) => [ + 'id' => (int) $row->Id, + 'name' => Sanitize::text($row->Name), + 'slug' => Sanitize::slug($row->Name), + ])->all(), + 'id' + ); + + $this->finishProgressBar('Imported project categories'); + } + + protected function importProjects(): void + { + $query = $this->db + ->table('dbo.ONGProjects') + ->addSelect([ + 'dbo.ONGProjects.*', + 'dbo.Projects.*', + 'MainImageId' => $this->db + ->table('dbo.ProjectImages') + ->select('ImageId') + ->whereColumn('dbo.ProjectImages.ProjectId', 'dbo.ONGProjects.Id') + ->where('dbo.ProjectImages.IsMainImage', 1), + 'GalleryImageIds' => $this->db + ->table('dbo.ProjectImages') + ->selectRaw("STRING_AGG(ImageId,',')") + ->whereColumn('dbo.ProjectImages.ProjectId', 'dbo.ONGProjects.Id') + ->where('dbo.ProjectImages.IsMainImage', 0), + ]) + ->join('dbo.Projects', 'dbo.Projects.Id', 'dbo.ONGProjects.Id') + ->orderBy('dbo.ONGProjects.Id'); + + $this->createProgressBar( + $this->option('skip-files') + ? 'Importing projects [skip-files]...' + : 'Importing projects...', + $query->count() + ); + + $query->chunk((int) $this->option('chunk'), function (Collection $items) { + $items + ->reject(fn (object $row) => $this->getRejectedOrganizations()->contains($row->ONGId)) + ->each(function (object $row) { + $created_at = Carbon::parse($row->CreationDate); + + try { + $project = Project::forceCreate([ + 'id' => (int) $row->Id, + 'organization_id' => (int) $row->ONGId, + 'name' => Sanitize::text($row->Name), + 'slug' => Sanitize::text($row->DynamicUrl), + 'description' => Sanitize::text($row->Description), + 'target_budget' => (int) $row->TargetAmmount, + 'start' => $this->parseDate($row->StartDate), + 'end' => $this->parseDate($row->EndDate), + 'accepting_volunteers' => (bool) $row->HasVolunteering, + 'accepting_comments' => (bool) $row->AcceptComments, + + 'created_at' => $created_at, + 'updated_at' => $created_at, + + 'status' => match ($row->ProjectStatusTypeId) { + 1 => ProjectStatus::pending, + 2 => ProjectStatus::approved, + 3 => ProjectStatus::approved,//set arhivat + 4 => ProjectStatus::approved, + default => ProjectStatus::draft, + }, + ]); + + $project->categories()->attach($row->ProjectCategoryId); + + if (! $this->option('skip-files')) { + // Add main image + $this->addFilesToCollection($project, $row->MainImageId, 'preview'); + + // Add gallery images + if ($row->GalleryImageIds) { + $this->addFilesToCollection($project, explode(',', $row->GalleryImageIds), 'gallery'); + } + } + } catch (Throwable $th) { + $this->logError('Error importing project #' . $row->Id, [$th->getMessage()]); + } + + $this->progressBar->advance(); + }); + }); + + $this->finishProgressBar( + $this->option('skip-files') + ? 'Imported projects [skip-files]' + : 'Imported projects' + ); + } +} diff --git a/app/Console/Commands/Import/ImportUsersCommand.php b/app/Console/Commands/Import/ImportUsersCommand.php new file mode 100644 index 00000000..a877f2c6 --- /dev/null +++ b/app/Console/Commands/Import/ImportUsersCommand.php @@ -0,0 +1,89 @@ +confirmToProceed()) { + return static::FAILURE; + } + + $query = $this->db + ->table('user.Users') + ->where('user.Users.IsActivated', 1) + ->leftJoin('dbo.ONGAdministrators', 'dbo.ONGAdministrators.UserId', 'user.Users.Id') + ->select([ + 'user.Users.*', + 'dbo.ONGAdministrators.ONGId', + ]) + ->orderBy('user.Users.Id'); + + $this->createProgressBar('Importing users...', $query->count()); + + $query->chunk((int) $this->option('chunk'), function (Collection $items, int $page) { + try { + $values = $items + ->reject(fn (object $row) => $this->getRejectedOrganizations()->contains($row->ONGId)) + ->map(fn (object $row) => [ + 'id' => $row->Id, + 'name' => $row->FirstName . ' ' . $row->LastName, + 'email' => $row->Email, + 'old_password' => $row->Password, + 'old_salt' => $row->PasswordSalt, + 'created_at' => Carbon::parse($row->CreationDate), + 'updated_at' => $row->ActivationCodeGeneratedDate, + 'password_set_at' => $row->ActivationCodeGeneratedDate, + 'email_verified_at' => $row->IsActivated ? $row->ActivationCodeGeneratedDate : null, + 'role' => match ($row->RoleId) { + 1 => UserRole::USER, + 2 => UserRole::ADMIN, + 3 => UserRole::SUPERADMIN, + }, + 'organization_id' => match ($row->RoleId) { + 2 => $row->ONGId, + default => null, + }, + ]); + + User::upsert($values->all(), 'email'); + } catch (Throwable $th) { + $this->logError('Error importing user batch #' . $page, [$th->getMessage()]); + } + + $this->progressBar->advance($values->count()); + }); + + $this->finishProgressBar('Imported users'); + + return static::SUCCESS; + } +} diff --git a/app/Enums/OrganizationStatus.php b/app/Enums/OrganizationStatus.php index 135bc66a..eb8eecb4 100644 --- a/app/Enums/OrganizationStatus.php +++ b/app/Enums/OrganizationStatus.php @@ -16,8 +16,8 @@ enum OrganizationStatus: string case draft = 'draft'; case pending = 'pending'; - case approved = 'active'; - case rejected = 'disabled'; + case approved = 'approved'; + case rejected = 'rejected'; case pending_changes = 'pending_changes'; protected function labelKeyPrefix(): ?string diff --git a/app/Filament/Resources/Articles/ArticleResource.php b/app/Filament/Resources/Articles/ArticleResource.php index 5a148697..a28ea60c 100644 --- a/app/Filament/Resources/Articles/ArticleResource.php +++ b/app/Filament/Resources/Articles/ArticleResource.php @@ -9,11 +9,11 @@ use App\Models\Article; use Camya\Filament\Forms\Components\TitleWithSlugInput; use Filament\Forms\Components\Card; +use Filament\Forms\Components\DateTimePicker; use Filament\Forms\Components\Group; use Filament\Forms\Components\Select; use Filament\Forms\Components\SpatieMediaLibraryFileUpload; use Filament\Forms\Components\TextInput; -use Filament\Forms\Components\Toggle; use Filament\Resources\Concerns\Translatable; use Filament\Resources\Form; use Filament\Resources\Resource; @@ -22,6 +22,7 @@ use Filament\Tables\Columns\TextColumn; use Filament\Tables\Filters\SelectFilter; use FilamentTiptapEditor\TiptapEditor; +use Illuminate\Database\Eloquent\Builder; class ArticleResource extends Resource { @@ -55,6 +56,12 @@ protected static function getNavigationBadge(): ?string return (string) Article::count(); } + public static function getEloquentQuery(): Builder + { + return parent::getEloquentQuery() + ->withDrafted(); + } + public static function form(Form $form): Form { return $form @@ -132,8 +139,9 @@ public static function form(Form $form): Form ->label(__('article.updated_at')) ->withTime(), - Toggle::make('is_published') - ->label(__('article.is_published')), + DateTimePicker::make('published_at') + ->label(__('article.published_at')) + ->withoutSeconds(), ]), ]); diff --git a/app/Filament/Resources/OrganizationResource.php b/app/Filament/Resources/OrganizationResource.php index 45344abb..7df86295 100644 --- a/app/Filament/Resources/OrganizationResource.php +++ b/app/Filament/Resources/OrganizationResource.php @@ -162,8 +162,8 @@ public static function form(Form $form): Form ->required() ->maxLength(255), - TextInput::make('street_address') - ->label(__('organization.labels.street_address')) + TextInput::make('address') + ->label(__('organization.labels.address')) ->inlineLabel() ->required() ->maxLength(255), diff --git a/app/Filament/Resources/OrganizationResource/Actions/Tables/ExportAction.php b/app/Filament/Resources/OrganizationResource/Actions/Tables/ExportAction.php index 2a31c9a1..bde8f74b 100644 --- a/app/Filament/Resources/OrganizationResource/Actions/Tables/ExportAction.php +++ b/app/Filament/Resources/OrganizationResource/Actions/Tables/ExportAction.php @@ -82,8 +82,8 @@ protected function setUp(): void Column::make('website') ->heading(__('organization.labels.website')), - Column::make('street_address') - ->heading(__('organization.labels.street_address')), + Column::make('address') + ->heading(__('organization.labels.address')), Column::make('counties') ->heading(__('organization.labels.counties')) diff --git a/app/Filament/Resources/OrganizationResource/Pages/ViewOrganization.php b/app/Filament/Resources/OrganizationResource/Pages/ViewOrganization.php index da034300..e77fb8a7 100644 --- a/app/Filament/Resources/OrganizationResource/Pages/ViewOrganization.php +++ b/app/Filament/Resources/OrganizationResource/Pages/ViewOrganization.php @@ -30,11 +30,11 @@ protected function getActions(): array ReactivateOrganizationAction::make() ->record($this->getRecord()) - ->visible($this->getRecord()->isDisabled()), + ->visible($this->getRecord()->isRejected()), DeactivateOrganizationAction::make() ->record($this->getRecord()) - ->visible($this->getRecord()->isActive()), + ->visible($this->getRecord()->isApproved()), ]; } diff --git a/app/Filament/Resources/ProjectResource/Actions/Tables/ExportAction.php b/app/Filament/Resources/ProjectResource/Actions/Tables/ExportAction.php index 16222f35..d3f1ccbb 100644 --- a/app/Filament/Resources/ProjectResource/Actions/Tables/ExportAction.php +++ b/app/Filament/Resources/ProjectResource/Actions/Tables/ExportAction.php @@ -82,8 +82,8 @@ protected function setUp(): void Column::make('website') ->heading(__('organization.labels.website')), - Column::make('street_address') - ->heading(__('organization.labels.street_address')), + Column::make('address') + ->heading(__('organization.labels.address')), Column::make('counties') ->heading(__('organization.labels.counties')) diff --git a/app/Http/Controllers/ArticleController.php b/app/Http/Controllers/ArticleController.php index 1dade08e..ace96660 100644 --- a/app/Http/Controllers/ArticleController.php +++ b/app/Http/Controllers/ArticleController.php @@ -21,12 +21,10 @@ public function index(Request $request): Response 'collection' => ArticleCardResource::collection( Article::query() ->orderByDesc('id') - ->wherePublished() ->paginate(5) ), 'topArticles' => ArticleCardResource::collection( Article::query() - ->wherePublished() ->inRandomOrder() ->limit(3) ->get() @@ -42,13 +40,11 @@ public function category(ArticleCategory $category, Request $request): Response 'collection' => ArticleCardResource::collection( Article::query() ->whereBelongsTo($category, 'category') - ->wherePublished() ->orderByDesc('id') ->paginate(5) ), 'topArticles' => ArticleCardResource::collection( Article::query() - ->wherePublished() ->inRandomOrder() ->limit(3) ->get() @@ -61,7 +57,6 @@ public function show(Article $article): Response return Inertia::render('Public/Articles/Show', [ 'resource' => ArticleResource::make($article), 'related' => ArticleCardResource::collection(Article::query() - ->wherePublished() ->whereBelongsTo($article->category, 'category') ->whereNot('id', $article->id) ->inRandomOrder() diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 2df303ea..ce349527 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -44,7 +44,6 @@ public function index() 'articles' => ArticleCardResource::collection( Article::query() ->latest() - ->wherePublished() ->limit(3) ->get() ), diff --git a/app/Http/Controllers/OrganizationController.php b/app/Http/Controllers/OrganizationController.php index 18ef00c3..861782cc 100644 --- a/app/Http/Controllers/OrganizationController.php +++ b/app/Http/Controllers/OrganizationController.php @@ -43,7 +43,7 @@ public function index(Request $request) public function show(Organization $organization) { - if (! $organization->isActive()) { + if (! $organization->isApproved()) { if (! auth()->check()) { abort(404); } diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php index 16458852..f328b910 100644 --- a/app/Http/Controllers/ProjectController.php +++ b/app/Http/Controllers/ProjectController.php @@ -80,7 +80,7 @@ public function show(Project $project) ]); } - public function donation(Project $project, Request $request) + public function donate(Project $project, Request $request) { $request->validate([ 'amount' => 'required|numeric|min:1', diff --git a/app/Http/Requests/Organization/UpdateOrganizationRequest.php b/app/Http/Requests/Organization/UpdateOrganizationRequest.php index 3643858e..a22efbe0 100644 --- a/app/Http/Requests/Organization/UpdateOrganizationRequest.php +++ b/app/Http/Requests/Organization/UpdateOrganizationRequest.php @@ -21,7 +21,7 @@ public function rules(): array 'description' => ['nullable', 'string'], 'logo' => ['nullable', 'file', 'mimes:jpg,png'], 'statute' => ['nullable', 'file', 'mimes:pdf'], - 'street_address' => ['nullable', 'string'], + 'address' => ['nullable', 'string'], 'cif' => ['nullable', 'string', 'unique:organizations,cif', new ValidCIF], 'contact_email' => ['nullable', 'email'], 'contact_phone' => ['nullable', 'string'], diff --git a/app/Http/Requests/RegistrationRequest.php b/app/Http/Requests/RegistrationRequest.php index b1c092d5..d68f29da 100644 --- a/app/Http/Requests/RegistrationRequest.php +++ b/app/Http/Requests/RegistrationRequest.php @@ -33,7 +33,7 @@ public function rules(): array 'ngo.description' => ['required', 'string', 'max:1000'], 'ngo.logo' => ['required', 'image'], 'ngo.statute' => ['required', 'file', 'mimes:pdf', 'max:15240'], - 'ngo.street_address' => ['required', 'string'], + 'ngo.address' => ['required', 'string'], 'ngo.cif' => ['required', 'string', 'unique:organizations,cif', new ValidCIF], 'ngo.contact_email' => ['required', 'email'], 'ngo.contact_phone' => ['nullable', 'string'], diff --git a/app/Http/Resources/OrganizationCardsResource.php b/app/Http/Resources/OrganizationCardsResource.php index 9e1cbb62..71c4e3b3 100644 --- a/app/Http/Resources/OrganizationCardsResource.php +++ b/app/Http/Resources/OrganizationCardsResource.php @@ -20,6 +20,7 @@ public function toArray(Request $request): array return [ 'id' => $this->id, 'name' => $this->name, + 'slug' => $this->slug, 'logo' => $this->getFirstMediaUrl('logo'), 'activity_domains' => $this->activityDomains->pluck('name')->join(', '), ]; diff --git a/app/Http/Resources/Organizations/EditOrganizationResource.php b/app/Http/Resources/Organizations/EditOrganizationResource.php index 30540b7c..31c6974e 100644 --- a/app/Http/Resources/Organizations/EditOrganizationResource.php +++ b/app/Http/Resources/Organizations/EditOrganizationResource.php @@ -24,7 +24,7 @@ public function toArray(Request $request): array 'logo' => $this->getFirstMediaUrl('logo', 'preview'), 'statute_link' => $this->getFirstMediaUrl('statute'), 'description' => $this->description, - 'street_address' => $this->street_address, + 'address' => $this->address, 'contact_person' => $this->contact_person, 'contact_phone' => $this->contact_phone, 'contact_email' => $this->contact_email, diff --git a/app/Http/Resources/Organizations/ShowOrganizationResource.php b/app/Http/Resources/Organizations/ShowOrganizationResource.php index 238b6179..2e59d06f 100644 --- a/app/Http/Resources/Organizations/ShowOrganizationResource.php +++ b/app/Http/Resources/Organizations/ShowOrganizationResource.php @@ -22,7 +22,7 @@ public function toArray(Request $request): array 'activity_domains' => $this->activityDomains->pluck('name')->join(', '), 'logo' => $this->getFirstMediaUrl('logo', 'preview'), 'description' => $this->description, - 'street_address' => $this->street_address, + 'address' => $this->address, 'contact_person' => $this->contact_person, 'contact_phone' => $this->contact_phone, 'contact_email' => $this->contact_email, diff --git a/app/Http/Resources/ProjectCardResource.php b/app/Http/Resources/ProjectCardResource.php index b61c146a..e35f28af 100644 --- a/app/Http/Resources/ProjectCardResource.php +++ b/app/Http/Resources/ProjectCardResource.php @@ -21,8 +21,8 @@ public function toArray(Request $request): array 'image' => $this->getFirstMediaUrl('preview'), 'organization' => [ 'name' => $this->organization->name, + 'slug' => $this->organization->slug, 'id' => $this->organization->id, - ], 'categories' => $this->categories->pluck('name')->join(', '), 'donations' => [ diff --git a/app/Models/ActivityDomain.php b/app/Models/ActivityDomain.php index eec14963..65c70679 100644 --- a/app/Models/ActivityDomain.php +++ b/app/Models/ActivityDomain.php @@ -4,12 +4,29 @@ namespace App\Models; +use App\Concerns\HasSlug; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; class ActivityDomain extends Model { use HasFactory; + use HasSlug; public $timestamps = false; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'name', + ]; + + public function organizations(): BelongsToMany + { + return $this->belongsToMany(Organization::class); + } } diff --git a/app/Models/Article.php b/app/Models/Article.php index c3b6cc2c..434c1d39 100644 --- a/app/Models/Article.php +++ b/app/Models/Article.php @@ -6,7 +6,7 @@ use App\Concerns\HasSlug; use App\Concerns\HasTranslations; -use Illuminate\Database\Eloquent\Builder; +use App\Concerns\Publishable; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -22,12 +22,12 @@ class Article extends Model implements HasMedia use InteractsWithMedia; use HasSlug; use HasTranslations; + use Publishable; protected $fillable = [ 'title', 'slug', 'content', - 'is_published', 'article_category_id', 'author', ]; @@ -52,13 +52,11 @@ public function registerMediaCollections(): void ->registerMediaConversions(function (Media $media) { $this ->addMediaConversion('thumb') - ->fit(Manipulations::FIT_CONTAIN, 400, 200) - ->nonQueued(); + ->fit(Manipulations::FIT_CONTAIN, 400, 200); $this ->addMediaConversion('large') - ->fit(Manipulations::FIT_CONTAIN, 1200, 600) - ->nonQueued(); + ->fit(Manipulations::FIT_CONTAIN, 1200, 600); }); $this->addMediaCollection('gallery') @@ -66,13 +64,11 @@ public function registerMediaCollections(): void ->registerMediaConversions(function (Media $media) { $this ->addMediaConversion('thumb') - ->fit(Manipulations::FIT_CONTAIN, 400, 200) - ->nonQueued(); + ->fit(Manipulations::FIT_CONTAIN, 400, 200); $this ->addMediaConversion('large') - ->fit(Manipulations::FIT_CONTAIN, 1200, 600) - ->nonQueued(); + ->fit(Manipulations::FIT_CONTAIN, 1200, 600); }); } @@ -80,9 +76,4 @@ public function category(): BelongsTo { return $this->belongsTo(ArticleCategory::class, 'article_category_id', 'id'); } - - public function scopeWherePublished(Builder $query): Builder - { - return $query->where('is_published', true); - } } diff --git a/app/Models/BCRProject.php b/app/Models/BCRProject.php index b0a6080b..ea8f2678 100644 --- a/app/Models/BCRProject.php +++ b/app/Models/BCRProject.php @@ -60,16 +60,14 @@ public function registerMediaCollections(): void ->registerMediaConversions(function (Media $media) { $this ->addMediaConversion('preview') - ->fit(Manipulations::FIT_CONTAIN, 300, 300) - ->nonQueued(); + ->fit(Manipulations::FIT_CONTAIN, 300, 300); }); $this->addMediaCollection('gallery') ->registerMediaConversions(function (Media $media) { $this ->addMediaConversion('preview') - ->fit(Manipulations::FIT_CONTAIN, 300, 300) - ->nonQueued(); + ->fit(Manipulations::FIT_CONTAIN, 300, 300); }); } diff --git a/app/Models/Badge.php b/app/Models/Badge.php index 819fb97a..cb71c5d9 100644 --- a/app/Models/Badge.php +++ b/app/Models/Badge.php @@ -41,8 +41,7 @@ public function registerMediaCollections(): void ->singleFile() ->registerMediaConversions(function (Media $media) { $this->addMediaConversion('thumb') - ->fit(Manipulations::FIT_CROP, 300, 300) - ->nonQueued(); + ->fit(Manipulations::FIT_CROP, 300, 300); }); } diff --git a/app/Models/Organization.php b/app/Models/Organization.php index 49d4c43d..1d789fa9 100644 --- a/app/Models/Organization.php +++ b/app/Models/Organization.php @@ -5,6 +5,7 @@ namespace App\Models; use App\Concerns\HasCounties; +use App\Concerns\HasSlug; use App\Concerns\HasVolunteers; use App\Concerns\LogsActivityForApproval; use App\Enums\OrganizationStatus; @@ -32,6 +33,7 @@ class Organization extends Model implements HasMedia use HasCounties; use HasVolunteers; use HasOrganizationStatus; + use HasSlug; use LogsActivityForApproval; /** @@ -43,11 +45,12 @@ class Organization extends Model implements HasMedia 'name', 'cif', 'description', - 'street_address', + 'address', 'contact_person', 'contact_phone', 'contact_email', 'website', + 'facebook', 'accepts_volunteers', 'why_volunteer', 'status', @@ -74,7 +77,7 @@ class Organization extends Model implements HasMedia public array $requiresApproval = [ 'name', 'cif', - 'street_address', + 'address', 'statute', ]; @@ -96,8 +99,7 @@ public function registerMediaCollections(): void ->registerMediaConversions(function (Media $media) { $this ->addMediaConversion('preview') - ->fit(Manipulations::FIT_CONTAIN, 300, 300) - ->nonQueued(); + ->fit(Manipulations::FIT_CONTAIN, 300, 300); }); $this->addMediaCollection('statute') diff --git a/app/Models/Project.php b/app/Models/Project.php index 598ebf22..7b72e1a7 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -5,6 +5,7 @@ namespace App\Models; use App\Concerns\HasCounties; +use App\Concerns\HasSlug; use App\Concerns\HasVolunteers; use App\Concerns\LogsActivityForApproval; use App\Enums\ProjectStatus; @@ -35,6 +36,7 @@ class Project extends Model implements HasMedia use HasVolunteers; use InteractsWithMedia; use HasProjectStatus; + use HasSlug; use LogsActivity; use LogsActivityForApproval; @@ -88,16 +90,14 @@ public function registerMediaCollections(): void ->registerMediaConversions(function (Media $media) { $this ->addMediaConversion('preview') - ->fit(Manipulations::FIT_CONTAIN, 300, 300) - ->nonQueued(); + ->fit(Manipulations::FIT_CONTAIN, 300, 300); }); $this->addMediaCollection('gallery') ->registerMediaConversions(function (Media $media) { $this ->addMediaConversion('preview') - ->fit(Manipulations::FIT_CONTAIN, 300, 300) - ->nonQueued(); + ->fit(Manipulations::FIT_CONTAIN, 300, 300); }); } diff --git a/app/Models/ProjectCategory.php b/app/Models/ProjectCategory.php index 2189ec6a..f856dbff 100644 --- a/app/Models/ProjectCategory.php +++ b/app/Models/ProjectCategory.php @@ -4,6 +4,7 @@ namespace App\Models; +use App\Concerns\HasSlug; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; @@ -11,10 +12,10 @@ class ProjectCategory extends Model { use HasFactory; + use HasSlug; protected $fillable = [ 'name', - 'slug', ]; public function projects(): BelongsToMany diff --git a/app/Models/RegionalProject.php b/app/Models/RegionalProject.php index 3f2cbab0..79620142 100644 --- a/app/Models/RegionalProject.php +++ b/app/Models/RegionalProject.php @@ -63,8 +63,7 @@ public function registerMediaConversions(Media $media = null): void { $this ->addMediaConversion('preview') - ->fit(Manipulations::FIT_CROP, 300, 300) - ->nonQueued(); + ->fit(Manipulations::FIT_CROP, 300, 300); } public function getActivitylogOptions(): LogOptions diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 27d52eea..e60da20c 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -37,6 +37,9 @@ public function boot(): void ->name('filament.') ->group(base_path('routes/filament.php')); + Route::middleware('web') + ->group(base_path('routes/redirects.php')); + Route::middleware('web') ->group(base_path('routes/web.php')); }); diff --git a/app/Services/EuPlatescService.php b/app/Services/EuPlatescService.php index c9bbd6c3..a416b1ca 100644 --- a/app/Services/EuPlatescService.php +++ b/app/Services/EuPlatescService.php @@ -45,7 +45,7 @@ public function getPaymentData(Donation $donation): array $data['lname'] = $donation->last_name; $data['ExtraData[silenturl]'] = route('donation.callback', $donation->uuid); $data['ExtraData[successurl]'] = route('donation.thanks', $donation->uuid); - $data['ExtraData[backtosite]'] = route('project', $donation->project->slug); + $data['ExtraData[backtosite]'] = route('projects.show', $donation->project->slug); $data['payment_url'] = $this->url; return $data; diff --git a/app/Services/Sanitize.php b/app/Services/Sanitize.php new file mode 100644 index 00000000..b961a02c --- /dev/null +++ b/app/Services/Sanitize.php @@ -0,0 +1,66 @@ +stripTags() + ->trim(); + } + + public static function url(?string $link): ?string + { + $link = static::sanitize($link) + ->value(); + + return collect(filter_var_array([ + $link, + "http://{$link}", + ], \FILTER_VALIDATE_URL)) + ->filter() + ->first(); + } + + public static function email(?string $email): ?string + { + $email = static::sanitize($email) + ->value(); + + if (filter_var($email, \FILTER_VALIDATE_EMAIL)) { + return $email; + } + + return null; + } + + public static function text(?string $text, int | null $limit = null): ?string + { + return static::sanitize($text) + ->when($limit, fn (Stringable $string, int $limit) => $string->limit($limit, '')) + ->value() ?: null; + } + + public static function slug(?string $title): ?string + { + return static::sanitize($title) + ->limit(100, '') + ->slug() + ->value() ?: null; + } + + public static function truthy(string | int | null $source): bool + { + $attribute = Str::of($source)->slug(); + + return $attribute->isNotEmpty() && + ! $attribute->contains(['0', 'nu', 'no', 'non']); + } +} diff --git a/app/Traits/HasOrganizationStatus.php b/app/Traits/HasOrganizationStatus.php index 2bd80e73..e6185fe2 100644 --- a/app/Traits/HasOrganizationStatus.php +++ b/app/Traits/HasOrganizationStatus.php @@ -20,12 +20,12 @@ public function isPending(): bool return $this->status === OrganizationStatus::pending; } - public function isActive(): bool + public function isApproved(): bool { return $this->status === OrganizationStatus::approved; } - public function isDisabled(): bool + public function isRejected(): bool { return $this->status === OrganizationStatus::rejected; } diff --git a/app/Traits/HasProjectStatus.php b/app/Traits/HasProjectStatus.php index ada46d1e..e77242b8 100644 --- a/app/Traits/HasProjectStatus.php +++ b/app/Traits/HasProjectStatus.php @@ -16,22 +16,22 @@ public function initializeHasProjectStatus() public function isPending(): bool { - return $this->status === ProjectStatus::pending; + return ProjectStatus::pending->is($this->status); } public function isApproved(): bool { - return $this->status === ProjectStatus::approved; + return ProjectStatus::approved->is($this->status); } public function isRejected(): bool { - return $this->status === ProjectStatus::rejected; + return ProjectStatus::rejected->is($this->status); } public function isDraft(): bool { - return $this->status === ProjectStatus::draft; + return ProjectStatus::draft->is($this->status); } public function isPublished(): bool diff --git a/config/app.php b/config/app.php index 0a323981..672f25c0 100644 --- a/config/app.php +++ b/config/app.php @@ -72,7 +72,7 @@ | */ - 'timezone' => 'UTC', + 'timezone' => env('APP_TIMEZONE', 'Europe/Bucharest'), /* |-------------------------------------------------------------------------- diff --git a/config/database.php b/config/database.php index d220309b..8a098a3e 100644 --- a/config/database.php +++ b/config/database.php @@ -80,19 +80,18 @@ 'sslmode' => 'prefer', ], - 'sqlsrv' => [ + 'import' => [ 'driver' => 'sqlsrv', - 'url' => env('DATABASE_URL'), - 'host' => env('DB_HOST', 'localhost'), - 'port' => env('DB_PORT', '1433'), - 'database' => env('DB_DATABASE', 'forge'), - 'username' => env('DB_USERNAME', 'forge'), - 'password' => env('DB_PASSWORD', ''), + 'host' => env('IMPORT_DB_HOST', 'localhost'), + 'port' => env('IMPORT_DB_PORT', '1433'), + 'database' => env('IMPORT_DB_DATABASE', 'forge'), + 'username' => env('IMPORT_DB_USERNAME', 'forge'), + 'password' => env('IMPORT_DB_PASSWORD', ''), 'charset' => 'utf8', 'prefix' => '', 'prefix_indexes' => true, - // 'encrypt' => env('DB_ENCRYPT', 'yes'), - // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), + 'encrypt' => env('DB_ENCRYPT', 'no'), + 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), ], ], diff --git a/config/media-library.php b/config/media-library.php new file mode 100644 index 00000000..a99f18f1 --- /dev/null +++ b/config/media-library.php @@ -0,0 +1,258 @@ + env('MEDIA_DISK', 'public'), + + /* + * The maximum file size of an item in bytes. + * Adding a larger file will result in an exception. + */ + 'max_file_size' => 1024 * 1024 * 10, // 10MB + + /* + * This queue connection will be used to generate derived and responsive images. + * Leave empty to use the default queue connection. + */ + 'queue_connection_name' => env('QUEUE_CONNECTION', 'sync'), + + /* + * This queue will be used to generate derived and responsive images. + * Leave empty to use the default queue. + */ + 'queue_name' => '', + + /* + * By default all conversions will be performed on a queue. + */ + 'queue_conversions_by_default' => env('QUEUE_CONVERSIONS_BY_DEFAULT', true), + + /* + * The fully qualified class name of the media model. + */ + 'media_model' => Spatie\MediaLibrary\MediaCollections\Models\Media::class, + + /* + * When enabled, media collections will be serialised using the default + * laravel model serialization behaviour. + * + * Keep this option disabled if using Media Library Pro components (https://medialibrary.pro) + */ + 'use_default_collection_serialization' => false, + + /* + * The fully qualified class name of the model used for temporary uploads. + * + * This model is only used in Media Library Pro (https://medialibrary.pro) + */ + 'temporary_upload_model' => Spatie\MediaLibraryPro\Models\TemporaryUpload::class, + + /* + * When enabled, Media Library Pro will only process temporary uploads that were uploaded + * in the same session. You can opt to disable this for stateless usage of + * the pro components. + */ + 'enable_temporary_uploads_session_affinity' => true, + + /* + * When enabled, Media Library pro will generate thumbnails for uploaded file. + */ + 'generate_thumbnails_for_temporary_uploads' => true, + + /* + * This is the class that is responsible for naming generated files. + */ + 'file_namer' => Spatie\MediaLibrary\Support\FileNamer\DefaultFileNamer::class, + + /* + * The class that contains the strategy for determining a media file's path. + */ + 'path_generator' => Spatie\MediaLibrary\Support\PathGenerator\DefaultPathGenerator::class, + + /* + * Here you can specify which path generator should be used for the given class. + */ + 'custom_path_generators' => [ + // Model::class => PathGenerator::class + // or + // 'model_morph_alias' => PathGenerator::class + ], + + /* + * When urls to files get generated, this class will be called. Use the default + * if your files are stored locally above the site root or on s3. + */ + 'url_generator' => Spatie\MediaLibrary\Support\UrlGenerator\DefaultUrlGenerator::class, + + /* + * Moves media on updating to keep path consistent. Enable it only with a custom + * PathGenerator that uses, for example, the media UUID. + */ + 'moves_media_on_update' => false, + + /* + * Whether to activate versioning when urls to files get generated. + * When activated, this attaches a ?v=xx query string to the URL. + */ + 'version_urls' => false, + + /* + * The media library will try to optimize all converted images by removing + * metadata and applying a little bit of compression. These are + * the optimizers that will be used by default. + */ + 'image_optimizers' => [ + Spatie\ImageOptimizer\Optimizers\Jpegoptim::class => [ + '-m85', // set maximum quality to 85% + '--force', // ensure that progressive generation is always done also if a little bigger + '--strip-all', // this strips out all text information such as comments and EXIF data + '--all-progressive', // this will make sure the resulting image is a progressive one + ], + Spatie\ImageOptimizer\Optimizers\Pngquant::class => [ + '--force', // required parameter for this package + ], + Spatie\ImageOptimizer\Optimizers\Optipng::class => [ + '-i0', // this will result in a non-interlaced, progressive scanned image + '-o2', // this set the optimization level to two (multiple IDAT compression trials) + '-quiet', // required parameter for this package + ], + Spatie\ImageOptimizer\Optimizers\Svgo::class => [ + '--disable=cleanupIDs', // disabling because it is known to cause troubles + ], + Spatie\ImageOptimizer\Optimizers\Gifsicle::class => [ + '-b', // required parameter for this package + '-O3', // this produces the slowest but best results + ], + Spatie\ImageOptimizer\Optimizers\Cwebp::class => [ + '-m 6', // for the slowest compression method in order to get the best compression. + '-pass 10', // for maximizing the amount of analysis pass. + '-mt', // multithreading for some speed improvements. + '-q 90', //quality factor that brings the least noticeable changes. + ], + Spatie\ImageOptimizer\Optimizers\Avifenc::class => [ + '-a cq-level=23', // constant quality level, lower values mean better quality and greater file size (0-63). + '-j all', // number of jobs (worker threads, "all" uses all available cores). + '--min 0', // min quantizer for color (0-63). + '--max 63', // max quantizer for color (0-63). + '--minalpha 0', // min quantizer for alpha (0-63). + '--maxalpha 63', // max quantizer for alpha (0-63). + '-a end-usage=q', // rate control mode set to Constant Quality mode. + '-a tune=ssim', // SSIM as tune the encoder for distortion metric. + ], + ], + + /* + * These generators will be used to create an image of media files. + */ + 'image_generators' => [ + Spatie\MediaLibrary\Conversions\ImageGenerators\Image::class, + Spatie\MediaLibrary\Conversions\ImageGenerators\Webp::class, + Spatie\MediaLibrary\Conversions\ImageGenerators\Avif::class, + Spatie\MediaLibrary\Conversions\ImageGenerators\Pdf::class, + Spatie\MediaLibrary\Conversions\ImageGenerators\Svg::class, + Spatie\MediaLibrary\Conversions\ImageGenerators\Video::class, + ], + + /* + * The path where to store temporary files while performing image conversions. + * If set to null, storage_path('media-library/temp') will be used. + */ + 'temporary_directory_path' => storage_path('app/media-library-tmp'), + + /* + * The engine that should perform the image conversions. + * Should be either `gd` or `imagick`. + */ + 'image_driver' => env('IMAGE_DRIVER', 'imagick'), + + /* + * FFMPEG & FFProbe binaries paths, only used if you try to generate video + * thumbnails and have installed the php-ffmpeg/php-ffmpeg composer + * dependency. + */ + 'ffmpeg_path' => env('FFMPEG_PATH', '/usr/bin/ffmpeg'), + 'ffprobe_path' => env('FFPROBE_PATH', '/usr/bin/ffprobe'), + + /* + * Here you can override the class names of the jobs used by this package. Make sure + * your custom jobs extend the ones provided by the package. + */ + 'jobs' => [ + 'perform_conversions' => Spatie\MediaLibrary\Conversions\Jobs\PerformConversionsJob::class, + 'generate_responsive_images' => Spatie\MediaLibrary\ResponsiveImages\Jobs\GenerateResponsiveImagesJob::class, + ], + + /* + * When using the addMediaFromUrl method you may want to replace the default downloader. + * This is particularly useful when the url of the image is behind a firewall and + * need to add additional flags, possibly using curl. + */ + 'media_downloader' => Spatie\MediaLibrary\Downloaders\DefaultDownloader::class, + + 'remote' => [ + /* + * Any extra headers that should be included when uploading media to + * a remote disk. Even though supported headers may vary between + * different drivers, a sensible default has been provided. + * + * Supported by S3: CacheControl, Expires, StorageClass, + * ServerSideEncryption, Metadata, ACL, ContentEncoding + */ + 'extra_headers' => [ + 'CacheControl' => 'max-age=604800', + ], + ], + + 'responsive_images' => [ + /* + * This class is responsible for calculating the target widths of the responsive + * images. By default we optimize for filesize and create variations that each are 30% + * smaller than the previous one. More info in the documentation. + * + * https://docs.spatie.be/laravel-medialibrary/v9/advanced-usage/generating-responsive-images + */ + 'width_calculator' => Spatie\MediaLibrary\ResponsiveImages\WidthCalculator\FileSizeOptimizedWidthCalculator::class, + + /* + * By default rendering media to a responsive image will add some javascript and a tiny placeholder. + * This ensures that the browser can already determine the correct layout. + */ + 'use_tiny_placeholders' => true, + + /* + * This class will generate the tiny placeholder used for progressive image loading. By default + * the media library will use a tiny blurred jpg image. + */ + 'tiny_placeholder_generator' => Spatie\MediaLibrary\ResponsiveImages\TinyPlaceholderGenerator\Blurred::class, + ], + + /* + * When enabling this option, a route will be registered that will enable + * the Media Library Pro Vue and React components to move uploaded files + * in a S3 bucket to their right place. + */ + 'enable_vapor_uploads' => env('ENABLE_MEDIA_LIBRARY_VAPOR_UPLOADS', false), + + /* + * When converting Media instances to response the media library will add + * a `loading` attribute to the `img` tag. Here you can set the default + * value of that attribute. + * + * Possible values: 'lazy', 'eager', 'auto' or null if you don't want to set any loading instruction. + * + * More info: https://css-tricks.com/native-lazy-loading/ + */ + 'default_loading_attribute_value' => null, + + /* + * You can specify a prefix for that is used for storing all media. + * If you set this to `/my-subdir`, all your media will be stored in a `/my-subdir` directory. + */ + 'prefix' => env('MEDIA_PREFIX', ''), +]; diff --git a/database/factories/ArticleFactory.php b/database/factories/ArticleFactory.php index fcb78687..2e9547ee 100644 --- a/database/factories/ArticleFactory.php +++ b/database/factories/ArticleFactory.php @@ -20,7 +20,7 @@ public function definition(): array 'content' => collect(fake()->paragraphs(10)) ->map(fn (string $paragraph) => "

{$paragraph}

") ->implode(''), - 'is_published' => fake()->boolean(75), + 'published_at' => fake()->boolean(95) ? fake()->dateTimeThisYear() : null, 'author' => fake()->name(), 'article_category_id' => ArticleCategory::factory(), ]; diff --git a/database/factories/OrganizationFactory.php b/database/factories/OrganizationFactory.php index 8b21c103..ac402ddf 100644 --- a/database/factories/OrganizationFactory.php +++ b/database/factories/OrganizationFactory.php @@ -16,6 +16,7 @@ use App\Models\Volunteer; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Sequence; +use Illuminate\Support\Str; /** * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Organization> @@ -29,11 +30,14 @@ class OrganizationFactory extends Factory */ public function definition(): array { + $name = fake()->company(); + return [ - 'name' => fake()->company(), + 'name' => $name, + 'slug' => Str::slug($name), 'cif' => fake()->unixTime(), 'description' => fake()->text(500), - 'street_address' => fake()->streetName(), + 'address' => fake()->address(), 'contact_person' => fake()->name(), 'contact_phone' => fake()->phoneNumber(), 'contact_email' => fake()->companyEmail(), diff --git a/database/migrations/2023_05_05_142228_create_organizations_table.php b/database/migrations/2023_05_05_142228_create_organizations_table.php index c7ed5205..a79a2c38 100644 --- a/database/migrations/2023_05_05_142228_create_organizations_table.php +++ b/database/migrations/2023_05_05_142228_create_organizations_table.php @@ -17,13 +17,15 @@ public function up(): void Schema::create('organizations', function (Blueprint $table) { $table->id(); $table->string('name')->index(); + $table->string('slug')->unique(); $table->string('cif')->unique(); $table->text('description'); - $table->string('street_address'); + $table->string('address'); $table->string('contact_person'); $table->string('contact_phone'); $table->string('contact_email'); $table->string('website')->nullable(); + $table->string('facebook')->nullable(); $table->boolean('accepts_volunteers')->default(true); $table->text('why_volunteer')->nullable(); $table->string('status')->default(OrganizationStatus::pending->value); diff --git a/database/migrations/2023_06_25_110506_create_articles_table.php b/database/migrations/2023_06_25_110506_create_articles_table.php index a38990a4..496e846c 100644 --- a/database/migrations/2023_06_25_110506_create_articles_table.php +++ b/database/migrations/2023_06_25_110506_create_articles_table.php @@ -18,11 +18,11 @@ public function up(): void Schema::create('articles', function (Blueprint $table) { $table->id(); $table->timestamps(); + $table->timestamp('published_at')->nullable(); $table->json('title'); $table->json('slug'); $table->json('content'); $table->json('author'); - $table->boolean('is_published')->default(true); $table->foreignIdFor(ArticleCategory::class)->nullable()->constrained()->cascadeOnDelete(); $table->foreignIdFor(Championship::class)->nullable()->constrained()->cascadeOnDelete(); diff --git a/docs/migrate.md b/docs/migrate.md new file mode 100644 index 00000000..f70b13ad --- /dev/null +++ b/docs/migrate.md @@ -0,0 +1,95 @@ +# Migrating data from the previous version + + +## Model mappings + +### Organization +Table: `dbo.ONGs` + +```ini +[Id] => id +[Name] => name +[LogoImageId] => media library -> logo collection +[Description] => description +[AnualReportFileId] => media library -> default collection +[Recommendations] => ? +[CIF] => cif +[Address] => address +[PhoneNb] => contact_phone +[Email] => contact_email +[ContactPerson] => contact_person +[WebSite] => website +[HasVolunteering] => accepts_volunteers +[WhyVolunteer] => why_volunteer +[ONGStatusId] => { + 1 Approval -> OrganizationStatus::pending + 2 Active -> OrganizationStatus::approved + 3 Inactive -> OrganizationStatus::rejected +} +[ONGApprovalStatusTypeId] => ? +[CreationDate] => created_at +[HasChanges] => ? +[MerchantId] => eu_platesc_merchant_id +[MerchantKey] => eu_platesc_private_key +[AllCounties] => X +[OrganizationalStatusId] => media library -> statute collection +[Tags] => x +[FacebookPageLink] => facebook +[DynamicUrl] => slug +``` + +### Activity Domain +Table: `lkp.ActivityDomains` + +```ini +[Id] => id +[Name] => name +slug([Name]) => slug +``` + +### User +```ini +[Id] => id +[FirstName] + [LastName] => name +[Email] => email +[Password] => old_password +[PasswordSalt] => old_salt +[CreationDate] => created_at +[ActivationCodeGeneratedDate] => email_verified_at +[RoleId] => { + 1 => UserRole::USER + 2 => UserRole::ADMIN => needs organization_id from `dbo.UserONGs` + 3 => UserRole::SUPERADMIN +} +``` + +### Project +Table: `dbo.ONGProjects` + `dbo.Projects` +```ini +[Id] => id +[ONGId] => organization_id +[Name] => name +[DynamicUrl] => slug +[Description] => description +[TargetAmmount] => target_budget +[StartDate] => start +[EndDate] => end +[HasVolunteering] => accepting_volunteers +[AcceptComments] => accepting_comments +[CreationDate] => created_at +[ProjectStatusTypeId] => status { + 1 => ProjectStatus::approved + 2 => ProjectStatus::approved + 3 => ProjectStatus::approved + 4 => ProjectStatus::rejected + default => ProjectStatus::draft +} +``` + +### Project Categories +Table: `lkp.ProjectCategories` +```ini +[Id] => id +[Name] => name +slug([Name]) => slug +``` diff --git a/lang/ro.json b/lang/ro.json index eea963bd..9f044591 100644 --- a/lang/ro.json +++ b/lang/ro.json @@ -177,7 +177,7 @@ "organization_contact_person_label": "Persoana de contact", "organization_email_label": "Email contact organizație (public)", "organization_address_label": "Adresă sediu", - "street_address_label": "Strada", + "address_label": "Strada", "payment_gateway_data": "Date EuPlătesc", "merchant_id": "Merchant ID", "key_label": "Key", @@ -385,7 +385,7 @@ "currency": "RON", "bcr_for_community": "BCR pentru comunitate", "articles": "Articole", - "project_period" : "Perioada proiect", + "project_period": "Perioada proiect", "categories": "Categorii", "other_articles_label": "Alte articole", "donate_to_a_project": "Donează către un proiect", @@ -452,24 +452,19 @@ "locales.ro": "Română", "locales.en": "English", - "projects.status.open" : "Deschis", - "projects.status.close" : "Închis", - "projects.status.starting_soon" : "Incepe in curand", - "projects.status.ending_soon" : "Se incheie in curand", + "projects.status.open": "Deschis", + "projects.status.close": "Închis", + "projects.status.starting_soon": "Incepe in curand", + "projects.status.ending_soon": "Se incheie in curand", "project.labels.preview_image": "Imagine de prezentare", - "project.labels.videos" : "Videoclipuri externe ale proiectului", - "project.labels.videos_extra" : "Aici puteti adauga link-uri de pe diverse platforme de streaming (youtube, vimeo, etc.)", + "project.labels.videos": "Videoclipuri externe ale proiectului", + "project.labels.videos_extra": "Aici puteti adauga link-uri de pe diverse platforme de streaming (youtube, vimeo, etc.)", "project.labels.external_links_title": "Titlu link extern", "project.labels.external_links_url": "URL link extern", "project.labels.change_gallery_label": "Schimbă galeria de imagini", - "organization_status_disabled" : "Organizația este dezactivată, vezi detalii în secțiunea de tichete", - "organization_status_pending" : "Organizația este în așteptare" - - - - - + "organization_status_disabled": "Organizația este dezactivată, vezi detalii în secțiunea de tichete", + "organization_status_pending": "Organizația este în așteptare" } diff --git a/lang/ro/organization.php b/lang/ro/organization.php index 43916850..0bfd6004 100644 --- a/lang/ro/organization.php +++ b/lang/ro/organization.php @@ -12,6 +12,8 @@ 'pending' => 'În așteptare', 'active' => 'Activă', 'disabled' => 'Inactivă', + 'approved' => 'Aprobată', + 'rejected' => 'Respinsă', ], 'actions' => [ 'view' => 'Vizualizează', @@ -58,7 +60,7 @@ 'email_contact_organization' => 'Email contact organizație (public)', 'phone_contact_organization' => 'Telefon contact organizație (public)', 'contact_person' => 'Persoană de contact', - 'street_address' => 'Adresă sediu', + 'address' => 'Adresă sediu', 'eu_platesc_data' => 'Date EuPlătesc', 'eu_platesc_merchant_id' => 'Merchant ID', 'eu_platesc_private_key' => 'Key', diff --git a/resources/js/Components/Footer.vue b/resources/js/Components/Footer.vue index 9d2f4f82..bf66ea07 100644 --- a/resources/js/Components/Footer.vue +++ b/resources/js/Components/Footer.vue @@ -80,7 +80,7 @@
  • {{ $t('organizations_link') }} @@ -88,7 +88,7 @@
  • {{ $t('projects_link') }} diff --git a/resources/js/Components/Navbar.vue b/resources/js/Components/Navbar.vue index 5a700c3c..99d8a113 100644 --- a/resources/js/Components/Navbar.vue +++ b/resources/js/Components/Navbar.vue @@ -233,12 +233,12 @@ { name: 'Proiecte', description: 'Descoperă proiectele înscrise la Bursa Binelui și susține proiectul pe care îl îndrăgești.', - href: route('projects'), + href: route('projects.index'), }, { name: 'Organizatii', description: 'Descoperă toate organizațiile înscrise pe Bursa Binelui și alege cauza pe care vrei să o susții.', - href: route('organizations'), + href: route('organizations.index'), }, { name: 'BCR pentru comunitate', diff --git a/resources/js/Components/cards/OrganizationCard.vue b/resources/js/Components/cards/OrganizationCard.vue index 62299f08..046e6d92 100644 --- a/resources/js/Components/cards/OrganizationCard.vue +++ b/resources/js/Components/cards/OrganizationCard.vue @@ -1,7 +1,7 @@