Continue fleshing out project management system

This commit is contained in:
Alex Cabal 2024-12-16 14:56:10 -06:00
parent 657ecc68d4
commit 051e286a6d
19 changed files with 420 additions and 108 deletions

View file

@ -81,6 +81,7 @@ const POSTMARK_WEBHOOK_LOG_FILE_PATH = '/var/log/local/webhooks-postmark.log'; /
const ZOHO_WEBHOOK_LOG_FILE_PATH = '/var/log/local/webhooks-zoho.log'; // Must be writable by `www-data` Unix user.
const DONATIONS_LOG_FILE_PATH = '/var/log/local/donations.log'; // Must be writable by `www-data` Unix user.
const ARTWORK_UPLOADS_LOG_FILE_PATH = '/var/log/local/artwork-uploads.log'; // Must be writable by `www-data` Unix user.
const EMAIL_LOG_FILE_PATH = '/var/log/local/standardebooks.org-email.log'; // Must be writable by `www-data` Unix user.
define('PD_YEAR', intval(PD_NOW->format('Y')) - 96);
define('PD_STRING', 'January 1, ' . (PD_YEAR + 1));

View file

@ -73,10 +73,11 @@ class Email{
}
if(SITE_STATUS == SITE_STATUS_DEV){
Log::WriteErrorLogEntry('Sending mail to ' . $this->To . ' from ' . $this->From);
Log::WriteErrorLogEntry('Subject: ' . $this->Subject);
Log::WriteErrorLogEntry($this->Body);
Log::WriteErrorLogEntry($this->TextBody);
$log = new Log(EMAIL_LOG_FILE_PATH);
$log->Write('Sending mail to ' . $this->To . ' from ' . $this->From);
$log->Write('Subject: ' . $this->Subject);
$log->Write($this->Body);
$log->Write($this->TextBody);
}
else{
$phpMailer->Send();

View file

@ -0,0 +1,10 @@
<?
namespace Enums;
enum ProjectReminderType: string{
/** An email to nudge the producer on a stalled project. */
case Stalled = 'stalled';
/** An email to notify the producer we are considering their project abandoned. */
case Abandoned = 'abandoned';
}

View file

@ -82,6 +82,20 @@ class Formatter{
return htmlspecialchars(trim($text ?? ''), ENT_QUOTES|ENT_XML1, 'utf-8');
}
/**
* Escape a string for use in Markdown.
*/
public static function EscapeMarkdown(?string $text): string{
if($text === null){
return '';
}
return str_replace(
['\\', '-', '#', '*', '+', '`', '.', '[', ']', '(', ')', '!', '<', '>', '_', '{', '}', '|'],
['\\\\', '\-', '\#', '\*', '\+', '\`', '\.', '\[', '\]', '\(', '\)', '\!', '\<', '\>', '\_', '\{', '\}', '\|'],
$text);
}
/**
* Convert a string of Markdown into HTML.
*/

View file

@ -12,10 +12,11 @@ use Safe\DateTimeImmutable;
/**
* @property Ebook $Ebook
* @property User $ManagerUser
* @property User $ReviewerUser
* @property User $Manager
* @property User $Reviewer
* @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.
* @property array<ProjectReminder> $Reminders
*/
class Project{
use Traits\Accessor;
@ -36,12 +37,15 @@ class Project{
public int $ReviewerUserId;
public ?DateTimeImmutable $LastCommitTimestamp = null;
public ?DateTimeImmutable $LastDiscussionTimestamp = null;
public bool $IsStatusAutomaticallyUpdated = true;
protected Ebook $_Ebook;
protected User $_ManagerUser;
protected User $_ReviewerUser;
protected User $_Manager;
protected User $_Reviewer;
protected string $_Url;
protected DateTimeImmutable $_LastActivityTimestamp;
/** @var array<ProjectReminder> $_Reminders */
protected array $_Reminders;
// *******
@ -72,6 +76,39 @@ class Project{
return $this->_LastActivityTimestamp;
}
/**
* @throws Exceptions\UserNotFoundException If the `User` can't be found.
*/
protected function GetManager(): User{
if(!isset($this->_Manager)){
$this->_Manager = User::Get($this->ManagerUserId);
}
return $this->_Manager;
}
/**
* @throws Exceptions\UserNotFoundException If the `User` can't be found.
*/
protected function GetReviewer(): User{
if(!isset($this->_Reviewer)){
$this->_Reviewer = User::Get($this->ReviewerUserId);
}
return $this->_Reviewer;
}
/**
* @return array<ProjectReminder>
*/
protected function GetReminders(): array{
if(!isset($this->_Reminders)){
$this->_Reminders = Db::Query('SELECT * from ProjectReminders where ProjectId = ? order by Created asc', [$this->ProjectId], ProjectReminder::class);
}
return $this->_Reminders;
}
// *******
// METHODS
@ -134,7 +171,7 @@ class Project{
}
else{
try{
$this->_ManagerUser = User::Get($this->ManagerUserId);
$this->_Manager = User::Get($this->ManagerUserId);
}
catch(Exceptions\UserNotFoundException){
$error->Add(new Exceptions\UserNotFoundException('Manager user not found.'));
@ -146,7 +183,7 @@ class Project{
}
else{
try{
$this->_ReviewerUser = User::Get($this->ReviewerUserId);
$this->_Reviewer = User::Get($this->ReviewerUserId);
}
catch(Exceptions\UserNotFoundException){
$error->Add(new Exceptions\UserNotFoundException('Reviewer user not found.'));
@ -215,7 +252,8 @@ class Project{
ManagerUserId,
ReviewerUserId,
LastCommitTimestamp,
LastDiscussionTimestamp
LastDiscussionTimestamp,
IsStatusAutomaticallyUpdated
)
values
(
@ -232,11 +270,49 @@ class Project{
?,
?,
?,
?,
?
)
', [$this->EbookId, $this->Status, $this->ProducerName, $this->ProducerEmail, $this->DiscussionUrl, $this->VcsUrl, NOW, NOW, $this->Started, $this->Ended, $this->ManagerUserId, $this->ReviewerUserId, $this->LastCommitTimestamp, $this->LastDiscussionTimestamp]);
', [$this->EbookId, $this->Status, $this->ProducerName, $this->ProducerEmail, $this->DiscussionUrl, $this->VcsUrl, NOW, NOW, $this->Started, $this->Ended, $this->ManagerUserId, $this->ReviewerUserId, $this->LastCommitTimestamp, $this->LastDiscussionTimestamp, $this->IsStatusAutomaticallyUpdated]);
$this->ProjectId = Db::GetLastInsertedId();
// Notify the manager.
if($this->Status == Enums\ProjectStatusType::InProgress){
if($this->ManagerUserId == $this->ReviewerUserId){
if($this->Manager->Email !== null){
$em = new Email();
$em->From = ADMIN_EMAIL_ADDRESS;
$em->To = $this->Manager->Email;
$em->Subject = 'New ebook project to manage and review';
$em->Body = Template::EmailManagerNewProject(['project' => $this, 'role' => 'manage and review']);
$em->TextBody = Template::EmailManagerNewProjectText(['project' => $this, 'role' => 'manage and review']);
$em->Send();
}
}
else{
if($this->Manager->Email !== null){
$em = new Email();
$em->From = ADMIN_EMAIL_ADDRESS;
$em->To = $this->Manager->Email;
$em->Subject = 'New ebook project to manage';
$em->Body = Template::EmailManagerNewProject(['project' => $this, 'role' => 'manage']);
$em->TextBody = Template::EmailManagerNewProjectText(['project' => $this, 'role' => 'manage']);
$em->Send();
}
// Notify the reviewer.
if($this->Reviewer->Email !== null){
$em = new Email();
$em->From = ADMIN_EMAIL_ADDRESS;
$em->To = $this->Reviewer->Email;
$em->Subject = 'New ebook project to review';
$em->Body = Template::EmailManagerNewProject(['project' => $this, 'role' => 'review']);
$em->TextBody = Template::EmailManagerNewProjectText(['project' => $this, 'role' => 'review']);
$em->Send();
}
}
}
}
/**
@ -259,10 +335,11 @@ class Project{
ManagerUserId = ?,
ReviewerUserId = ?,
LastCommitTimestamp = ?,
LastDiscussionTimestamp = ?
LastDiscussionTimestamp = ?,
IsStatusAutomaticallyUpdated = ?
where
ProjectId = ?
', [$this->Status, $this->ProducerName, $this->ProducerEmail, $this->DiscussionUrl, $this->VcsUrl, $this->Started, $this->Ended, $this->ManagerUserId, $this->ReviewerUserId, $this->LastCommitTimestamp, $this->LastDiscussionTimestamp, $this->ProjectId]);
', [$this->Status, $this->ProducerName, $this->ProducerEmail, $this->DiscussionUrl, $this->VcsUrl, $this->Started, $this->Ended, $this->ManagerUserId, $this->ReviewerUserId, $this->LastCommitTimestamp, $this->LastDiscussionTimestamp, $this->IsStatusAutomaticallyUpdated, $this->ProjectId]);
if($this->Status == Enums\ProjectStatusType::Abandoned){
Db::Query('
@ -286,6 +363,7 @@ class Project{
$this->PropertyFromHttp('Ended');
$this->PropertyFromHttp('ManagerUserId');
$this->PropertyFromHttp('ReviewerUserId');
$this->PropertyFromHttp('IsStatusAutomaticallyUpdated');
}
/**
@ -396,6 +474,47 @@ class Project{
}
}
public function GetReminder(Enums\ProjectReminderType $type): ?ProjectReminder{
foreach($this->Reminders as $reminder){
if($reminder->Type == $type){
return $reminder;
}
}
return null;
}
public function SendReminder(Enums\ProjectReminderType $type): void{
if($this->ProducerEmail === null || $this->GetReminder($type) !== null){
return;
}
$reminder = new ProjectReminder();
$reminder->ProjectId = $this->ProjectId;
$reminder->Type = $type;
$reminder->Create();
$em = new Email();
$em->From = EDITOR_IN_CHIEF_EMAIL_ADDRESS;
$em->FromName = EDITOR_IN_CHIEF_NAME;
$em->To = $this->ProducerEmail;
$em->Subject = 'Your Standard Ebooks ebook';
switch($type){
case Enums\ProjectReminderType::Stalled:
$em->Body = Template::EmailProjectStalled();
$em->TextBody = Template::EmailProjectStalledText();
break;
case Enums\ProjectReminderType::Abandoned:
$em->Body = Template::EmailProjectAbandoned();
$em->TextBody = Template::EmailProjectAbandonedText();
break;
}
$em->Send();
}
// ***********
// ORM METHODS

26
lib/ProjectReminder.php Normal file
View file

@ -0,0 +1,26 @@
<?
use Safe\DateTimeImmutable;
class ProjectReminder{
public int $ProjectId;
public DateTimeImmutable $Created;
public Enums\ProjectReminderType $Type;
public function Create(): void{
$this->Created = NOW;
Db::Query('
INSERT
into ProjectReminders
(
ProjectId,
Created,
Type
)
values(
?,
?,
?
)
', [$this->ProjectId, $this->Created, $this->Type]);
}
}