mirror of
https://github.com/standardebooks/web.git
synced 2025-07-13 01:52:02 -04:00
Add admin tool to handle duplicate artist names (#480)
This commit is contained in:
parent
576fcea0b1
commit
a91509726c
9 changed files with 261 additions and 2 deletions
4
config/apache/rewrites/artists.conf
Normal file
4
config/apache/rewrites/artists.conf
Normal file
|
@ -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]
|
|
@ -202,6 +202,7 @@ Define conf_rewrite_root ${web_root}/config/apache/rewrites
|
||||||
Include ${conf_rewrite_root}/feeds.conf
|
Include ${conf_rewrite_root}/feeds.conf
|
||||||
Include ${conf_rewrite_root}/ebooks.conf
|
Include ${conf_rewrite_root}/ebooks.conf
|
||||||
Include ${conf_rewrite_root}/newsletters.conf
|
Include ${conf_rewrite_root}/newsletters.conf
|
||||||
|
Include ${conf_rewrite_root}/artists.conf
|
||||||
Include ${conf_rewrite_root}/artworks.conf
|
Include ${conf_rewrite_root}/artworks.conf
|
||||||
Include ${conf_rewrite_root}/polls.conf
|
Include ${conf_rewrite_root}/polls.conf
|
||||||
Include ${conf_rewrite_root}/users.conf
|
Include ${conf_rewrite_root}/users.conf
|
||||||
|
|
|
@ -5,6 +5,7 @@ use Safe\DateTimeImmutable;
|
||||||
* @property ?int $DeathYear
|
* @property ?int $DeathYear
|
||||||
* @property ?string $UrlName
|
* @property ?string $UrlName
|
||||||
* @property ?string $Url
|
* @property ?string $Url
|
||||||
|
* @property string $DeleteUrl
|
||||||
* @property ?array<string> $AlternateNames
|
* @property ?array<string> $AlternateNames
|
||||||
*/
|
*/
|
||||||
class Artist{
|
class Artist{
|
||||||
|
@ -19,6 +20,7 @@ class Artist{
|
||||||
|
|
||||||
protected string $_UrlName;
|
protected string $_UrlName;
|
||||||
protected string $_Url;
|
protected string $_Url;
|
||||||
|
protected string $_DeleteUrl;
|
||||||
/** @var array<string> $_AlternateNames */
|
/** @var array<string> $_AlternateNames */
|
||||||
protected array $_AlternateNames;
|
protected array $_AlternateNames;
|
||||||
|
|
||||||
|
@ -44,6 +46,10 @@ class Artist{
|
||||||
return $this->_Url ??= '/artworks/' . $this->UrlName;
|
return $this->_Url ??= '/artworks/' . $this->UrlName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function GetDeleteUrl(): string{
|
||||||
|
return $this->_DeleteUrl ??= '/artists/' . $this->UrlName . '/delete';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string>
|
* @return array<string>
|
||||||
*/
|
*/
|
||||||
|
@ -134,6 +140,38 @@ class Artist{
|
||||||
order by Name asc', [], Artist::class);
|
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
|
* @throws Exceptions\ArtistNotFoundException
|
||||||
*/
|
*/
|
||||||
|
@ -151,6 +189,36 @@ class Artist{
|
||||||
', [$urlName], Artist::class)[0] ?? throw new Exceptions\ArtistNotFoundException();
|
', [$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
|
* @throws Exceptions\InvalidArtistException
|
||||||
*/
|
*/
|
||||||
|
@ -188,7 +256,21 @@ class Artist{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exceptions\ArtistHasArtworkException
|
||||||
|
*/
|
||||||
public function Delete(): void{
|
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('
|
Db::Query('
|
||||||
DELETE
|
DELETE
|
||||||
from Artists
|
from Artists
|
||||||
|
|
7
lib/Exceptions/ArtistAlternateNameExistsException.php
Normal file
7
lib/Exceptions/ArtistAlternateNameExistsException.php
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class ArtistAlternateNameExistsException extends AppException{
|
||||||
|
/** @var string $message */
|
||||||
|
protected $message = 'Artist already has that alternate name (A.K.A.).';
|
||||||
|
}
|
7
lib/Exceptions/ArtistHasArtworkException.php
Normal file
7
lib/Exceptions/ArtistHasArtworkException.php
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class ArtistHasArtworkException extends AppException{
|
||||||
|
/** @var string $message */
|
||||||
|
protected $message = 'Artist has associated artwork and cannot be deleted.';
|
||||||
|
}
|
82
www/artists/delete.php
Normal file
82
www/artists/delete.php
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
<?
|
||||||
|
use function Safe\session_unset;
|
||||||
|
|
||||||
|
$artist = null;
|
||||||
|
|
||||||
|
try{
|
||||||
|
if(Session::$User === null){
|
||||||
|
throw new Exceptions\LoginRequiredException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!Session::$User->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);
|
||||||
|
}
|
||||||
|
?><?= Template::Header(title: 'Delete ' . $artist->Name, css: ['/css/artwork.css']) ?>
|
||||||
|
<main>
|
||||||
|
<section class="narrow">
|
||||||
|
<nav class="breadcrumbs">
|
||||||
|
<a href="<?= $artist->Url ?>"><?= $artist->Name ?></a> →
|
||||||
|
</nav>
|
||||||
|
<h1>Delete</h1>
|
||||||
|
|
||||||
|
<?= Template::Error(exception: $exception) ?>
|
||||||
|
|
||||||
|
<form method="<?= Enums\HttpMethod::Post->value ?>" action="/artists/<?= $artist->UrlName ?>">
|
||||||
|
<input type="hidden" name="_method" value="<?= Enums\HttpMethod::Delete->value ?>" />
|
||||||
|
<p>Are you sure you want to permanently delete <?= Formatter::EscapeHtml($artist->Name) ?>?</p>
|
||||||
|
<label class="icon user">
|
||||||
|
<span>Canonical Artist</span>
|
||||||
|
<span>Reassign artwork by <?= Formatter::EscapeHtml($artist->Name) ?> to this artist.</span>
|
||||||
|
<datalist id="artist-names-except-this-artist">
|
||||||
|
<? foreach(Artist::GetAll() as $a){ ?>
|
||||||
|
<? if($a->ArtistId != $artist->ArtistId){ ?>
|
||||||
|
<option value="<?= Formatter::EscapeHtml($a->Name) ?>"><?= Formatter::EscapeHtml($a->Name) ?>, d. <? if($a->DeathYear !== null){ ?><?= $a->DeathYear ?><? }else{ ?>unknown<? } ?></option>
|
||||||
|
<? foreach(($a->AlternateNames ?? []) as $alternateName){ ?>
|
||||||
|
<option value="<?= Formatter::EscapeHtml($alternateName) ?>"><?= Formatter::EscapeHtml($alternateName) ?>, d. <? if($a->DeathYear !== null){ ?><?= Formatter::EscapeHtml((string)$a->DeathYear) ?><? }else{ ?>unknown<? } ?></option>
|
||||||
|
<? } ?>
|
||||||
|
<? } ?>
|
||||||
|
<? } ?>
|
||||||
|
</datalist>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="canonical-artist-name"
|
||||||
|
list="artist-names-except-this-artist"
|
||||||
|
required="required"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="add-alternate-name" />
|
||||||
|
<span>Add <?= Formatter::EscapeHtml($artist->Name) ?> as an alternate name (A.K.A.) to the canonical artist</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<button class="delete">Delete and Reassign</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<?= Template::Footer() ?>
|
|
@ -1,5 +1,8 @@
|
||||||
<?
|
<?
|
||||||
|
use function Safe\session_unset;
|
||||||
|
|
||||||
$isReviewerView = Session::$User?->Benefits->CanReviewArtwork ?? false;
|
$isReviewerView = Session::$User?->Benefits->CanReviewArtwork ?? false;
|
||||||
|
$isAdminView = Session::$User?->Benefits->CanReviewOwnArtwork ?? false;
|
||||||
$submitterUserId = Session::$User?->Benefits->CanUploadArtwork ? Session::$User->UserId : null;
|
$submitterUserId = Session::$User?->Benefits->CanUploadArtwork ? Session::$User->UserId : null;
|
||||||
$isSubmitterView = !$isReviewerView && $submitterUserId !== null;
|
$isSubmitterView = !$isReviewerView && $submitterUserId !== null;
|
||||||
|
|
||||||
|
@ -13,24 +16,45 @@ if($isSubmitterView){
|
||||||
$artworkFilterType = Enums\ArtworkFilterType::ApprovedSubmitter;
|
$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{
|
try{
|
||||||
$artworks = Artwork::GetAllByArtist(HttpInput::Str(GET, 'artist-url-name'), $artworkFilterType, $submitterUserId);
|
$artworks = Artwork::GetAllByArtist(HttpInput::Str(GET, 'artist-url-name'), $artworkFilterType, $submitterUserId);
|
||||||
|
|
||||||
if(sizeof($artworks) == 0){
|
if(sizeof($artworks) == 0){
|
||||||
throw new Exceptions\ArtistNotFoundException();
|
throw new Exceptions\ArtistNotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$artist = $artworks[0]->Artist;
|
||||||
|
|
||||||
|
if($isArtistDeleted){
|
||||||
|
session_unset();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch(Exceptions\ArtistNotFoundException){
|
catch(Exceptions\ArtistNotFoundException){
|
||||||
Template::ExitWithCode(Enums\HttpCode::NotFound);
|
Template::ExitWithCode(Enums\HttpCode::NotFound);
|
||||||
}
|
}
|
||||||
?><?= Template::Header(title: 'Artwork by ' . $artworks[0]->Artist->Name, css: ['/css/artwork.css']) ?>
|
?><?= Template::Header(title: 'Artwork by ' . $artist->Name, css: ['/css/artwork.css']) ?>
|
||||||
<main class="artworks">
|
<main class="artworks">
|
||||||
<section class="narrow">
|
<section class="narrow">
|
||||||
<h1>Artwork by <?= Formatter::EscapeHtml($artworks[0]->Artist->Name) ?></h1>
|
<h1>Artwork by <?= Formatter::EscapeHtml($artist->Name) ?></h1>
|
||||||
|
|
||||||
|
<? if($isArtistDeleted && $deletedArtist !== null){ ?>
|
||||||
|
<p class="message success">Artist deleted: <?= $deletedArtist->Name ?><? if($isAlternateNameAdded){ ?>. An alternate name (A.K.A.) was added.<? } ?></p>
|
||||||
|
<? } ?>
|
||||||
|
|
||||||
<?= Template::ImageCopyrightNotice() ?>
|
<?= Template::ImageCopyrightNotice() ?>
|
||||||
|
|
||||||
<?= Template::ArtworkList(artworks: $artworks) ?>
|
<?= Template::ArtworkList(artworks: $artworks) ?>
|
||||||
|
|
||||||
|
<? if($isAdminView){ ?>
|
||||||
|
<h2>Admin</h2>
|
||||||
|
<p><a href="<?= $artist->DeleteUrl ?>">Delete artist and reassign artwork</a></p>
|
||||||
|
<? } ?>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<?= Template::Footer() ?>
|
<?= Template::Footer() ?>
|
||||||
|
|
51
www/artists/post.php
Normal file
51
www/artists/post.php
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?
|
||||||
|
try{
|
||||||
|
session_start();
|
||||||
|
$httpMethod = HttpInput::ValidateRequestMethod([Enums\HttpMethod::Delete]);
|
||||||
|
$exceptionRedirectUrl = '/artworks';
|
||||||
|
|
||||||
|
if(Session::$User === null){
|
||||||
|
throw new Exceptions\LoginRequiredException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!Session::$User->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);
|
||||||
|
}
|
|
@ -166,6 +166,7 @@ form[action^="/artworks/"]{
|
||||||
grid-column: 1 / span 4;
|
grid-column: 1 / span 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user + label:has(input[type="checkbox"]),
|
||||||
.year + label:has(input[type="checkbox"]){
|
.year + label:has(input[type="checkbox"]){
|
||||||
margin-top: .5rem;
|
margin-top: .5rem;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue