mirror of
https://github.com/standardebooks/web.git
synced 2025-07-19 04:44:48 -04:00
Continue fleshing out project management system
This commit is contained in:
parent
657ecc68d4
commit
051e286a6d
19 changed files with 420 additions and 108 deletions
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
|
|
10
lib/Enums/ProjectReminderType.php
Normal file
10
lib/Enums/ProjectReminderType.php
Normal 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';
|
||||
}
|
|
@ -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.
|
||||
*/
|
||||
|
|
139
lib/Project.php
139
lib/Project.php
|
@ -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
26
lib/ProjectReminder.php
Normal 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]);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue