Add new project form, and allow projects to be created when attempting to create a placeholder that already exists.

This commit is contained in:
Alex Cabal 2024-12-16 21:27:45 -06:00
parent 8e6b05a150
commit d902074285
17 changed files with 217 additions and 29 deletions

View file

@ -53,4 +53,5 @@ RewriteRule ^/ebooks/jules-verne/twenty-thousand-leagues-under-the-seas/f-p-walt
# Prevent this rule from firing if we're getting a distribution file
RewriteCond %{REQUEST_FILENAME} !^/ebooks/.+?/downloads/.+$
RewriteCond %{REQUEST_FILENAME} !^/ebooks/.+?/text.*$
RewriteCond %{REQUEST_FILENAME} !^/ebooks/.+?/projects.*$
RewriteRule ^/ebooks/([^\.]+?)$ /ebooks/get.php?url-path=$1

View file

@ -0,0 +1 @@
RewriteRule ^/ebooks/([^\.]+?)/projects/new$ /projects/new.php?ebook-url-path=$1

View file

@ -217,6 +217,7 @@ Define conf_rewrite_root ${web_root}/config/apache/rewrites
Include ${conf_rewrite_root}/artworks.conf
Include ${conf_rewrite_root}/polls.conf
Include ${conf_rewrite_root}/users.conf
Include ${conf_rewrite_root}/projects.conf
# Specific config for /ebooks/<author>/<ebook>/downloads
<DirectoryMatch "^${web_root}/www/ebooks/.+">

View file

@ -199,6 +199,7 @@ Define conf_rewrite_root ${web_root}/config/apache/rewrites
Include ${conf_rewrite_root}/artworks.conf
Include ${conf_rewrite_root}/polls.conf
Include ${conf_rewrite_root}/users.conf
Include ${conf_rewrite_root}/projects.conf
# Specific config for /ebooks/<author>/<ebook>/downloads
<DirectoryMatch "^${web_root}/www/ebooks/.+">

View file

@ -191,7 +191,7 @@ class Project{
}
if(!isset($this->Started)){
$error->Add(new Exceptions\StartedTimestampRequiredException());
$this->Started = NOW;
}
if($error->HasExceptions){
@ -356,6 +356,7 @@ class Project{
}
public function FillFromHttpPost(): void{
$this->PropertyFromHttp('EbookId');
$this->PropertyFromHttp('ProducerName');
$this->PropertyFromHttp('ProducerEmail');
$this->PropertyFromHttp('DiscussionUrl');

View file

@ -70,7 +70,7 @@ $ebook = $ebook ?? new Ebook();
<label class="icon book">
<span>Title</span>
<input type="text" name="ebook-title" required="required"
value="<?= Formatter::EscapeHtml($ebook->Title ?? '') ?>"/>
value="<?= Formatter::EscapeHtml($ebook->Title ?? '') ?>" autocomplete="off"/>
</label>
<label class="icon year">
Year published
@ -79,6 +79,7 @@ $ebook = $ebook ?? new Ebook();
name="ebook-placeholder-year-published"
inputmode="numeric"
pattern="[0-9]{1,4}"
autocomplete="off"
value="<?= Formatter::EscapeHtml((string)($ebook->EbookPlaceholder?->YearPublished)) ?>"
/>
</label>
@ -116,6 +117,7 @@ $ebook = $ebook ?? new Ebook();
name="sequence-number-collection-name-1"
inputmode="numeric"
pattern="[0-9]{1,3}"
autocomplete="off"
value="<? if(isset($ebook->CollectionMemberships) && sizeof($ebook->CollectionMemberships) > 0){ ?><?= Formatter::EscapeHtml((string)$ebook->CollectionMemberships[0]->SequenceNumber) ?><? } ?>"
/>
</label>
@ -149,6 +151,7 @@ $ebook = $ebook ?? new Ebook();
name="sequence-number-collection-name-2"
inputmode="numeric"
pattern="[0-9]{1,3}"
autocomplete="off"
value="<? if(isset($ebook->CollectionMemberships) && sizeof($ebook->CollectionMemberships) > 1){ ?><?= Formatter::EscapeHtml((string)$ebook->CollectionMemberships[1]->SequenceNumber) ?><? } ?>"
/>
</label>
@ -180,6 +183,7 @@ $ebook = $ebook ?? new Ebook();
name="sequence-number-collection-name-3"
inputmode="numeric"
pattern="[0-9]{1,3}"
autocomplete="off"
value="<? if(isset($ebook->CollectionMemberships) && sizeof($ebook->CollectionMemberships) > 2){ ?><?= Formatter::EscapeHtml((string)$ebook->CollectionMemberships[2]->SequenceNumber) ?><? } ?>"
/>
</label>
@ -197,7 +201,7 @@ $ebook = $ebook ?? new Ebook();
/>
</label>
<fieldset class="project-form">
<?= Template::ProjectForm(['project' => $ebook->ProjectInProgress]) ?>
<?= Template::ProjectForm(['project' => $ebook->ProjectInProgress, 'areFieldsRequired' => false]) ?>
</fieldset>
</fieldset>
<fieldset>
@ -237,6 +241,7 @@ $ebook = $ebook ?? new Ebook();
<input
type="url"
name="ebook-placeholder-transcription-url"
autocomplete="off"
value="<?= Formatter::EscapeHtml($ebook->EbookPlaceholder?->TranscriptionUrl) ?>"
/>
</label>

View file

@ -2,10 +2,15 @@
/**
* @var Ebook $ebook
*/
$showAddButton = $showAddButton ?? false;
?>
<? if(sizeof($ebook->Projects) > 0){ ?>
<section id="projects">
<h2>Projects</h2>
<section id="projects">
<h2>Projects</h2>
<? if($showAddButton){ ?>
<a href="<?= $ebook->Url ?>/projects/new">New project</a>
<? } ?>
<? if(sizeof($ebook->Projects) > 0){ ?>
<?= Template::ProjectsTable(['projects' => $ebook->Projects, 'includeTitle' => false]) ?>
</section>
<? } ?>
<? } ?>
</section>

View file

@ -2,12 +2,18 @@
$project = $project ?? new Project();
$managers = User::GetAllByCanManageProjects();
$reviewers = User::GetAllByCanReviewProjects();
$areFieldsRequired = $areFieldsRequired ?? true;
?>
<input type="hidden" name="project-ebook-id" value="<?= $project->EbookId ?? '' ?>" />
<label class="icon user">
<span>Producer name</span>
<input
type="text"
name="project-producer-name"
<? if($areFieldsRequired){ ?>
required="required"
<? } ?>
value="<?= Formatter::EscapeHtml($project->ProducerName ?? '') ?>"
/>
</label>
@ -72,6 +78,7 @@ $reviewers = User::GetAllByCanReviewProjects();
name="project-vcs-url"
placeholder="https://github.com/..."
pattern="^https:\/\/github\.com\/[^\/]+/[^\/]+/?$"
autocomplete="off"
value="<?= Formatter::EscapeHtml($project->VcsUrl ?? '') ?>"
/>
</label>
@ -81,6 +88,7 @@ $reviewers = User::GetAllByCanReviewProjects();
<input
type="url"
name="project-discussion-url"
autocomplete="off"
value="<?= Formatter::EscapeHtml($project->DiscussionUrl) ?>"
/>
</label>

View file

@ -3326,6 +3326,13 @@ nav.breadcrumbs + h1{
margin-top: 0;
}
.extra-line > span:first-child::before{
content: 'Spacer';
visibility: hidden;
display: block;
font-weight: normal;
}
@media (hover: none) and (pointer: coarse){ /* target ipads and smartphones without a mouse */
/* For iPad, unset the height so it matches the other elements */
select[multiple]{

View file

@ -99,10 +99,3 @@ article.ebook.ebook-placeholder .placeholder-details{
article.ebook.ebook-placeholder .placeholder-details ul{
list-style: disc;
}
.extra-line > span:first-child::before{
content: 'Spacer';
visibility: hidden;
display: block;
font-weight: normal;
}

View file

@ -12,6 +12,20 @@
grid-column-start: 1;
}
.project-form{
display: grid;
gap: 2rem;
}
.project-form label:has(input[type="checkbox"]){
grid-column: 1 / span 2;
}
.project-form div.footer{
grid-column: 1 / span 2;
text-align: right;
}
table.data-table .status,
table.data-table .producer{
white-space: nowrap;

View file

@ -1,5 +1,4 @@
<?
use function Safe\preg_replace;
/** @var string $identifier Passed from script this is included from. */
@ -108,7 +107,7 @@ catch(Exceptions\EbookNotFoundException){
<? } ?>
<? if(Session::$User?->Benefits->CanEditProjects || Session::$User?->Benefits->CanManageProjects || Session::$User?->Benefits->CanReviewProjects){ ?>
<?= Template::EbookProjects(['ebook' => $ebook]) ?>
<?= Template::EbookProjects(['ebook' => $ebook, 'showAddButton' => Session::$User->Benefits->CanEditProjects && $ebook->ProjectInProgress === null]) ?>
<? } ?>
</article>
</main>

View file

@ -4,8 +4,10 @@ use function Safe\session_unset;
session_start();
$isCreated = HttpInput::Bool(SESSION, 'is-ebook-placeholder-created') ?? false;
$isOnlyProjectCreated = HttpInput::Bool(SESSION, 'is-only-ebook-project-created') ?? false;
$exception = HttpInput::SessionObject('exception', Exceptions\AppException::class);
$ebook = HttpInput::SessionObject('ebook', Ebook::class);
$project = HttpInput::SessionObject('project', Project::class);
try{
if(Session::$User === null){
@ -16,7 +18,7 @@ try{
throw new Exceptions\InvalidPermissionsException();
}
if($isCreated){
if($isCreated || $isOnlyProjectCreated){
// We got here because an `Ebook` was successfully created.
http_response_code(Enums\HttpCode::Created->value);
if($ebook !== null){
@ -70,8 +72,10 @@ catch(Exceptions\InvalidPermissionsException){
<?= Template::Error(['exception' => $exception]) ?>
<? if($isCreated && isset($createdEbook)){ ?>
<p class="message success">Ebook Placeholder created: <a href="<?= $createdEbook->Url ?>"><?= Formatter::EscapeHtml($createdEbook->Title) ?></a>!</p>
<? if($isOnlyProjectCreated){ ?>
<p class="message success">An ebook placeholder <a href="<?= $createdEbook->Url ?>">already exists</a> for this ebook, but a a new project was created!</p>
<? }elseif($isCreated && isset($createdEbook)){ ?>
<p class="message success">Ebook placeholder created: <a href="<?= $createdEbook->Url ?>"><?= Formatter::EscapeHtml($createdEbook->Title) ?></a>!</p>
<? } ?>
<form class="create-update-ebook-placeholder" method="<?= Enums\HttpMethod::Post->value ?>" action="/ebook-placeholders" autocomplete="off">

View file

@ -85,26 +85,34 @@ try{
}
$ebook->FillIdentifierFromTitleAndContributors();
try{
$existingEbook = Ebook::GetByIdentifier($ebook->Identifier);
throw new Exceptions\DuplicateEbookException($ebook->Identifier);
}
catch(Exceptions\EbookNotFoundException){
// Pass and create the placeholder. There is no existing ebook with this identifier.
}
// These properties must be set before calling `Ebook::Create()` to prevent the getters from triggering DB queries or accessing `Ebook::$EbookId` before it is set.
$ebook->Tags = [];
$ebook->LocSubjects = [];
$ebook->Illustrators = [];
$ebook->Contributors = [];
$ebook->Create();
try{
$ebook->Create();
}
catch(Exceptions\DuplicateEbookException $ex){
// If the identifier already exists but a `Project` was sent with this request, create the `Project` anyway.
$existingEbook = Ebook::GetByIdentifier($ebook->Identifier);
if($ebookPlaceholder->IsInProgress && $project !== null){
$ebook->EbookId = $existingEbook->EbookId;
$_SESSION['is-only-ebook-project-created'] = true;
}
else{
// No `Project`, throw the exception and really fail.
$ebook = $existingEbook;
throw $ex;
}
}
if($ebookPlaceholder->IsInProgress && $project !== null){
$project->EbookId = $ebook->EbookId;
$project->Ebook = $ebook;
$project->Create();
$ebook->ProjectInProgress = $project;
}
$_SESSION['ebook'] = $ebook;

View file

@ -1,4 +1,5 @@
<?
try{
if(Session::$User === null){
throw new Exceptions\LoginRequiredException();
@ -14,6 +15,16 @@ try{
throw new Exceptions\InvalidPermissionsException();
}
session_start();
$isCreated = HttpInput::Bool(SESSION, 'is-project-created') ?? false;
if($isCreated){
// We got here because a `Project` was successfully submitted.
http_response_code(Enums\HttpCode::Created->value);
session_unset();
}
$inProgressProjects = Project::GetAllByStatus(Enums\ProjectStatusType::InProgress);
$stalledProjects = Project::GetAllByStatus(Enums\ProjectStatusType::Stalled);
}
@ -31,6 +42,11 @@ catch(Exceptions\InvalidPermissionsException){
<main>
<section>
<h1>Projects</h1>
<? if($isCreated){ ?>
<p class="message success">Project created!</p>
<? } ?>
<section id="active">
<h2>Active projects</h2>
<? if(sizeof($inProgressProjects) == 0){ ?>

79
www/projects/new.php Normal file
View file

@ -0,0 +1,79 @@
<?
/**
* GET /ebooks/<ebook-identifier>/projects/new
* GET /projects/new
*/
use function Safe\session_unset;
session_start();
$urlPath = HttpInput::Str(GET, 'ebook-url-path');
$exception = HttpInput::SessionObject('exception', Exceptions\AppException::class);
$project = HttpInput::SessionObject('project', Project::class);
$ebook = null;
try{
if($urlPath !== null){
// Check this first so we can output a 404 immediately if it's not found.
$identifier = EBOOKS_IDENTIFIER_PREFIX . trim(str_replace('.', '', $urlPath), '/'); // Contains the portion of the URL (without query string) that comes after `https://standardebooks.org/ebooks/`.
$ebook = Ebook::GetByIdentifier($identifier);
}
if(Session::$User === null){
throw new Exceptions\LoginRequiredException();
}
if(!Session::$User->Benefits->CanEditProjects){
throw new Exceptions\InvalidPermissionsException();
}
if($exception){
// We got here because a `Project` submission had errors and the user has to try again.
http_response_code(Enums\HttpCode::UnprocessableContent->value);
session_unset();
}
if($project === null){
$project = new Project();
if($ebook !== null){
$project->Ebook = $ebook;
$project->EbookId = $ebook->EbookId;
}
}
}
catch(Exceptions\EbookNotFoundException){
Template::Emit404();
}
catch(Exceptions\LoginRequiredException){
Template::RedirectToLogin();
}
catch(Exceptions\InvalidPermissionsException){
Template::Emit403();
}
?><?= Template::Header([
'title' => 'New Project',
'css' => ['/css/project.css'],
'description' => 'Add a new ebook project.'
]) ?>
<main>
<section class="narrow">
<? if(isset($project->Ebook)){ ?>
<nav class="breadcrumbs">
<a href="<?= $project->Ebook->AuthorsUrl ?>"><?= $project->Ebook->AuthorsHtml ?></a> → <a href="<?= $project->Ebook->Url ?>"><?= Formatter::EscapeHtml($project->Ebook->Title) ?></a> →
</nav>
<? } ?>
<h1>New Project</h1>
<?= Template::Error(['exception' => $exception]) ?>
<form action="/projects" method="<?= Enums\HttpMethod::Post->value ?>" class="project-form">
<?= Template::ProjectForm(['project' => $project, 'areFieldsRequired' => true]) ?>
<div class="footer">
<button>Submit</button>
</div>
</form>
</section>
</main>
<?= Template::Footer() ?>

45
www/projects/post.php Normal file
View file

@ -0,0 +1,45 @@
<?
try{
session_start();
$httpMethod = HttpInput::ValidateRequestMethod([Enums\HttpMethod::Post, Enums\HttpMethod::Patch, Enums\HttpMethod::Put]);
if(Session::$User === null){
throw new Exceptions\LoginRequiredException();
}
if(!Session::$User->Benefits->CanEditProjects){
throw new Exceptions\InvalidPermissionsException();
}
// POSTing a new `Project`.
if($httpMethod == Enums\HttpMethod::Post){
$project = new Project();
$project->FillFromHttpPost();
$project->Create();
$_SESSION['project'] = $project;
$_SESSION['is-project-created'] = true;
http_response_code(Enums\HttpCode::SeeOther->value);
header('Location: /projects');
}
}
catch(Exceptions\EbookNotFoundException){
Template::Emit404();
}
catch(Exceptions\LoginRequiredException){
Template::RedirectToLogin();
}
catch(Exceptions\InvalidPermissionsException){
Template::Emit403();
}
catch(Exceptions\InvalidProjectException | Exceptions\ProjectExistsException | Exceptions\EbookIsNotAPlaceholderException $ex){
$_SESSION['project'] = $project;
$_SESSION['exception'] = $ex;
http_response_code(Enums\HttpCode::SeeOther->value);
header('Location: /projects/new');
}