From a91509726cc1518643ac973a3b01539779d26680 Mon Sep 17 00:00:00 2001 From: Mike Colagrosso Date: Thu, 6 Mar 2025 13:28:26 -0700 Subject: [PATCH] Add admin tool to handle duplicate artist names (#480) --- config/apache/rewrites/artists.conf | 4 + config/apache/standardebooks.test.conf | 1 + lib/Artist.php | 82 +++++++++++++++++++ .../ArtistAlternateNameExistsException.php | 7 ++ lib/Exceptions/ArtistHasArtworkException.php | 7 ++ www/artists/delete.php | 82 +++++++++++++++++++ www/artists/get.php | 28 ++++++- www/artists/post.php | 51 ++++++++++++ www/css/artwork.css | 1 + 9 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 config/apache/rewrites/artists.conf create mode 100644 lib/Exceptions/ArtistAlternateNameExistsException.php create mode 100644 lib/Exceptions/ArtistHasArtworkException.php create mode 100644 www/artists/delete.php create mode 100644 www/artists/post.php diff --git a/config/apache/rewrites/artists.conf b/config/apache/rewrites/artists.conf new file mode 100644 index 00000000..2391b6b8 --- /dev/null +++ b/config/apache/rewrites/artists.conf @@ -0,0 +1,4 @@ +RewriteRule ^/artists/(.+?)/delete$ /artists/delete.php?artist-url-name=$1 [L] + +RewriteCond expr "tolower(%{REQUEST_METHOD}) =~ /^post$/" +RewriteRule ^/artists/([^/\.]+)$ /artists/post.php?artist-url-name=$1 [L] diff --git a/config/apache/standardebooks.test.conf b/config/apache/standardebooks.test.conf index 13c1cc89..1225ca20 100644 --- a/config/apache/standardebooks.test.conf +++ b/config/apache/standardebooks.test.conf @@ -202,6 +202,7 @@ Define conf_rewrite_root ${web_root}/config/apache/rewrites Include ${conf_rewrite_root}/feeds.conf Include ${conf_rewrite_root}/ebooks.conf Include ${conf_rewrite_root}/newsletters.conf + Include ${conf_rewrite_root}/artists.conf Include ${conf_rewrite_root}/artworks.conf Include ${conf_rewrite_root}/polls.conf Include ${conf_rewrite_root}/users.conf diff --git a/lib/Artist.php b/lib/Artist.php index 2a97d2ca..4ddc1f82 100644 --- a/lib/Artist.php +++ b/lib/Artist.php @@ -5,6 +5,7 @@ use Safe\DateTimeImmutable; * @property ?int $DeathYear * @property ?string $UrlName * @property ?string $Url + * @property string $DeleteUrl * @property ?array $AlternateNames */ class Artist{ @@ -19,6 +20,7 @@ class Artist{ protected string $_UrlName; protected string $_Url; + protected string $_DeleteUrl; /** @var array $_AlternateNames */ protected array $_AlternateNames; @@ -44,6 +46,10 @@ class Artist{ return $this->_Url ??= '/artworks/' . $this->UrlName; } + protected function GetDeleteUrl(): string{ + return $this->_DeleteUrl ??= '/artists/' . $this->UrlName . '/delete'; + } + /** * @return array */ @@ -134,6 +140,38 @@ class Artist{ order by Name asc', [], Artist::class); } + /** + * @throws Exceptions\ArtistNotFoundException + */ + public static function GetByName(?string $name): Artist{ + if($name === null){ + throw new Exceptions\ArtistNotFoundException(); + } + + return Db::Query(' + SELECT a.* + from Artists a + left outer join ArtistAlternateNames aan using (ArtistId) + where a.Name = ? + or aan.Name = ? + ', [$name, $name], Artist::class)[0] ?? throw new Exceptions\ArtistNotFoundException(); + } + + /** + * @throws Exceptions\ArtistNotFoundException + */ + public static function GetByUrlName(?string $urlName): Artist{ + if($urlName === null){ + throw new Exceptions\ArtistNotFoundException(); + } + + return Db::Query(' + SELECT * + from Artists + where UrlName = ? + ', [$urlName], Artist::class)[0] ?? throw new Exceptions\ArtistNotFoundException(); + } + /** * @throws Exceptions\ArtistNotFoundException */ @@ -151,6 +189,36 @@ class Artist{ ', [$urlName], Artist::class)[0] ?? throw new Exceptions\ArtistNotFoundException(); } + /** + * @throws Exceptions\ArtistAlternateNameExistsException + */ + public function AddAlternateName(string $name): void{ + try{ + Db::Query(' + INSERT into ArtistAlternateNames (ArtistId, Name, UrlName) + values (?, + ?, + ?) + ', [$this->ArtistId, $name, Formatter::MakeUrlSafe($name)]); + } + catch(Exceptions\DuplicateDatabaseKeyException){ + throw new Exceptions\ArtistAlternateNameExistsException(); + } + } + + /** + * Reassigns all the artworks currently assigned to this artist to the given canoncial artist. + * + * @param Artist $canonicalArtist + */ + public function ReassignArtworkTo(Artist $canonicalArtist): void{ + Db::Query(' + UPDATE Artworks + set ArtistId = ? + where ArtistId = ? + ', [$canonicalArtist->ArtistId, $this->ArtistId]); + } + /** * @throws Exceptions\InvalidArtistException */ @@ -188,7 +256,21 @@ class Artist{ } } + /** + * @throws Exceptions\ArtistHasArtworkException + */ public function Delete(): void{ + $hasArtwork = Db::QueryBool(' + SELECT exists ( + SELECT ArtworkId + from Artworks + where ArtistId = ? + )', [$this->ArtistId]); + + if($hasArtwork){ + throw new Exceptions\ArtistHasArtworkException(); + } + Db::Query(' DELETE from Artists diff --git a/lib/Exceptions/ArtistAlternateNameExistsException.php b/lib/Exceptions/ArtistAlternateNameExistsException.php new file mode 100644 index 00000000..54ea9c0e --- /dev/null +++ b/lib/Exceptions/ArtistAlternateNameExistsException.php @@ -0,0 +1,7 @@ +Benefits->CanReviewOwnArtwork){ + throw new Exceptions\InvalidPermissionsException(); + } + + session_start(); + + $exception = HttpInput::SessionObject('exception', Exceptions\AppException::class); + + $artist = Artist::GetByUrlName(HttpInput::Str(GET, 'artist-url-name')); + + if($exception){ + http_response_code(Enums\HttpCode::UnprocessableContent->value); + session_unset(); + } +} +catch(Exceptions\ArtistNotFoundException){ + Template::ExitWithCode(Enums\HttpCode::NotFound); +} +catch(Exceptions\LoginRequiredException){ + Template::RedirectToLogin(); +} +catch(Exceptions\InvalidPermissionsException){ + Template::ExitWithCode(Enums\HttpCode::Forbidden); +} +?>Name, css: ['/css/artwork.css']) ?> +
+
+ +

Delete

+ + + +
+ +

Are you sure you want to permanently delete Name) ?>?

+ + + + + +
+
+
+ diff --git a/www/artists/get.php b/www/artists/get.php index d1c47309..aefff9c3 100644 --- a/www/artists/get.php +++ b/www/artists/get.php @@ -1,5 +1,8 @@ Benefits->CanReviewArtwork ?? false; +$isAdminView = Session::$User?->Benefits->CanReviewOwnArtwork ?? false; $submitterUserId = Session::$User?->Benefits->CanUploadArtwork ? Session::$User->UserId : null; $isSubmitterView = !$isReviewerView && $submitterUserId !== null; @@ -13,24 +16,45 @@ if($isSubmitterView){ $artworkFilterType = Enums\ArtworkFilterType::ApprovedSubmitter; } +session_start(); + +$isArtistDeleted = HttpInput::Bool(SESSION, 'is-artist-deleted') ?? false; +$deletedArtist = HttpInput::SessionObject('deleted-artist', Artist::class); +$isAlternateNameAdded = HttpInput::Bool(SESSION, 'is-alternate-name-added') ?? false; + try{ $artworks = Artwork::GetAllByArtist(HttpInput::Str(GET, 'artist-url-name'), $artworkFilterType, $submitterUserId); if(sizeof($artworks) == 0){ throw new Exceptions\ArtistNotFoundException(); } + + $artist = $artworks[0]->Artist; + + if($isArtistDeleted){ + session_unset(); + } } catch(Exceptions\ArtistNotFoundException){ Template::ExitWithCode(Enums\HttpCode::NotFound); } -?>Artist->Name, css: ['/css/artwork.css']) ?> +?>Name, css: ['/css/artwork.css']) ?>
-

Artwork by Artist->Name) ?>

+

Artwork by Name) ?>

+ + +

Artist deleted: Name ?>. An alternate name (A.K.A.) was added.

+ + + +

Admin

+

Delete artist and reassign artwork

+
diff --git a/www/artists/post.php b/www/artists/post.php new file mode 100644 index 00000000..dd223996 --- /dev/null +++ b/www/artists/post.php @@ -0,0 +1,51 @@ +Benefits->CanReviewOwnArtwork){ + throw new Exceptions\InvalidPermissionsException(); + } + + // DELETE an `Artist`. + if($httpMethod == Enums\HttpMethod::Delete){ + $artistToDelete = Artist::GetByUrlName(HttpInput::Str(GET, 'artist-url-name') ?? ''); + $exceptionRedirectUrl = $artistToDelete->DeleteUrl; + + $canonicalArtist = Artist::GetByName(HttpInput::Str(POST, 'canonical-artist-name') ?? ''); + $addAlternateName = HttpInput::Bool(POST, 'add-alternate-name'); + + if($addAlternateName){ + $canonicalArtist->AddAlternateName($artistToDelete->Name); + } + + $artistToDelete->ReassignArtworkTo($canonicalArtist); + $artistToDelete->Delete(); + + $_SESSION['is-artist-deleted'] = true; + $_SESSION['deleted-artist'] = $artistToDelete; + if($addAlternateName){ + $_SESSION['is-alternate-name-added'] = true; + } + + http_response_code(Enums\HttpCode::SeeOther->value); + header('Location: ' . $canonicalArtist->Url); + } +} +catch(Exceptions\LoginRequiredException){ + Template::RedirectToLogin(); +} +catch(Exceptions\InvalidPermissionsException | Exceptions\HttpMethodNotAllowedException){ + Template::ExitWithCode(Enums\HttpCode::Forbidden); +} +catch(Exceptions\ArtistNotFoundException | Exceptions\ArtistHasArtworkException | Exceptions\ArtistAlternateNameExistsException $ex){ + $_SESSION['exception'] = $ex; + + http_response_code(Enums\HttpCode::SeeOther->value); + header('Location: ' . $exceptionRedirectUrl); +} diff --git a/www/css/artwork.css b/www/css/artwork.css index bc09cc4f..940e8273 100644 --- a/www/css/artwork.css +++ b/www/css/artwork.css @@ -166,6 +166,7 @@ form[action^="/artworks/"]{ grid-column: 1 / span 4; } +.user + label:has(input[type="checkbox"]), .year + label:has(input[type="checkbox"]){ margin-top: .5rem; }