Allow editing of projects

This commit is contained in:
Alex Cabal 2024-12-18 14:20:28 -06:00
parent e21f411191
commit b2191d1219
16 changed files with 191 additions and 75 deletions

View file

@ -1 +1,6 @@
RewriteRule ^/ebooks/([^\.]+?)/projects/new$ /projects/new.php?ebook-url-path=$1 RewriteRule ^/ebooks/([^\.]+?)/projects/new$ /projects/new.php?ebook-url-path=$1
RewriteRule ^/projects/([\d]+)/edit$ /projects/edit.php?project-id=$1
RewriteCond expr "tolower(%{REQUEST_METHOD}) =~ /^post$/"
RewriteRule ^/projects/([\d]+)$ /projects/post.php?project-id=$1 [L]

View file

@ -36,6 +36,7 @@ use function Safe\shell_exec;
* @property string $ReadingTime * @property string $ReadingTime
* @property string $AuthorsHtml * @property string $AuthorsHtml
* @property string $AuthorsUrl This is a single URL even if there are multiple authors; for example, `/ebooks/karl-marx_friedrich-engels/`. * @property string $AuthorsUrl This is a single URL even if there are multiple authors; for example, `/ebooks/karl-marx_friedrich-engels/`.
* @property string $AuthorsString
* @property string $ContributorsHtml * @property string $ContributorsHtml
* @property string $TitleWithCreditsHtml * @property string $TitleWithCreditsHtml
* @property string $TextUrl * @property string $TextUrl
@ -115,6 +116,7 @@ class Ebook{
protected string $_ReadingTime; protected string $_ReadingTime;
protected string $_AuthorsHtml; protected string $_AuthorsHtml;
protected string $_AuthorsUrl; protected string $_AuthorsUrl;
protected string $_AuthorsString;
protected string $_ContributorsHtml; protected string $_ContributorsHtml;
protected string $_TitleWithCreditsHtml; protected string $_TitleWithCreditsHtml;
protected string $_TextUrl; protected string $_TextUrl;
@ -538,6 +540,14 @@ class Ebook{
return $this->_AuthorsUrl; return $this->_AuthorsUrl;
} }
protected function GetAuthorsString(): string{
if(!isset($this->_AuthorsString)){
$this->_AuthorsString = strip_tags(Ebook::GenerateContributorList($this->Authors, false));
}
return $this->_AuthorsString;
}
protected function GetContributorsHtml(): string{ protected function GetContributorsHtml(): string{
if(!isset($this->_ContributorsHtml)){ if(!isset($this->_ContributorsHtml)){
$this->_ContributorsHtml = ''; $this->_ContributorsHtml = '';

View file

@ -16,6 +16,7 @@ use Safe\DateTimeImmutable;
* @property User $Manager * @property User $Manager
* @property User $Reviewer * @property User $Reviewer
* @property string $Url * @property string $Url
* @property string $EditUrl
* @property DateTimeImmutable $LastActivityTimestamp The timestamp of the latest activity, whether it's a commit, a discussion post, or simply the started timestamp. * @property DateTimeImmutable $LastActivityTimestamp The timestamp of the latest activity, whether it's a commit, a discussion post, or simply the started timestamp.
* @property array<ProjectReminder> $Reminders * @property array<ProjectReminder> $Reminders
* @property ?string $VcsUrlDomain * @property ?string $VcsUrlDomain
@ -46,6 +47,7 @@ class Project{
protected User $_Manager; protected User $_Manager;
protected User $_Reviewer; protected User $_Reviewer;
protected string $_Url; protected string $_Url;
protected string $_EditUrl;
protected DateTimeImmutable $_LastActivityTimestamp; protected DateTimeImmutable $_LastActivityTimestamp;
/** @var array<ProjectReminder> $_Reminders */ /** @var array<ProjectReminder> $_Reminders */
protected array $_Reminders; protected array $_Reminders;
@ -115,6 +117,14 @@ class Project{
return $this->_Url; return $this->_Url;
} }
protected function GetEditUrl(): string{
if(!isset($this->_EditUrl)){
$this->_EditUrl = $this->Url . '/edit';
}
return $this->_EditUrl;
}
protected function GetLastActivityTimestamp(): DateTimeImmutable{ protected function GetLastActivityTimestamp(): DateTimeImmutable{
if(!isset($this->_LastActivityTimestamp)){ if(!isset($this->_LastActivityTimestamp)){
$dates = [ $dates = [

View file

@ -9,6 +9,7 @@ use function Safe\preg_match;
* @property bool $IsRegistered A user is "registered" if they have an entry in the `Benefits` table; a password is required to log in. * @property bool $IsRegistered A user is "registered" if they have an entry in the `Benefits` table; a password is required to log in.
* @property Benefits $Benefits * @property Benefits $Benefits
* @property string $Url * @property string $Url
* @property string $EditUrl
* @property ?Patron $Patron * @property ?Patron $Patron
* @property ?NewsletterSubscription $NewsletterSubscription * @property ?NewsletterSubscription $NewsletterSubscription
* @property ?Payment $LastPayment * @property ?Payment $LastPayment
@ -32,6 +33,7 @@ class User{
protected ?Payment $_LastPayment; protected ?Payment $_LastPayment;
protected Benefits $_Benefits; protected Benefits $_Benefits;
protected string $_Url; protected string $_Url;
protected string $_EditUrl;
protected ?Patron $_Patron; protected ?Patron $_Patron;
protected ?NewsletterSubscription $_NewsletterSubscription; protected ?NewsletterSubscription $_NewsletterSubscription;
protected string $_DisplayName; protected string $_DisplayName;
@ -91,6 +93,14 @@ class User{
return $this->_Url; return $this->_Url;
} }
protected function GetEditUrl(): string{
if(!isset($this->_EditUrl)){
$this->_EditUrl = $this->Url . '/edit';
}
return $this->_EditUrl;
}
/** /**
* @return array<Payment> * @return array<Payment>
*/ */

View file

@ -2,6 +2,8 @@
/** /**
* @var Ebook $ebook * @var Ebook $ebook
*/ */
$showPlaceholderMetadata = $showPlaceholderMetadata ?? false;
?> ?>
<section id="metadata"> <section id="metadata">
<h2>Metadata</h2> <h2>Metadata</h2>
@ -11,7 +13,16 @@
<td>Ebook ID:</td> <td>Ebook ID:</td>
<td><?= $ebook->EbookId ?></td> <td><?= $ebook->EbookId ?></td>
</tr> </tr>
<? if($ebook->IsPlaceholder() && $ebook->EbookPlaceholder !== null){ ?> </tbody>
</table>
</section>
<? if($showPlaceholderMetadata && $ebook->IsPlaceholder() && $ebook->EbookPlaceholder !== null){ ?>
<section id="placeholder-metadata">
<h2>Placeholder metadata</h2>
<p><a href="<?= $ebook->EditUrl ?>">Edit placeholder</a></p>
<table class="admin-table">
<tbody>
<tr> <tr>
<td>Is wanted:</td> <td>Is wanted:</td>
<td><? if($ebook->EbookPlaceholder->IsWanted){ ?>☑<? }else{ ?>☐<? } ?></td> <td><? if($ebook->EbookPlaceholder->IsWanted){ ?>☑<? }else{ ?>☐<? } ?></td>
@ -26,7 +37,8 @@
<td>Difficulty:</td> <td>Difficulty:</td>
<td><?= ucfirst($ebook->EbookPlaceholder->Difficulty->value ?? '') ?></td> <td><?= ucfirst($ebook->EbookPlaceholder->Difficulty->value ?? '') ?></td>
</tr> </tr>
<? } ?> </tbody>
</tbody> </table>
</table> </section>
</section> <? } ?>

View file

@ -1,5 +1,6 @@
<? <?
$ebook = $ebook ?? new Ebook(); $ebook = $ebook ?? new Ebook();
$isEditForm = $isEditForm ?? false;
?> ?>
<fieldset> <fieldset>
<legend>Contributors</legend> <legend>Contributors</legend>
@ -189,21 +190,23 @@ $ebook = $ebook ?? new Ebook();
</label> </label>
</fieldset> </fieldset>
</details> </details>
<fieldset> <? if(!$isEditForm){ ?>
<legend>Project</legend> <fieldset>
<label class="controls-following-fieldset"> <legend>Project</legend>
<span>In progress?</span> <label class="controls-following-fieldset">
<input type="hidden" name="ebook-placeholder-is-in-progress" value="false" /> <span>In progress?</span>
<input <input type="hidden" name="ebook-placeholder-is-in-progress" value="false" />
type="checkbox" <input
name="ebook-placeholder-is-in-progress" type="checkbox"
<? if($ebook->EbookPlaceholder?->IsInProgress){ ?>checked="checked"<? } ?> name="ebook-placeholder-is-in-progress"
/> <? if($ebook->EbookPlaceholder?->IsInProgress){ ?>checked="checked"<? } ?>
</label> />
<fieldset class="project-form"> </label>
<?= Template::ProjectForm(['project' => $ebook->ProjectInProgress, 'areFieldsRequired' => false]) ?> <fieldset class="project-form">
<?= Template::ProjectForm(['project' => $ebook->ProjectInProgress, 'areFieldsRequired' => false]) ?>
</fieldset>
</fieldset> </fieldset>
</fieldset> <? } ?>
<fieldset> <fieldset>
<legend>Wanted list</legend> <legend>Wanted list</legend>
<label class="controls-following-fieldset"> <label class="controls-following-fieldset">

View file

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

View file

@ -3,8 +3,11 @@ $project = $project ?? new Project();
$managers = User::GetAllByCanManageProjects(); $managers = User::GetAllByCanManageProjects();
$reviewers = User::GetAllByCanReviewProjects(); $reviewers = User::GetAllByCanReviewProjects();
$areFieldsRequired = $areFieldsRequired ?? true; $areFieldsRequired = $areFieldsRequired ?? true;
$isEditForm = $isEditForm ?? false;
?> ?>
<input type="hidden" name="project-ebook-id" value="<?= $project->EbookId ?? '' ?>" /> <? if(!$isEditForm){ ?>
<input type="hidden" name="project-ebook-id" value="<?= $project->EbookId ?? '' ?>" />
<? } ?>
<label class="icon user"> <label class="icon user">
<span>Producer name</span> <span>Producer name</span>

View file

@ -5,6 +5,7 @@
$includeTitle = $includeTitle ?? true; $includeTitle = $includeTitle ?? true;
$includeStatus = $includeStatus ?? true; $includeStatus = $includeStatus ?? true;
$showEditButton = $showEditButton ?? false;
?> ?>
<table class="data-table"> <table class="data-table">
<caption aria-hidden="true">Scroll right </caption> <caption aria-hidden="true">Scroll right </caption>
@ -22,6 +23,9 @@ $includeStatus = $includeStatus ?? true;
<? } ?> <? } ?>
<th></th> <th></th>
<th></th> <th></th>
<? if($showEditButton){ ?>
<th></th>
<? } ?>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -65,6 +69,11 @@ $includeStatus = $includeStatus ?? true;
<a href="<?= Formatter::EscapeHtml($project->DiscussionUrl) ?>">Discussion</a> <a href="<?= Formatter::EscapeHtml($project->DiscussionUrl) ?>">Discussion</a>
<? } ?> <? } ?>
</td> </td>
<? if($showEditButton){ ?>
<td>
<a href="<?= $project->EditUrl ?>">Edit</a>
</td>
<? } ?>
</tr> </tr>
<? } ?> <? } ?>
</tbody> </tbody>

View file

@ -7,7 +7,6 @@
grid-column: 1 / span 2 grid-column: 1 / span 2
} }
.project-form label:has(input[name="project-manager-user-id"]){ .project-form label:has(input[name="project-manager-user-id"]){
grid-column-start: 1; grid-column-start: 1;
} }

View file

@ -3,21 +3,11 @@ use function Safe\session_unset;
session_start(); session_start();
$exception = HttpInput::SessionObject('exception', Exceptions\AppException::class);
/** @var string $identifier Passed from script this is included from. */ /** @var string $identifier Passed from script this is included from. */
$ebook = HttpInput::SessionObject('ebook', Ebook::class); $ebook = HttpInput::SessionObject('ebook', Ebook::class);
$exception = HttpInput::SessionObject('exception', Exceptions\AppException::class);
try{ try{
if(Session::$User === null){
throw new Exceptions\LoginRequiredException();
}
if(!Session::$User->Benefits->CanEditEbookPlaceholders){
throw new Exceptions\InvalidPermissionsException();
}
if($ebook === null){ if($ebook === null){
$ebook = Ebook::GetByIdentifier($identifier); $ebook = Ebook::GetByIdentifier($identifier);
} }
@ -26,6 +16,14 @@ try{
throw new Exceptions\EbookNotFoundException(); throw new Exceptions\EbookNotFoundException();
} }
if(Session::$User === null){
throw new Exceptions\LoginRequiredException();
}
if(!Session::$User->Benefits->CanEditEbookPlaceholders){
throw new Exceptions\InvalidPermissionsException();
}
if($exception){ if($exception){
http_response_code(Enums\HttpCode::UnprocessableContent->value); http_response_code(Enums\HttpCode::UnprocessableContent->value);
session_unset(); session_unset();
@ -57,7 +55,7 @@ catch(Exceptions\InvalidPermissionsException){
<form class="create-update-ebook-placeholder" method="<?= Enums\HttpMethod::Post->value ?>" action="<?= $ebook->Url ?>" autocomplete="off"> <form class="create-update-ebook-placeholder" method="<?= Enums\HttpMethod::Post->value ?>" action="<?= $ebook->Url ?>" autocomplete="off">
<input type="hidden" name="_method" value="<?= Enums\HttpMethod::Put->value ?>" /> <input type="hidden" name="_method" value="<?= Enums\HttpMethod::Put->value ?>" />
<?= Template::EbookPlaceholderForm(['ebook' => $ebook]) ?> <?= Template::EbookPlaceholderForm(['ebook' => $ebook, 'isEditForm' => true]) ?>
</form> </form>
</section> </section>
</main> </main>

View file

@ -8,6 +8,7 @@ session_start();
$ebook = null; $ebook = null;
$isSaved = HttpInput::Bool(SESSION, 'is-ebook-placeholder-saved') ?? false; $isSaved = HttpInput::Bool(SESSION, 'is-ebook-placeholder-saved') ?? false;
$isProjectSaved = HttpInput::Bool(SESSION, 'is-project-saved') ?? false;
try{ try{
$ebook = Ebook::GetByIdentifier($identifier); $ebook = Ebook::GetByIdentifier($identifier);
@ -63,7 +64,11 @@ catch(Exceptions\EbookNotFoundException){
</header> </header>
<? if($isSaved){ ?> <? if($isSaved){ ?>
<p class="message success">Ebook Placeholder saved!</p> <p class="message success">Ebook placeholder saved!</p>
<? } ?>
<? if($isProjectSaved){ ?>
<p class="message success">Project saved!</p>
<? } ?> <? } ?>
<aside id="reading-ease"> <aside id="reading-ease">
@ -115,17 +120,12 @@ catch(Exceptions\EbookNotFoundException){
<? } ?> <? } ?>
</section> </section>
<? if(Session::$User?->Benefits->CanEditEbooks){ ?> <? if(Session::$User?->Benefits->CanEditEbooks || Session::$User?->Benefits->CanEditEbookPlaceholders){ ?>
<?= Template::EbookMetadata(['ebook' => $ebook]) ?> <?= Template::EbookMetadata(['ebook' => $ebook, 'showPlaceholderMetadata' => Session::$User?->Benefits->CanEditEbookPlaceholders]) ?>
<? } ?> <? } ?>
<? if(Session::$User?->Benefits->CanEditProjects || Session::$User?->Benefits->CanManageProjects || Session::$User?->Benefits->CanReviewProjects){ ?> <? if(Session::$User?->Benefits->CanEditProjects || Session::$User?->Benefits->CanManageProjects || Session::$User?->Benefits->CanReviewProjects){ ?>
<?= Template::EbookProjects(['ebook' => $ebook, 'showAddButton' => Session::$User->Benefits->CanEditProjects && $ebook->ProjectInProgress === null]) ?> <?= Template::EbookProjects(['ebook' => $ebook, 'showAddButton' => Session::$User->Benefits->CanEditProjects && $ebook->ProjectInProgress === null, 'showEditButton' => Session::$User->Benefits->CanEditProjects]) ?>
<? } ?>
<? if(Session::$User?->Benefits->CanEditEbookPlaceholders){ ?>
<h2>Edit ebook placeholder</h2>
<p><a href="<?= $ebook->EditUrl ?>">Edit this ebook placeholder.</a></p>
<? } ?> <? } ?>
</article> </article>
</main> </main>

View file

@ -16,7 +16,7 @@ try{
throw new Exceptions\InvalidPermissionsException(); throw new Exceptions\InvalidPermissionsException();
} }
// POSTing a new ebook placeholder. // POSTing an `EbookPlaceholder`.
if($httpMethod == Enums\HttpMethod::Post){ if($httpMethod == Enums\HttpMethod::Post){
$ebook = new Ebook(); $ebook = new Ebook();
@ -61,7 +61,8 @@ try{
http_response_code(Enums\HttpCode::SeeOther->value); http_response_code(Enums\HttpCode::SeeOther->value);
header('Location: /ebook-placeholders/new'); header('Location: /ebook-placeholders/new');
} }
// PUT a new ebook placeholder.
// PUT an `EbookPlaceholder`.
if($httpMethod == Enums\HttpMethod::Put){ if($httpMethod == Enums\HttpMethod::Put){
$originalEbook = Ebook::GetByIdentifier($identifier); $originalEbook = Ebook::GetByIdentifier($identifier);
$exceptionRedirectUrl = $originalEbook->EditUrl; $exceptionRedirectUrl = $originalEbook->EditUrl;
@ -72,35 +73,8 @@ try{
$ebook->EbookId = $originalEbook->EbookId; $ebook->EbookId = $originalEbook->EbookId;
$ebook->Created = $originalEbook->Created; $ebook->Created = $originalEbook->Created;
// Do we have a `Project` to create/save at the same time?
$project = null;
if($ebook->EbookPlaceholder?->IsInProgress){
$originalProject = $originalEbook->ProjectInProgress;
$project = new Project();
$project->FillFromHttpPost();
$project->EbookId = $ebook->EbookId;
$project->Ebook = $ebook;
if(isset($originalProject)){
$project->ProjectId = $originalProject->ProjectId;
$project->Started = $originalProject->Started;
}
else{
$project->Started = NOW;
}
$project->Validate();
}
$ebook->Save(); $ebook->Save();
if($ebook->EbookPlaceholder?->IsInProgress && $project !== null){
if(isset($originalProject)){
$project->Save();
}
else{
$project->Create();
}
}
$_SESSION['is-ebook-placeholder-saved'] = true; $_SESSION['is-ebook-placeholder-saved'] = true;
http_response_code(Enums\HttpCode::SeeOther->value); http_response_code(Enums\HttpCode::SeeOther->value);
header('Location: ' . $ebook->Url); header('Location: ' . $ebook->Url);

65
www/projects/edit.php Normal file
View file

@ -0,0 +1,65 @@
<?
use function Safe\session_unset;
session_start();
$project = HttpInput::SessionObject('project', Project::class);
$exception = HttpInput::SessionObject('exception', Exceptions\AppException::class);
try{
if($project === null){
$project = Project::Get(HttpInput::Int(GET, 'project-id'));
}
if(Session::$User === null){
throw new Exceptions\LoginRequiredException();
}
if(!Session::$User->Benefits->CanEditProjects){
throw new Exceptions\InvalidPermissionsException();
}
if($exception){
http_response_code(Enums\HttpCode::UnprocessableContent->value);
session_unset();
}
}
catch(Exceptions\ProjectNotFoundException){
Template::ExitWithCode(Enums\HttpCode::NotFound);
}
catch(Exceptions\LoginRequiredException){
Template::RedirectToLogin();
}
catch(Exceptions\InvalidPermissionsException){
Template::ExitWithCode(Enums\HttpCode::Forbidden);
}
?>
<?= Template::Header(
[
'title' => 'Edit Project for ' . $project->Ebook->Title,
'css' => ['/css/project.css'],
'highlight' => '',
'description' => 'Edit the project for ' . $project->Ebook->Title
]
) ?>
<main>
<section class="narrow">
<nav class="breadcrumbs">
<a href="<?= $project->Ebook->AuthorsUrl ?>"><?= $project->Ebook->AuthorsString ?></a> →
<a href="<?= $project->Ebook->Url ?>"><?= Formatter::EscapeHtml($project->Ebook->Title) ?></a> →
</nav>
<h1>Edit Project</h1>
<?= Template::Error(['exception' => $exception]) ?>
<form class="project-form" method="<?= Enums\HttpMethod::Post->value ?>" action="<?= $project->Url ?>" autocomplete="off">
<input type="hidden" name="_method" value="<?= Enums\HttpMethod::Put->value ?>" />
<?= Template::ProjectForm(['project' => $project, 'isEditForm' => true]) ?>
<div class="footer">
<button>Save</button>
</div>
</form>
</section>
</main>
<?= Template::Footer() ?>

View file

@ -4,6 +4,7 @@ try{
session_start(); session_start();
$httpMethod = HttpInput::ValidateRequestMethod([Enums\HttpMethod::Post, Enums\HttpMethod::Patch, Enums\HttpMethod::Put]); $httpMethod = HttpInput::ValidateRequestMethod([Enums\HttpMethod::Post, Enums\HttpMethod::Patch, Enums\HttpMethod::Put]);
$exceptionRedirectUrl = '/projects/new';
if(Session::$User === null){ if(Session::$User === null){
throw new Exceptions\LoginRequiredException(); throw new Exceptions\LoginRequiredException();
@ -26,6 +27,20 @@ try{
http_response_code(Enums\HttpCode::SeeOther->value); http_response_code(Enums\HttpCode::SeeOther->value);
header('Location: /projects'); header('Location: /projects');
} }
// PUTing a `Project`.
if($httpMethod == Enums\HttpMethod::Put){
$project = Project::Get(HttpInput::Int(GET, 'project-id'));
$exceptionRedirectUrl = $project->EditUrl;
$project->FillFromHttpPost();
$project->Save();
$_SESSION['is-project-saved'] = true;
http_response_code(Enums\HttpCode::SeeOther->value);
header('Location: ' . $project->Ebook->Url);
}
} }
catch(Exceptions\EbookNotFoundException){ catch(Exceptions\EbookNotFoundException){
Template::ExitWithCode(Enums\HttpCode::NotFound); Template::ExitWithCode(Enums\HttpCode::NotFound);
@ -41,5 +56,5 @@ catch(Exceptions\InvalidProjectException | Exceptions\ProjectExistsException | E
$_SESSION['exception'] = $ex; $_SESSION['exception'] = $ex;
http_response_code(Enums\HttpCode::SeeOther->value); http_response_code(Enums\HttpCode::SeeOther->value);
header('Location: /projects/new'); header('Location: ' . $exceptionRedirectUrl);
} }

View file

@ -45,7 +45,7 @@ catch(Exceptions\InvalidPermissionsException){
<p class="message success">User saved!</p> <p class="message success">User saved!</p>
<? } ?> <? } ?>
<a href="<?= $user->Url ?>/edit">Edit user</a> <a href="<?= $user->EditUrl ?>">Edit user</a>
<? if($user->Benefits->CanManageProjects || $user->Benefits->CanReviewProjects){ ?> <? if($user->Benefits->CanManageProjects || $user->Benefits->CanReviewProjects){ ?>
<a href="<?= $user->Url ?>/projects">Projects</a> <a href="<?= $user->Url ?>/projects">Projects</a>