From d902074285392e5d1a0be727be17712280260e00 Mon Sep 17 00:00:00 2001 From: Alex Cabal Date: Mon, 16 Dec 2024 21:27:45 -0600 Subject: [PATCH] Add new project form, and allow projects to be created when attempting to create a placeholder that already exists. --- config/apache/rewrites/ebooks.conf | 1 + config/apache/rewrites/projects.conf | 1 + config/apache/standardebooks.org.conf | 1 + config/apache/standardebooks.test.conf | 1 + lib/Project.php | 3 +- templates/EbookPlaceholderForm.php | 9 ++- templates/EbookProjects.php | 15 +++-- templates/ProjectForm.php | 8 +++ www/css/core.css | 7 +++ www/css/ebook-placeholder.css | 7 --- www/css/project.css | 14 +++++ www/ebook-placeholders/get.php | 3 +- www/ebook-placeholders/new.php | 10 +++- www/ebook-placeholders/post.php | 26 ++++++--- www/projects/index.php | 16 ++++++ www/projects/new.php | 79 ++++++++++++++++++++++++++ www/projects/post.php | 45 +++++++++++++++ 17 files changed, 217 insertions(+), 29 deletions(-) create mode 100644 config/apache/rewrites/projects.conf create mode 100644 www/projects/new.php create mode 100644 www/projects/post.php diff --git a/config/apache/rewrites/ebooks.conf b/config/apache/rewrites/ebooks.conf index 8d5b1edb..de4d34bd 100644 --- a/config/apache/rewrites/ebooks.conf +++ b/config/apache/rewrites/ebooks.conf @@ -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 diff --git a/config/apache/rewrites/projects.conf b/config/apache/rewrites/projects.conf new file mode 100644 index 00000000..ddbcbbf0 --- /dev/null +++ b/config/apache/rewrites/projects.conf @@ -0,0 +1 @@ +RewriteRule ^/ebooks/([^\.]+?)/projects/new$ /projects/new.php?ebook-url-path=$1 diff --git a/config/apache/standardebooks.org.conf b/config/apache/standardebooks.org.conf index 307aeb65..d4d6fee4 100644 --- a/config/apache/standardebooks.org.conf +++ b/config/apache/standardebooks.org.conf @@ -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///downloads diff --git a/config/apache/standardebooks.test.conf b/config/apache/standardebooks.test.conf index 3464d940..b2a90159 100644 --- a/config/apache/standardebooks.test.conf +++ b/config/apache/standardebooks.test.conf @@ -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///downloads diff --git a/lib/Project.php b/lib/Project.php index c77922f3..ee7026c8 100644 --- a/lib/Project.php +++ b/lib/Project.php @@ -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'); diff --git a/templates/EbookPlaceholderForm.php b/templates/EbookPlaceholderForm.php index ca542e49..981fe959 100644 --- a/templates/EbookPlaceholderForm.php +++ b/templates/EbookPlaceholderForm.php @@ -70,7 +70,7 @@ $ebook = $ebook ?? new Ebook(); @@ -116,6 +117,7 @@ $ebook = $ebook ?? new Ebook(); name="sequence-number-collection-name-1" inputmode="numeric" pattern="[0-9]{1,3}" + autocomplete="off" value="CollectionMemberships) && sizeof($ebook->CollectionMemberships) > 0){ ?>CollectionMemberships[0]->SequenceNumber) ?>" /> @@ -149,6 +151,7 @@ $ebook = $ebook ?? new Ebook(); name="sequence-number-collection-name-2" inputmode="numeric" pattern="[0-9]{1,3}" + autocomplete="off" value="CollectionMemberships) && sizeof($ebook->CollectionMemberships) > 1){ ?>CollectionMemberships[1]->SequenceNumber) ?>" /> @@ -180,6 +183,7 @@ $ebook = $ebook ?? new Ebook(); name="sequence-number-collection-name-3" inputmode="numeric" pattern="[0-9]{1,3}" + autocomplete="off" value="CollectionMemberships) && sizeof($ebook->CollectionMemberships) > 2){ ?>CollectionMemberships[2]->SequenceNumber) ?>" /> @@ -197,7 +201,7 @@ $ebook = $ebook ?? new Ebook(); />
- $ebook->ProjectInProgress]) ?> + $ebook->ProjectInProgress, 'areFieldsRequired' => false]) ?>
@@ -237,6 +241,7 @@ $ebook = $ebook ?? new Ebook(); diff --git a/templates/EbookProjects.php b/templates/EbookProjects.php index 9e8f1da6..a7bedc00 100644 --- a/templates/EbookProjects.php +++ b/templates/EbookProjects.php @@ -2,10 +2,15 @@ /** * @var Ebook $ebook */ + +$showAddButton = $showAddButton ?? false; ?> -Projects) > 0){ ?> -
-

Projects

+
+

Projects

+ + New project + + Projects) > 0){ ?> $ebook->Projects, 'includeTitle' => false]) ?> -
- + +
diff --git a/templates/ProjectForm.php b/templates/ProjectForm.php index 7e7ecdc9..c98216f4 100644 --- a/templates/ProjectForm.php +++ b/templates/ProjectForm.php @@ -2,12 +2,18 @@ $project = $project ?? new Project(); $managers = User::GetAllByCanManageProjects(); $reviewers = User::GetAllByCanReviewProjects(); +$areFieldsRequired = $areFieldsRequired ?? true; ?> + + @@ -72,6 +78,7 @@ $reviewers = User::GetAllByCanReviewProjects(); name="project-vcs-url" placeholder="https://github.com/..." pattern="^https:\/\/github\.com\/[^\/]+/[^\/]+/?$" + autocomplete="off" value="VcsUrl ?? '') ?>" /> @@ -81,6 +88,7 @@ $reviewers = User::GetAllByCanReviewProjects(); diff --git a/www/css/core.css b/www/css/core.css index b1be4f5c..cbd1f166 100644 --- a/www/css/core.css +++ b/www/css/core.css @@ -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]{ diff --git a/www/css/ebook-placeholder.css b/www/css/ebook-placeholder.css index 221325b7..5b901dc1 100644 --- a/www/css/ebook-placeholder.css +++ b/www/css/ebook-placeholder.css @@ -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; -} diff --git a/www/css/project.css b/www/css/project.css index 5f8ddd18..8005450b 100644 --- a/www/css/project.css +++ b/www/css/project.css @@ -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; diff --git a/www/ebook-placeholders/get.php b/www/ebook-placeholders/get.php index 658d7f6d..b76702c4 100644 --- a/www/ebook-placeholders/get.php +++ b/www/ebook-placeholders/get.php @@ -1,5 +1,4 @@ Benefits->CanEditProjects || Session::$User?->Benefits->CanManageProjects || Session::$User?->Benefits->CanReviewProjects){ ?> - $ebook]) ?> + $ebook, 'showAddButton' => Session::$User->Benefits->CanEditProjects && $ebook->ProjectInProgress === null]) ?> diff --git a/www/ebook-placeholders/new.php b/www/ebook-placeholders/new.php index 820bfe9d..8f2638b8 100644 --- a/www/ebook-placeholders/new.php +++ b/www/ebook-placeholders/new.php @@ -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){ $exception]) ?> - -

Ebook Placeholder created: Title) ?>!

+ +

An ebook placeholder already exists for this ebook, but a a new project was created!

+ +

Ebook placeholder created: Title) ?>!

diff --git a/www/ebook-placeholders/post.php b/www/ebook-placeholders/post.php index 7fe066cf..50b5cf40 100644 --- a/www/ebook-placeholders/post.php +++ b/www/ebook-placeholders/post.php @@ -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; diff --git a/www/projects/index.php b/www/projects/index.php index 9980f981..d67c1b4a 100644 --- a/www/projects/index.php +++ b/www/projects/index.php @@ -1,4 +1,5 @@ value); + session_unset(); + } + $inProgressProjects = Project::GetAllByStatus(Enums\ProjectStatusType::InProgress); $stalledProjects = Project::GetAllByStatus(Enums\ProjectStatusType::Stalled); } @@ -31,6 +42,11 @@ catch(Exceptions\InvalidPermissionsException){

Projects

+ + +

Project created!

+ +

Active projects

diff --git a/www/projects/new.php b/www/projects/new.php new file mode 100644 index 00000000..bec85412 --- /dev/null +++ b/www/projects/new.php @@ -0,0 +1,79 @@ +/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(); +} +?> 'New Project', + 'css' => ['/css/project.css'], + 'description' => 'Add a new ebook project.' + ]) ?> +
+
+ Ebook)){ ?> + + +

New Project

+ + $exception]) ?> + + + $project, 'areFieldsRequired' => true]) ?> + + +
+
+ diff --git a/www/projects/post.php b/www/projects/post.php new file mode 100644 index 00000000..031276e1 --- /dev/null +++ b/www/projects/post.php @@ -0,0 +1,45 @@ +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'); +}