mirror of
https://github.com/standardebooks/web.git
synced 2025-07-16 03:16:36 -04:00
Add new project form, and allow projects to be created when attempting to create a placeholder that already exists.
This commit is contained in:
parent
8e6b05a150
commit
d902074285
17 changed files with 217 additions and 29 deletions
|
@ -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
|
||||
|
|
1
config/apache/rewrites/projects.conf
Normal file
1
config/apache/rewrites/projects.conf
Normal file
|
@ -0,0 +1 @@
|
|||
RewriteRule ^/ebooks/([^\.]+?)/projects/new$ /projects/new.php?ebook-url-path=$1
|
|
@ -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/.+">
|
||||
|
|
|
@ -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/.+">
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -2,10 +2,15 @@
|
|||
/**
|
||||
* @var Ebook $ebook
|
||||
*/
|
||||
|
||||
$showAddButton = $showAddButton ?? false;
|
||||
?>
|
||||
<? if(sizeof($ebook->Projects) > 0){ ?>
|
||||
<section id="projects">
|
||||
<h2>Projects</h2>
|
||||
<?= Template::ProjectsTable(['projects' => $ebook->Projects, 'includeTitle' => false]) ?>
|
||||
</section>
|
||||
<? if($showAddButton){ ?>
|
||||
<a href="<?= $ebook->Url ?>/projects/new">New project</a>
|
||||
<? } ?>
|
||||
<? if(sizeof($ebook->Projects) > 0){ ?>
|
||||
<?= Template::ProjectsTable(['projects' => $ebook->Projects, 'includeTitle' => false]) ?>
|
||||
<? } ?>
|
||||
</section>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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]{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 = [];
|
||||
|
||||
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;
|
||||
|
|
|
@ -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
79
www/projects/new.php
Normal 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
45
www/projects/post.php
Normal 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');
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue