From afe741250f29cf9991a11a9fbf9fd7018d9ce02b Mon Sep 17 00:00:00 2001 From: bernardhanna Date: Mon, 28 Sep 2026 11:20:52 +0100 Subject: [PATCH] Hide and purge removed Profil Klett grant report links. The PDF files were deleted but stale database rows still rendered 404 tiles on the grassroots grants page. Co-authored-by: Cursor --- app/GrassrootsGrantsPage.php | 21 ++++++++ app/GrassrootsGrantsProjectImage.php | 41 ++++++++++++++ .../GrassrootsGrantsController.php | 5 +- .../Composers/GrassrootsGrantsComposer.php | 7 +-- ...ternal_profil_klett_grant_report_links.php | 23 ++++++++ database/seeders/GrassrootsGrantsSeeder.php | 11 +--- .../GrassrootsGrantsInternalReportsTest.php | 53 ++++++++++++++++++- 7 files changed, 140 insertions(+), 21 deletions(-) create mode 100644 database/migrations/2026_09_28_111800_purge_internal_profil_klett_grant_report_links.php diff --git a/app/GrassrootsGrantsPage.php b/app/GrassrootsGrantsPage.php index f8125906e..f5b671bea 100644 --- a/app/GrassrootsGrantsPage.php +++ b/app/GrassrootsGrantsPage.php @@ -37,6 +37,27 @@ public function activeHubs(): HasMany return $this->hubs()->where('active', true); } + public function withoutExcludedEvidence(): self + { + $this->loadMissing([ + 'activeHubs.activeProjects.links', + 'activeHubs.activeProjects.images', + ]); + + $this->activeHubs->each(function (GrassrootsGrantsHub $hub): void { + $hub->activeProjects->each(function (GrassrootsGrantsProject $project): void { + $project->setRelation( + 'images', + $project->images + ->reject(fn (GrassrootsGrantsProjectImage $image): bool => $image->isExcludedFromPublic()) + ->values() + ); + }); + }); + + return $this; + } + public static function config(): self { $page = self::first(); diff --git a/app/GrassrootsGrantsProjectImage.php b/app/GrassrootsGrantsProjectImage.php index dc5104275..b722ec704 100644 --- a/app/GrassrootsGrantsProjectImage.php +++ b/app/GrassrootsGrantsProjectImage.php @@ -9,6 +9,20 @@ class GrassrootsGrantsProjectImage extends Model { protected $table = 'grassroots_grants_project_images'; + /** + * Internal grant reports that must not appear on the public grassroots grants page. + * + * @var list + */ + public const EXCLUDED_PUBLIC_FILENAMES = [ + 'Pazin City Library_Final narrative report in English.pdf', + 'Article about workshops_My first code.pdf', + 'BETA_Evidence of conducted workshops.pdf', + 'BETA_Final narrative report in English.pdf', + 'BETA_Narrative report with evidence.pdf', + 'HROBOS_Final narrative report with evidence.pdf', + ]; + protected $fillable = [ 'project_id', 'url', @@ -61,4 +75,31 @@ public function isPdf(): bool { return $this->file_type === 'pdf' || str_ends_with(strtolower($this->url), '.pdf'); } + + public function isExcludedFromPublic(): bool + { + return self::urlIsExcludedFromPublic((string) $this->url); + } + + public static function urlIsExcludedFromPublic(string $url): bool + { + $path = parse_url($url, PHP_URL_PATH) ?: $url; + $filename = rawurldecode(basename($path)); + + return in_array($filename, self::EXCLUDED_PUBLIC_FILENAMES, true); + } + + public static function purgeExcludedFromPublic(): int + { + $ids = self::query() + ->pluck('url', 'id') + ->filter(fn ($url): bool => self::urlIsExcludedFromPublic((string) $url)) + ->keys(); + + if ($ids->isEmpty()) { + return 0; + } + + return self::query()->whereIn('id', $ids->all())->delete(); + } } diff --git a/app/Http/Controllers/GrassrootsGrantsController.php b/app/Http/Controllers/GrassrootsGrantsController.php index b10c1c464..b45154525 100644 --- a/app/Http/Controllers/GrassrootsGrantsController.php +++ b/app/Http/Controllers/GrassrootsGrantsController.php @@ -32,9 +32,6 @@ public function preview(): View private function loadPage(): GrassrootsGrantsPage { - return GrassrootsGrantsPage::config()->load([ - 'activeHubs.activeProjects.links', - 'activeHubs.activeProjects.images', - ]); + return GrassrootsGrantsPage::config()->withoutExcludedEvidence(); } } diff --git a/app/Http/View/Composers/GrassrootsGrantsComposer.php b/app/Http/View/Composers/GrassrootsGrantsComposer.php index 2c35b37a0..c4e3cb29b 100644 --- a/app/Http/View/Composers/GrassrootsGrantsComposer.php +++ b/app/Http/View/Composers/GrassrootsGrantsComposer.php @@ -16,11 +16,6 @@ public function compose(View $view): void return; } - $page = GrassrootsGrantsPage::config()->load([ - 'activeHubs.activeProjects.links', - 'activeHubs.activeProjects.images', - ]); - - $view->with('page', $page); + $view->with('page', GrassrootsGrantsPage::config()->withoutExcludedEvidence()); } } diff --git a/database/migrations/2026_09_28_111800_purge_internal_profil_klett_grant_report_links.php b/database/migrations/2026_09_28_111800_purge_internal_profil_klett_grant_report_links.php new file mode 100644 index 000000000..ceadefc4d --- /dev/null +++ b/database/migrations/2026_09_28_111800_purge_internal_profil_klett_grant_report_links.php @@ -0,0 +1,23 @@ + */ - public const EXCLUDED_EVIDENCE_FILES = [ - 'Pazin City Library_Final narrative report in English.pdf', - 'Article about workshops_My first code.pdf', - 'BETA_Evidence of conducted workshops.pdf', - 'BETA_Final narrative report in English.pdf', - 'BETA_Narrative report with evidence.pdf', - 'HROBOS_Final narrative report with evidence.pdf', - ]; + public const EXCLUDED_EVIDENCE_FILES = GrassrootsGrantsProjectImage::EXCLUDED_PUBLIC_FILENAMES; public function run(): void { @@ -109,7 +102,7 @@ private function seedProjectImages( $position = 0; foreach ($finder as $file) { - if (in_array($file->getFilename(), self::EXCLUDED_EVIDENCE_FILES, true)) { + if (in_array($file->getFilename(), GrassrootsGrantsProjectImage::EXCLUDED_PUBLIC_FILENAMES, true)) { continue; } diff --git a/tests/Feature/GrassrootsGrantsInternalReportsTest.php b/tests/Feature/GrassrootsGrantsInternalReportsTest.php index ad38994ff..dcf3323f0 100644 --- a/tests/Feature/GrassrootsGrantsInternalReportsTest.php +++ b/tests/Feature/GrassrootsGrantsInternalReportsTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature; +use App\GrassrootsGrantsPage; use App\GrassrootsGrantsProjectImage; use Database\Seeders\GrassrootsGrantsSeeder; use Illuminate\Foundation\Testing\DatabaseMigrations; @@ -19,16 +20,64 @@ public function internal_profil_klett_reports_are_not_published(): void $published = GrassrootsGrantsProjectImage::query()->pluck('url'); - foreach (GrassrootsGrantsSeeder::EXCLUDED_EVIDENCE_FILES as $filename) { + foreach (GrassrootsGrantsProjectImage::EXCLUDED_PUBLIC_FILENAMES as $filename) { $this->assertFileDoesNotExist($this->grantEvidencePath($filename)); $this->assertFalse( - $published->contains(fn (string $url): bool => rawurldecode(basename(parse_url($url, PHP_URL_PATH) ?: $url)) === $filename), + $published->contains(fn (string $url): bool => GrassrootsGrantsProjectImage::urlIsExcludedFromPublic($url) && rawurldecode(basename(parse_url($url, PHP_URL_PATH) ?: $url)) === $filename), $filename.' is still linked on the grassroots grants page.' ); } } + #[Test] + public function stale_internal_report_links_are_hidden_from_the_page(): void + { + $page = GrassrootsGrantsPage::create([ + 'is_preview_mode' => false, + 'hero_title' => 'Grassroots Grants', + 'round_title' => 'Round 1', + ]); + + $hub = $page->hubs()->create([ + 'title' => 'Croatia & Slovenia – Profil Klett', + 'hub_status' => 'active', + 'position' => 0, + 'active' => true, + ]); + + $project = $hub->projects()->create([ + 'title' => 'MY FIRST CODE: First steps into the world of coding with LEGO', + 'position' => 0, + 'active' => true, + ]); + + $staleUrl = '/images/grants/Croatia & Slovenia/MY FIRST CODE First steps into the world of coding with LEGO/Article about workshops_My first code.pdf'; + + $project->images()->create([ + 'url' => $staleUrl, + 'alt' => 'Internal report', + 'file_type' => 'pdf', + 'position' => 0, + ]); + + $project->images()->create([ + 'url' => '/images/grants/Croatia & Slovenia/MY FIRST CODE First steps into the world of coding with LEGO/City Library Pazin_Evidence of workshop.png', + 'alt' => 'Workshop evidence', + 'file_type' => 'image', + 'position' => 1, + ]); + + $html = $this->get(route('grassroots-grants'))->assertOk()->getContent(); + + $this->assertStringNotContainsString('Article about workshops_My first code.pdf', $html); + $this->assertStringNotContainsString('Article%20about%20workshops_My%20first%20code.pdf', $html); + $this->assertStringContainsString('City%20Library%20Pazin_Evidence%20of%20workshop.png', $html); + + $this->assertSame(1, GrassrootsGrantsProjectImage::purgeExcludedFromPublic()); + $this->assertSame(0, GrassrootsGrantsProjectImage::query()->where('url', $staleUrl)->count()); + } + private function grantEvidencePath(string $filename): string { $folders = [