Further refine projects system

This commit is contained in:
Alex Cabal 2024-12-15 22:44:31 -06:00
parent 2449de6f6c
commit 5782d6ca7d
20 changed files with 307 additions and 94 deletions

View file

@ -3,4 +3,6 @@ RewriteRule ^/users/([\d]+)$ /users/post.php?user-id=$1 [L]
RewriteRule ^/users/([^/]+)$ /users/get.php?user-identifier=$1 [B,L] RewriteRule ^/users/([^/]+)$ /users/get.php?user-identifier=$1 [B,L]
RewriteRule ^/users/([\d]+)/edit$ /users/edit.php?user-id=$1 [L] RewriteRule ^/users/([^/]+)/edit$ /users/edit.php?user-identifier=$1 [L]
RewriteRule ^/users/([^/]+)/projects$ /users/projects/index.php?user-identifier=$1 [L]

View file

@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS `Benefits` (
`CanEditUsers` tinyint(1) unsigned NOT NULL DEFAULT 0, `CanEditUsers` tinyint(1) unsigned NOT NULL DEFAULT 0,
`CanEditCollections` tinyint(1) unsigned NOT NULL DEFAULT 0, `CanEditCollections` tinyint(1) unsigned NOT NULL DEFAULT 0,
`CanEditEbooks` tinyint(1) unsigned NOT NULL DEFAULT 0, `CanEditEbooks` tinyint(1) unsigned NOT NULL DEFAULT 0,
`CanCreateEbookPlaceholders` tinyint(1) unsigned NOT NULL DEFAULT 0, `CanEditEbookPlaceholders` tinyint(1) unsigned NOT NULL DEFAULT 0,
`CanManageProjects` tinyint(1) unsigned NOT NULL DEFAULT 0, `CanManageProjects` tinyint(1) unsigned NOT NULL DEFAULT 0,
`CanReviewProjects` tinyint(1) unsigned NOT NULL DEFAULT 0, `CanReviewProjects` tinyint(1) unsigned NOT NULL DEFAULT 0,
`CanEditProjects` tinyint(1) unsigned NOT NULL DEFAULT 0, `CanEditProjects` tinyint(1) unsigned NOT NULL DEFAULT 0,

View file

@ -17,7 +17,7 @@ class Benefits{
public bool $CanEditUsers = false; public bool $CanEditUsers = false;
public bool $CanEditCollections = false; public bool $CanEditCollections = false;
public bool $CanEditEbooks = false; public bool $CanEditEbooks = false;
public bool $CanCreateEbookPlaceholders = false; public bool $CanEditEbookPlaceholders = false;
public bool $CanManageProjects = false; public bool $CanManageProjects = false;
public bool $CanReviewProjects = false; public bool $CanReviewProjects = false;
public bool $CanEditProjects = false; public bool $CanEditProjects = false;
@ -38,7 +38,7 @@ class Benefits{
|| ||
$this->CanEditEbooks $this->CanEditEbooks
|| ||
$this->CanCreateEbookPlaceholders $this->CanEditEbookPlaceholders
|| ||
$this->CanManageProjects $this->CanManageProjects
|| ||
@ -76,18 +76,18 @@ class Benefits{
public function Create(): void{ public function Create(): void{
Db::Query(' Db::Query('
INSERT into Benefits (UserId, CanAccessFeeds, CanVote, CanBulkDownload, CanUploadArtwork, CanReviewArtwork, CanReviewOwnArtwork, CanEditUsers, CanCreateEbookPlaceholders) INSERT into Benefits (UserId, CanAccessFeeds, CanVote, CanBulkDownload, CanUploadArtwork, CanReviewArtwork, CanReviewOwnArtwork, CanEditUsers, CanEditEbookPlaceholders)
values (?, ?, ?, ?, ?, ?, ?, ?, ?) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
', [$this->UserId, $this->CanAccessFeeds, $this->CanVote, $this->CanBulkDownload, $this->CanUploadArtwork, $this->CanReviewArtwork, $this->CanReviewOwnArtwork, $this->CanEditUsers, $this->CanCreateEbookPlaceholders]); ', [$this->UserId, $this->CanAccessFeeds, $this->CanVote, $this->CanBulkDownload, $this->CanUploadArtwork, $this->CanReviewArtwork, $this->CanReviewOwnArtwork, $this->CanEditUsers, $this->CanEditEbookPlaceholders]);
} }
public function Save(): void{ public function Save(): void{
Db::Query(' Db::Query('
UPDATE Benefits UPDATE Benefits
set CanAccessFeeds = ?, CanVote = ?, CanBulkDownload = ?, CanUploadArtwork = ?, CanReviewArtwork = ?, CanReviewOwnArtwork = ?, CanEditUsers = ?, CanCreateEbookPlaceholders = ? set CanAccessFeeds = ?, CanVote = ?, CanBulkDownload = ?, CanUploadArtwork = ?, CanReviewArtwork = ?, CanReviewOwnArtwork = ?, CanEditUsers = ?, CanEditEbookPlaceholders = ?
where where
UserId = ? UserId = ?
', [$this->CanAccessFeeds, $this->CanVote, $this->CanBulkDownload, $this->CanUploadArtwork, $this->CanReviewArtwork, $this->CanReviewOwnArtwork, $this->CanEditUsers, $this->CanCreateEbookPlaceholders, $this->UserId]); ', [$this->CanAccessFeeds, $this->CanVote, $this->CanBulkDownload, $this->CanUploadArtwork, $this->CanReviewArtwork, $this->CanReviewOwnArtwork, $this->CanEditUsers, $this->CanEditEbookPlaceholders, $this->UserId]);
} }
public function FillFromHttpPost(): void{ public function FillFromHttpPost(): void{
@ -98,6 +98,9 @@ class Benefits{
$this->PropertyFromHttp('CanReviewArtwork'); $this->PropertyFromHttp('CanReviewArtwork');
$this->PropertyFromHttp('CanReviewOwnArtwork'); $this->PropertyFromHttp('CanReviewOwnArtwork');
$this->PropertyFromHttp('CanEditUsers'); $this->PropertyFromHttp('CanEditUsers');
$this->PropertyFromHttp('CanCreateEbookPlaceholders'); $this->PropertyFromHttp('CanEditEbookPlaceholders');
$this->PropertyFromHttp('CanEditProjects');
$this->PropertyFromHttp('CanReviewProjects');
$this->PropertyFromHttp('CanManageProjects');
} }
} }

View file

@ -15,6 +15,7 @@ use Safe\DateTimeImmutable;
* @property User $ManagerUser * @property User $ManagerUser
* @property User $ReviewerUser * @property User $ReviewerUser
* @property string $Url * @property string $Url
* @property DateTimeImmutable $LastActivityTimestamp The timestamp of the latest activity, whether it's a commit, a discussion post, or simply the started timestamp.
*/ */
class Project{ class Project{
use Traits\Accessor; use Traits\Accessor;
@ -40,6 +41,7 @@ class Project{
protected User $_ManagerUser; protected User $_ManagerUser;
protected User $_ReviewerUser; protected User $_ReviewerUser;
protected string $_Url; protected string $_Url;
protected DateTimeImmutable $_LastActivityTimestamp;
// ******* // *******
@ -54,6 +56,22 @@ class Project{
return $this->_Url; return $this->_Url;
} }
protected function GetLastActivityTimestamp(): DateTimeImmutable{
if(!isset($this->_LastActivityTimestamp)){
$dates = [
(int)($this->LastCommitTimestamp?->format(Enums\DateTimeFormat::UnixTimestamp->value) ?? 0) => $this->LastCommitTimestamp ?? NOW,
(int)($this->LastDiscussionTimestamp?->format(Enums\DateTimeFormat::UnixTimestamp->value) ?? 0) => $this->LastDiscussionTimestamp ?? NOW,
(int)($this->Started->format(Enums\DateTimeFormat::UnixTimestamp->value)) => $this->Started,
];
ksort($dates);
$this->_LastActivityTimestamp = end($dates);
}
return $this->_LastActivityTimestamp;
}
// ******* // *******
// METHODS // METHODS
@ -96,6 +114,12 @@ class Project{
if($this->DiscussionUrl == ''){ if($this->DiscussionUrl == ''){
$this->DiscussionUrl = null; $this->DiscussionUrl = null;
} }
else{
if(preg_match('|^https://groups\.google\.com/g/standardebooks/|iu', $this->DiscussionUrl)){
// Get the base thread URL in case we were passed a URL with a specific message or query string.
$this->DiscussionUrl = preg_replace('|^(https://groups\.google\.com/g/standardebooks/c/[^/]+).*|iu', '\1', $this->DiscussionUrl);
}
}
$this->VcsUrl = rtrim(trim($this->VcsUrl ?? ''), '/'); $this->VcsUrl = rtrim(trim($this->VcsUrl ?? ''), '/');
if($this->VcsUrl == ''){ if($this->VcsUrl == ''){
@ -268,6 +292,10 @@ class Project{
* @throws Exceptions\AppException If the operation failed. * @throws Exceptions\AppException If the operation failed.
*/ */
public function FetchLatestCommitTimestamp(?string $apiKey = null): void{ public function FetchLatestCommitTimestamp(?string $apiKey = null): void{
if(!preg_match('|^https://github\.com/|iu', $this->VcsUrl)){
return;
}
$headers = [ $headers = [
'Accept: application/vnd.github+json', 'Accept: application/vnd.github+json',
'X-GitHub-Api-Version: 2022-11-28', 'X-GitHub-Api-Version: 2022-11-28',
@ -327,7 +355,7 @@ class Project{
* @throws Exceptions\AppException If the operation faile.d * @throws Exceptions\AppException If the operation faile.d
*/ */
public function FetchLastDiscussionTimestamp(): void{ public function FetchLastDiscussionTimestamp(): void{
if($this->DiscussionUrl === null){ if(!preg_match('|^https://groups\.google\.com/g/standardebooks/|iu', $this->DiscussionUrl ?? '')){
return; return;
} }
@ -347,7 +375,7 @@ class Project{
throw new Exception('HTTP code ' . $httpCode . ' received for URL <' . $this->DiscussionUrl . '>.'); throw new Exception('HTTP code ' . $httpCode . ' received for URL <' . $this->DiscussionUrl . '>.');
} }
$matchCount = preg_match_all('/<span class="[^"]+?">([a-z]{3} [\d]{1,2}, [\d]{4}, [\d]{2}:[\d]{2}:[\d]{2}(?:AM|PM))<\/span>/iu', $response, $matches); $matchCount = preg_match_all('/<span class="[^"]+?">([a-z]{3} [\d]{1,2}, [\d]{4}, [\d]{1,2}:[\d]{1,2}:[\d]{1,2}(?:AM|PM))/iu', $response, $matches);
if($matchCount > 0){ if($matchCount > 0){
// Unsure of the time zone, so just assume UTC. // Unsure of the time zone, so just assume UTC.
@ -390,4 +418,18 @@ class Project{
public static function GetAllByStatus(Enums\ProjectStatusType $status): array{ public static function GetAllByStatus(Enums\ProjectStatusType $status): array{
return Db::Query('SELECT * from Projects where Status = ? order by Started desc', [$status], Project::class); return Db::Query('SELECT * from Projects where Status = ? order by Started desc', [$status], Project::class);
} }
/**
* @return array<Project>
*/
public static function GetAllByManagerUserId(int $userId): array{
return Db::Query('SELECT * from Projects where ManagerUserId = ? and Status in (?, ?) order by Started desc', [$userId, Enums\ProjectStatusType::InProgress, Enums\ProjectStatusType::Stalled], Project::class);
}
/**
* @return array<Project>
*/
public static function GetAllByReviewerUserId(int $userId): array{
return Db::Query('SELECT * from Projects where ReviewerUserId = ? and Status in (?, ?) order by Started desc', [$userId, Enums\ProjectStatusType::InProgress, Enums\ProjectStatusType::Stalled], Project::class);
}
} }

View file

@ -12,6 +12,7 @@ use function Safe\preg_match;
* @property ?Patron $Patron * @property ?Patron $Patron
* @property ?NewsletterSubscription $NewsletterSubscription * @property ?NewsletterSubscription $NewsletterSubscription
* @property ?Payment $LastPayment * @property ?Payment $LastPayment
* @property string $DisplayName A string that represent's the `User`'s name, or email, or ID.
*/ */
class User{ class User{
use Traits\Accessor; use Traits\Accessor;
@ -33,12 +34,29 @@ class User{
protected string $_Url; protected string $_Url;
protected ?Patron $_Patron; protected ?Patron $_Patron;
protected ?NewsletterSubscription $_NewsletterSubscription; protected ?NewsletterSubscription $_NewsletterSubscription;
protected string $_DisplayName;
// ******* // *******
// GETTERS // GETTERS
// ******* // *******
protected function GetDisplayName(): string{
if(!isset($this->_DisplayName)){
if($this->Name !== null){
$this->_DisplayName = $this->Name;
}
elseif($this->Email !== null){
$this->_DisplayName = $this->Email;
}
else{
$this->_DisplayName = 'User #' . $this->UserId;
}
}
return $this->_DisplayName;
}
protected function GetNewsletterSubscription(): ?NewsletterSubscription{ protected function GetNewsletterSubscription(): ?NewsletterSubscription{
if(!isset($this->_NewsletterSubscription)){ if(!isset($this->_NewsletterSubscription)){
try{ try{

View file

@ -3,17 +3,12 @@
require_once('/standardebooks.org/web/lib/Core.php'); require_once('/standardebooks.org/web/lib/Core.php');
use function Safe\file_get_contents; use function Safe\file_get_contents;
use Safe\DateTimeImmutable;
/** /**
* Iterate over all `Project`s that are in progress or stalled and get their latest GitHub commit. If the commit is more than 30 days old, mark the `Project` as stalled. * Iterate over all `Project`s that are in progress or stalled and get their latest GitHub commit. If the commit is more than 30 days old, mark the `Project` as stalled.
*/ */
use Safe\DateTimeImmutable;
$projects = array_merge( $projects = array_merge(
Project::GetAllByStatus(Enums\ProjectStatusType::InProgress), Project::GetAllByStatus(Enums\ProjectStatusType::InProgress),
Project::GetAllByStatus(Enums\ProjectStatusType::Stalled) Project::GetAllByStatus(Enums\ProjectStatusType::Stalled)

View file

@ -3,33 +3,30 @@
* @var Ebook $ebook * @var Ebook $ebook
*/ */
?> ?>
<h2>Metadata</h2> <section id="metadata">
<table class="admin-table"> <h2>Metadata</h2>
<tbody> <table class="admin-table">
<tr> <tbody>
<td>Ebook ID:</td>
<td><?= $ebook->EbookId ?></td>
</tr>
<? if($ebook->IsPlaceholder() && $ebook->EbookPlaceholder !== null){ ?>
<tr> <tr>
<td>Is wanted:</td> <td>Ebook ID:</td>
<td><? if($ebook->EbookPlaceholder->IsWanted){ ?>☑<? }else{ ?>☐<? } ?></td> <td><?= $ebook->EbookId ?></td>
</tr> </tr>
<? if($ebook->EbookPlaceholder->IsWanted){ ?> <? if($ebook->IsPlaceholder() && $ebook->EbookPlaceholder !== null){ ?>
<tr> <tr>
<td>Is Patron selection:</td> <td>Is wanted:</td>
<td><? if($ebook->EbookPlaceholder->IsPatron){ ?>☑<? }else{ ?>☐<? } ?></td> <td><? if($ebook->EbookPlaceholder->IsWanted){ ?>☑<? }else{ ?>☐<? } ?></td>
</tr>
<? if($ebook->EbookPlaceholder->IsWanted){ ?>
<tr>
<td>Is Patron selection:</td>
<td><? if($ebook->EbookPlaceholder->IsPatron){ ?>☑<? }else{ ?>☐<? } ?></td>
</tr>
<? } ?>
<tr>
<td>Difficulty:</td>
<td><?= ucfirst($ebook->EbookPlaceholder->Difficulty->value ?? '') ?></td>
</tr> </tr>
<? } ?> <? } ?>
<tr> </tbody>
<td>Difficulty:</td> </table>
<td><?= ucfirst($ebook->EbookPlaceholder->Difficulty->value ?? '') ?></td> </section>
</tr>
<? } ?>
</tbody>
</table>
<? if(sizeof($ebook->Projects) > 0){ ?>
<h2>Projects</h2>
<?= Template::ProjectsTable(['projects' => $ebook->Projects, 'includeTitle' => false]) ?>
<? } ?>

View file

@ -0,0 +1,11 @@
<?
/**
* @var Ebook $ebook
*/
?>
<? if(sizeof($ebook->Projects) > 0){ ?>
<section id="projects">
<h2>Projects</h2>
<?= Template::ProjectsTable(['projects' => $ebook->Projects, 'includeTitle' => false]) ?>
</section>
<? } ?>

View file

@ -14,8 +14,10 @@ $includeStatus = $includeStatus ?? true;
<th scope="col">Title</th> <th scope="col">Title</th>
<? } ?> <? } ?>
<th scope="col">Producer</th> <th scope="col">Producer</th>
<th scope="col">Manager</th>
<th scope="col">Reviewer</th>
<th scope="col">Started</th> <th scope="col">Started</th>
<th scope="col">Last commit</th> <th scope="col">Last activity</th>
<? if($includeStatus){ ?> <? if($includeStatus){ ?>
<th scope="col">Status</th> <th scope="col">Status</th>
<? } ?> <? } ?>
@ -38,14 +40,20 @@ $includeStatus = $includeStatus ?? true;
<?= Formatter::EscapeHtml($project->ProducerName) ?> <?= Formatter::EscapeHtml($project->ProducerName) ?>
<? } ?> <? } ?>
</td> </td>
<td>
<a href="<?= $project->ManagerUser->Url ?>/projects"><?= Formatter::EscapeHtml($project->ManagerUser->DisplayName) ?></a>
</td>
<td>
<a href="<?= $project->ReviewerUser->Url ?>/projects"><?= Formatter::EscapeHtml($project->ReviewerUser->DisplayName) ?></a>
</td>
<td> <td>
<?= $project->Started->format(Enums\DateTimeFormat::ShortDate->value) ?> <?= $project->Started->format(Enums\DateTimeFormat::ShortDate->value) ?>
</td> </td>
<td> <td>
<?= $project->LastCommitTimestamp?->format(Enums\DateTimeFormat::ShortDate->value) ?> <?= $project->LastActivityTimestamp->format(Enums\DateTimeFormat::ShortDate->value) ?>
</td> </td>
<? if($includeStatus){ ?> <? if($includeStatus){ ?>
<td class="status"> <td class="status<? if($project->Status == Enums\ProjectStatusType::Stalled){ ?> stalled<? } ?>">
<?= ucfirst($project->Status->GetDisplayName()) ?> <?= ucfirst($project->Status->GetDisplayName()) ?>
</td> </td>
<? } ?> <? } ?>

View file

@ -137,9 +137,30 @@ $passwordAction = $passwordAction ?? Enums\PasswordActionType::None;
</li> </li>
<li> <li>
<label> <label>
<input type="hidden" name="benefits-can-create-ebook-placeholder" value="false" /> <input type="hidden" name="benefits-can-edit-ebook-placeholders" value="false" />
<input type="checkbox" name="benefits-can-create-ebook-placeholder" value="true"<? if($user->Benefits->CanCreateEbookPlaceholders){ ?> checked="checked"<? } ?> /> <input type="checkbox" name="benefits-can-edit-ebook-placeholders" value="true"<? if($user->Benefits->CanEditEbookPlaceholders){ ?> checked="checked"<? } ?> />
Can create ebook placeholders Can edit ebook placeholders
</label>
</li>
<li>
<label>
<input type="hidden" name="benefits-can-edit-projects" value="false" />
<input type="checkbox" name="benefits-can-edit-projects" value="true"<? if($user->Benefits->CanManageProjects){ ?> checked="checked"<? } ?> />
Can edit projects
</label>
</li>
<li>
<label>
<input type="hidden" name="benefits-can-manage-projects" value="false" />
<input type="checkbox" name="benefits-can-manage-projects" value="true"<? if($user->Benefits->CanManageProjects){ ?> checked="checked"<? } ?> />
Can manage projects
</label>
</li>
<li>
<label>
<input type="hidden" name="benefits-can-review-projects" value="false" />
<input type="checkbox" name="benefits-can-review-projects" value="true"<? if($user->Benefits->CanManageProjects){ ?> checked="checked"<? } ?> />
Can review projects
</label> </label>
</li> </li>
</ul> </ul>

View file

@ -749,6 +749,16 @@ ul.message.error > li + li{
padding-top: 2rem; padding-top: 2rem;
} }
.data-table td:first-child,
.data-table th:first-child{
padding-left: 0;
}
.data-table td:last-child,
.data-table th:last-child{
padding-left: 0;
}
.data-table td, .data-table td,
.data-table th{ .data-table th{
padding: .25rem .5rem; padding: .25rem .5rem;
@ -3293,6 +3303,20 @@ table.admin-table td + td{
width: 100%; width: 100%;
} }
.empty-notice{
text-align: center;
font-style: italic;
}
nav.breadcrumbs{
font-style: italic;
margin-top: 1rem;
}
nav.breadcrumbs + h1{
margin-top: 0;
}
@media (hover: none) and (pointer: coarse){ /* target ipads and smartphones without a mouse */ @media (hover: none) and (pointer: coarse){ /* target ipads and smartphones without a mouse */
/* For iPad, unset the height so it matches the other elements */ /* For iPad, unset the height so it matches the other elements */
select[multiple]{ select[multiple]{

View file

@ -12,15 +12,16 @@
grid-column-start: 1; grid-column-start: 1;
} }
h2 + table.projects{
margin-top: 2rem;
}
table.projects thead{
font-style: italic;
}
table.data-table .status, table.data-table .status,
table.data-table .producer{ table.data-table .producer{
white-space: nowrap; white-space: nowrap;
} }
table.data-table{
width: 100%;
}
table.data-table td.status.stalled{
background: #861d1d !important; /* Override hover backgound color */
color: #ffffff;
}

View file

@ -104,9 +104,11 @@ catch(Exceptions\EbookNotFoundException){
</section> </section>
<? if(Session::$User?->Benefits->CanEditEbooks){ ?> <? if(Session::$User?->Benefits->CanEditEbooks){ ?>
<section id="metadata"> <?= Template::EbookMetadata(['ebook' => $ebook]) ?>
<?= Template::EbookMetadata(['ebook' => $ebook]) ?> <? } ?>
</section>
<? if(Session::$User?->Benefits->CanEditProjects || Session::$User?->Benefits->CanManageProjects || Session::$User?->Benefits->CanReviewProjects){ ?>
<?= Template::EbookProjects(['ebook' => $ebook]) ?>
<? } ?> <? } ?>
</article> </article>
</main> </main>

View file

@ -12,7 +12,7 @@ try{
throw new Exceptions\LoginRequiredException(); throw new Exceptions\LoginRequiredException();
} }
if(!Session::$User->Benefits->CanCreateEbookPlaceholders){ if(!Session::$User->Benefits->CanEditEbookPlaceholders){
throw new Exceptions\InvalidPermissionsException(); throw new Exceptions\InvalidPermissionsException();
} }

View file

@ -11,7 +11,7 @@ try{
// POSTing a new ebook placeholder. // POSTing a new ebook placeholder.
if($httpMethod == Enums\HttpMethod::Post){ if($httpMethod == Enums\HttpMethod::Post){
if(!Session::$User->Benefits->CanCreateEbookPlaceholders){ if(!Session::$User->Benefits->CanEditEbookPlaceholders){
throw new Exceptions\InvalidPermissionsException(); throw new Exceptions\InvalidPermissionsException();
} }

View file

@ -398,9 +398,7 @@ catch(Exceptions\EbookNotFoundException){
</section> </section>
<? if(Session::$User?->Benefits->CanEditEbooks){ ?> <? if(Session::$User?->Benefits->CanEditEbooks){ ?>
<section id="metadata"> <?= Template::EbookMetadata(['ebook' => $ebook]) ?>
<?= Template::EbookMetadata(['ebook' => $ebook]) ?>
</section>
<? } ?> <? } ?>
<? if(sizeof($carousel) > 0){ ?> <? if(sizeof($carousel) > 0){ ?>

View file

@ -4,7 +4,13 @@ try{
throw new Exceptions\LoginRequiredException(); throw new Exceptions\LoginRequiredException();
} }
if(!Session::$User->Benefits->CanEditProjects){ if(
!Session::$User->Benefits->CanManageProjects
&&
!Session::$User->Benefits->CanReviewProjects
&&
!Session::$User->Benefits->CanEditProjects
){
throw new Exceptions\InvalidPermissionsException(); throw new Exceptions\InvalidPermissionsException();
} }
@ -17,22 +23,28 @@ catch(Exceptions\LoginRequiredException){
catch(Exceptions\InvalidPermissionsException){ catch(Exceptions\InvalidPermissionsException){
Template::Emit403(); Template::Emit403();
} }
?><?= Template::Header(['title' => 'Projects', 'css' => ['/css/project.css'], 'description' => 'Ebook projects currently underway at Standard Ebooks.']) ?> ?><?= Template::Header([
'title' => 'Projects',
'css' => ['/css/project.css'],
'description' => 'Ebook projects currently underway at Standard Ebooks.'
]) ?>
<main> <main>
<section class="narrow"> <section>
<h1>Projects</h1> <h1>Projects</h1>
<h2>Active projects</h2> <section id="active">
<? if(sizeof($inProgressProjects) == 0){ ?> <h2>Active projects</h2>
<p> <? if(sizeof($inProgressProjects) == 0){ ?>
<i>None.</i> <p class="empty-notice">None.</p>
</p> <? }else{ ?>
<? }else{ ?> <?= Template::ProjectsTable(['projects' => $inProgressProjects, 'includeStatus' => false]) ?>
<?= Template::ProjectsTable(['projects' => $inProgressProjects, 'includeStatus' => false]) ?> <? } ?>
<? } ?> </section>
<? if(sizeof($stalledProjects) > 0){ ?> <? if(sizeof($stalledProjects) > 0){ ?>
<h2>Stalled projects</h2> <section id="stalled">
<?= Template::ProjectsTable(['projects' => $stalledProjects, 'includeStatus' => false]) ?> <h2>Stalled projects</h2>
<?= Template::ProjectsTable(['projects' => $stalledProjects, 'includeStatus' => false]) ?>
</section>
<? } ?> <? } ?>
</section> </section>
</main> </main>

View file

@ -10,7 +10,7 @@ $passwordAction = HttpInput::SessionObject('password-action', Enums\PasswordActi
try{ try{
if($user === null){ if($user === null){
$user = User::Get(HttpInput::Int(GET, 'user-id')); $user = User::GetByIdentifier(HttpInput::Str(GET, 'user-identifier'));
} }
if(Session::$User === null){ if(Session::$User === null){
@ -40,13 +40,14 @@ catch(Exceptions\InvalidPermissionsException){
<?= Template::Header( <?= Template::Header(
[ [
'title' => 'Edit user #' . $user->UserId, 'title' => 'Edit user #' . $user->UserId,
'canonicalUrl' => $user->Url . '/edit',
'css' => ['/css/user.css'], 'css' => ['/css/user.css'],
'highlight' => '' 'highlight' => ''
] ]
) ?> ) ?>
<main> <main>
<section class="narrow"> <section class="narrow">
<h1>Edit User #<?= $user->UserId ?></h1> <h1>Edit <?= Formatter::EscapeHtml($user->DisplayName) ?></h1>
<?= Template::Error(['exception' => $exception]) ?> <?= Template::Error(['exception' => $exception]) ?>

View file

@ -8,11 +8,6 @@ $isSaved = HttpInput::Bool(SESSION, 'is-user-saved') ?? false;
try{ try{
$user = User::GetByIdentifier(HttpInput::Str(GET, 'user-identifier')); $user = User::GetByIdentifier(HttpInput::Str(GET, 'user-identifier'));
// Even though the identifier can be either an email, user ID, or UUID, we want the URL of this page to be based on a user ID only.
if(!ctype_digit(HttpInput::Str(GET, 'user-identifier'))){
throw new Exceptions\SeeOtherException($user->Url);
}
if(Session::$User === null){ if(Session::$User === null){
throw new Exceptions\LoginRequiredException(); throw new Exceptions\LoginRequiredException();
} }
@ -35,15 +30,16 @@ catch(Exceptions\LoginRequiredException){
catch(Exceptions\InvalidPermissionsException){ catch(Exceptions\InvalidPermissionsException){
Template::Emit403(); Template::Emit403();
} }
catch(Exceptions\SeeOtherException $ex){ ?><?= Template::Header(
http_response_code(Enums\HttpCode::SeeOther->value); [
header('Location: ' . $ex->Url); 'title' => $user->DisplayName,
} 'canonicalUrl' => $user->Url,
'css' => ['/css/user.css']
?><?= Template::Header(['title' => 'User #' . $user->UserId, 'css' => ['/css/user.css']]) ?> ]
) ?>
<main> <main>
<section class="narrow"> <section class="narrow">
<h1>User #<?= $user->UserId ?></h1> <h1><?= Formatter::EscapeHtml($user->DisplayName) ?></h1>
<? if($isSaved){ ?> <? if($isSaved){ ?>
<p class="message success">User saved!</p> <p class="message success">User saved!</p>
@ -51,9 +47,17 @@ catch(Exceptions\SeeOtherException $ex){
<a href="<?= $user->Url ?>/edit">Edit user</a> <a href="<?= $user->Url ?>/edit">Edit user</a>
<? if($user->Benefits->CanManageProjects || $user->Benefits->CanReviewProjects){ ?>
<a href="<?= $user->Url ?>/projects">Projects</a>
<? } ?>
<h2>Basics</h2> <h2>Basics</h2>
<table class="admin-table"> <table class="admin-table">
<tbody> <tbody>
<tr>
<td>User ID:</td>
<td><?= $user->UserId ?></td>
</tr>
<tr> <tr>
<td>Email:</td> <td>Email:</td>
<td><?= Formatter::EscapeHtml($user->Email) ?></td> <td><?= Formatter::EscapeHtml($user->Email) ?></td>
@ -170,8 +174,20 @@ catch(Exceptions\SeeOtherException $ex){
<td><? if($user->Benefits->CanEditUsers){ ?>☑<? }else{ ?>☐<? } ?></td> <td><? if($user->Benefits->CanEditUsers){ ?>☑<? }else{ ?>☐<? } ?></td>
</tr> </tr>
<tr> <tr>
<td>Can create ebook placeholders:</td> <td>Can edit ebook placeholders:</td>
<td><? if($user->Benefits->CanCreateEbookPlaceholders){ ?>☑<? }else{ ?>☐<? } ?></td> <td><? if($user->Benefits->CanEditEbookPlaceholders){ ?>☑<? }else{ ?>☐<? } ?></td>
</tr>
<tr>
<td>Can edit projects:</td>
<td><? if($user->Benefits->CanEditProjects){ ?>☑<? }else{ ?>☐<? } ?></td>
</tr>
<tr>
<td>Can manage projects:</td>
<td><? if($user->Benefits->CanManageProjects){ ?>☑<? }else{ ?>☐<? } ?></td>
</tr>
<tr>
<td>Can review projects:</td>
<td><? if($user->Benefits->CanReviewProjects){ ?>☑<? }else{ ?>☐<? } ?></td>
</tr> </tr>
<? } ?> <? } ?>
</tbody> </tbody>
@ -179,7 +195,7 @@ catch(Exceptions\SeeOtherException $ex){
<h2>Payments</h2> <h2>Payments</h2>
<? if(sizeof($user->Payments) == 0){ ?> <? if(sizeof($user->Payments) == 0){ ?>
<p>None.</p> <p class="empty-notice">None.</p>
<? }else{ ?> <? }else{ ?>
<p> <p>
<a href="https://fundraising.fracturedatlas.org/admin/general_support/donations?query=<?= urlencode($user->Email ?? '') ?>">View all payments at Fractured Atlas</a> <a href="https://fundraising.fracturedatlas.org/admin/general_support/donations?query=<?= urlencode($user->Email ?? '') ?>">View all payments at Fractured Atlas</a>

View file

@ -0,0 +1,62 @@
<?
try{
$user = User::GetByIdentifier(HttpInput::Str(GET, 'user-identifier'));
if(Session::$User === null){
throw new Exceptions\LoginRequiredException();
}
if(
!Session::$User->Benefits->CanManageProjects
&&
!Session::$User->Benefits->CanReviewProjects
&&
!Session::$User->Benefits->CanEditProjects
){
throw new Exceptions\InvalidPermissionsException();
}
$managingProjects = Project::GetAllByManagerUserId($user->UserId);
$reviewingProjects = Project::GetAllByReviewerUserId($user->UserId);
}
catch(Exceptions\UserNotFoundException){
Template::Emit404();
}
catch(Exceptions\LoginRequiredException){
Template::RedirectToLogin();
}
catch(Exceptions\InvalidPermissionsException){
Template::Emit403();
}
?><?= Template::Header(
[
'title' => 'Projects',
'canonicalUrl' => $user->Url,
'css' => ['/css/project.css'],
'description' => 'Ebook projects currently underway at Standard Ebooks.'
]
) ?>
<main>
<section>
<nav class="breadcrumbs"><a href="<?= $user->Url ?>"><?= Formatter::EscapeHtml($user->DisplayName) ?></a> →</nav>
<h1>Projects</h1>
<section id="managing">
<h2>Managing</h2>
<? if(sizeof($managingProjects) == 0){ ?>
<p class="empty-notice">None.</p>
<? }else{ ?>
<?= Template::ProjectsTable(['projects' => $managingProjects]) ?>
<? } ?>
</section>
<section id="reviewing">
<h2>Reviewing</h2>
<? if(sizeof($reviewingProjects) == 0){ ?>
<p class="empty-notice">None.</p>
<? }else{ ?>
<?= Template::ProjectsTable(['projects' => $reviewingProjects]) ?>
<? } ?>
</section>
</section>
</main>
<?= Template::Footer() ?>