mirror of
https://github.com/standardebooks/web.git
synced 2025-07-04 22:00:35 -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
4
config/sql/se/ProjectReminders.sql
Normal file
4
config/sql/se/ProjectReminders.sql
Normal file
|
@ -0,0 +1,4 @@
|
|||
CREATE TABLE IF NOT EXISTS `se`.`ProjectReminders` (
|
||||
`ProjectId` INT UNSIGNED NOT NULL,
|
||||
`Created` TIMESTAMP NOT NULL,
|
||||
`Type` ENUM('abandoned', 'stalled') NOT NULL);
|
|
@ -14,5 +14,6 @@ CREATE TABLE IF NOT EXISTS `Projects` (
|
|||
`ReviewerUserId` int(11) NOT NULL,
|
||||
`LastCommitTimestamp` DATETIME NULL DEFAULT NULL,
|
||||
`LastDiscussionTimestamp` DATETIME NULL DEFAULT NULL,
|
||||
`IsStatusAutomaticallyUpdated` tinyint(1) NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (`ProjectId`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
|
@ -9,13 +9,15 @@ 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.
|
||||
*/
|
||||
|
||||
/** @var array<Project> $projects */
|
||||
$projects = array_merge(
|
||||
Project::GetAllByStatus(Enums\ProjectStatusType::InProgress),
|
||||
Project::GetAllByStatus(Enums\ProjectStatusType::Stalled)
|
||||
);
|
||||
|
||||
$apiKey = trim(file_get_contents('/standardebooks.org/config/secrets/se-vcs-bot@api.github.com'));
|
||||
$oldestAllowedTimestamp = new DateTimeImmutable('30 days ago');
|
||||
$oldestStalledTimestamp = new DateTimeImmutable('60 days ago');
|
||||
$oldestAbandonedTimestamp = new DateTimeImmutable('90 days ago');
|
||||
|
||||
foreach($projects as $project){
|
||||
try{
|
||||
|
@ -33,59 +35,39 @@ foreach($projects as $project){
|
|||
Log::WriteErrorLogEntry($ex->getMessage());
|
||||
}
|
||||
|
||||
if(
|
||||
$project->Status == Enums\ProjectStatusType::InProgress
|
||||
&&
|
||||
(
|
||||
(
|
||||
$project->LastCommitTimestamp !== null
|
||||
&&
|
||||
$project->LastDiscussionTimestamp === null
|
||||
&&
|
||||
$project->LastCommitTimestamp < $oldestAllowedTimestamp
|
||||
)
|
||||
||
|
||||
(
|
||||
$project->LastCommitTimestamp !== null
|
||||
&&
|
||||
$project->LastDiscussionTimestamp !== null
|
||||
&&
|
||||
$project->LastCommitTimestamp < $oldestAllowedTimestamp
|
||||
&&
|
||||
$project->LastDiscussionTimestamp < $oldestAllowedTimestamp
|
||||
)
|
||||
)
|
||||
){
|
||||
// An active `Project` has stalled.
|
||||
$project->Status = Enums\ProjectStatusType::Stalled;
|
||||
}
|
||||
elseif(
|
||||
$project->Status == Enums\ProjectStatusType::Stalled
|
||||
&&
|
||||
(
|
||||
(
|
||||
$project->LastCommitTimestamp !== null
|
||||
&&
|
||||
$project->LastDiscussionTimestamp === null
|
||||
&&
|
||||
$project->LastCommitTimestamp >= $oldestAllowedTimestamp
|
||||
)
|
||||
||
|
||||
(
|
||||
$project->LastCommitTimestamp !== null
|
||||
&&
|
||||
$project->LastDiscussionTimestamp !== null
|
||||
&&
|
||||
(
|
||||
$project->LastCommitTimestamp >= $oldestAllowedTimestamp
|
||||
||
|
||||
$project->LastDiscussionTimestamp >= $oldestAllowedTimestamp
|
||||
)
|
||||
)
|
||||
)
|
||||
){
|
||||
// Revive previously-stalled `Project`s.
|
||||
$project->Status = Enums\ProjectStatusType::InProgress;
|
||||
if($project->IsStatusAutomaticallyUpdated){
|
||||
if(
|
||||
$project->Status == Enums\ProjectStatusType::InProgress
|
||||
&&
|
||||
$project->LastActivityTimestamp < $oldestStalledTimestamp
|
||||
){
|
||||
// An active `Project` has stalled.
|
||||
$project->Status = Enums\ProjectStatusType::Stalled;
|
||||
|
||||
// Send an email to the producer.
|
||||
$project->SendReminder(Enums\ProjectReminderType::Stalled);
|
||||
}
|
||||
elseif(
|
||||
$project->Status == Enums\ProjectStatusType::Stalled
|
||||
&&
|
||||
$project->LastActivityTimestamp >= $oldestStalledTimestamp
|
||||
){
|
||||
// Revive previously-stalled `Project`s.
|
||||
$project->Status = Enums\ProjectStatusType::InProgress;
|
||||
}
|
||||
elseif(
|
||||
$project->Status == Enums\ProjectStatusType::Stalled
|
||||
&&
|
||||
$project->LastActivityTimestamp < $oldestStalledTimestamp
|
||||
&&
|
||||
$project->GetReminder(Enums\ProjectReminderType::Stalled)?->Created < $oldestAbandonedTimestamp
|
||||
){
|
||||
// A stalled `Project` is now abandoned.
|
||||
$project->Status = Enums\ProjectStatusType::Abandoned;
|
||||
|
||||
// Send a notification to the producer.
|
||||
$project->SendReminder(Enums\ProjectReminderType::Abandoned);
|
||||
}
|
||||
}
|
||||
|
||||
$project->Save();
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
<div class="footer">
|
||||
<p>
|
||||
<a href="<?= SITE_URL ?>/donate">Donate</a> • <a href="<?= SITE_URL ?>/contribute">Get involved</a> • <a href="<?= SITE_URL ?>/feeds">Ebook feeds</a>
|
||||
</p>
|
||||
<?
|
||||
$includeLinks = $includeLinks ?? true;
|
||||
?>
|
||||
<div class="footer<? if(!$includeLinks){ ?> no-links<? } ?>">
|
||||
<? if($includeLinks){ ?>
|
||||
<p>
|
||||
<a href="<?= SITE_URL ?>/donate">Donate</a> • <a href="<?= SITE_URL ?>/contribute">Get involved</a> • <a href="<?= SITE_URL ?>/feeds">Ebook feeds</a>
|
||||
</p>
|
||||
<? } ?>
|
||||
<p>
|
||||
<a href="<?= SITE_URL ?>">
|
||||
<img src="https://standardebooks.org/images/logo-small.png" alt="The Standard Ebooks logo."/>
|
||||
</a>
|
||||
</p>
|
||||
<address>
|
||||
<p>Standard Ebooks L<sup>3</sup>C</p>
|
||||
<p>2027 W. Division St. Unit 106</p>
|
||||
<p>Chicago, IL 60622</p>
|
||||
</address>
|
||||
<? if($includeLinks){ ?>
|
||||
<address>
|
||||
<p>Standard Ebooks L<sup>3</sup>C</p>
|
||||
<p>2027 W. Division St. Unit 106</p>
|
||||
<p>Chicago, IL 60622</p>
|
||||
</address>
|
||||
<? } ?>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?
|
||||
$preheader = $preheader ?? null;
|
||||
$letterhead = $letterhead ?? false;
|
||||
$hasDataTable = $hasDataTable ?? false;
|
||||
?><!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
@ -38,30 +39,30 @@ $letterhead = $letterhead ?? false;
|
|||
}
|
||||
|
||||
<? if($letterhead){ ?>
|
||||
div.body.letterhead{
|
||||
background-image: url("https://standardebooks.org/images/logo-email.png");
|
||||
background-position: top 2em right 2em;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 210px 49px;
|
||||
padding-top: 5em;
|
||||
}
|
||||
div.body.letterhead{
|
||||
background-image: url("https://standardebooks.org/images/logo-email.png");
|
||||
background-position: top 2em center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 210px 49px;
|
||||
padding-top: 6em;
|
||||
}
|
||||
<? } ?>
|
||||
|
||||
<? if($preheader){ ?>
|
||||
.preheader{
|
||||
display: none !important;
|
||||
color: #ffffff;
|
||||
font-size: 1px;
|
||||
height: 0;
|
||||
line-height: 1px;
|
||||
mso-hide: all;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: -9999px;
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
}
|
||||
.preheader{
|
||||
display: none !important;
|
||||
color: #ffffff;
|
||||
font-size: 1px;
|
||||
height: 0;
|
||||
line-height: 1px;
|
||||
mso-hide: all;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: -9999px;
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
}
|
||||
<? } ?>
|
||||
|
||||
img.logo{
|
||||
|
@ -140,6 +141,11 @@ $letterhead = $letterhead ?? false;
|
|||
max-width: 55px;
|
||||
}
|
||||
|
||||
.footer.no-links{
|
||||
font-size: 1em;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
footer{
|
||||
margin-right: 4em;
|
||||
margin-top: 2em;
|
||||
|
@ -184,9 +190,17 @@ $letterhead = $letterhead ?? false;
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.letterhead{
|
||||
text-align: right;
|
||||
}
|
||||
<? if($hasDataTable){ ?>
|
||||
table.data-table td:first-child{
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
table.data-table td{
|
||||
padding: .25em;
|
||||
border: none;
|
||||
}
|
||||
<? } ?>
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
67
templates/EmailManagerNewProject.php
Normal file
67
templates/EmailManagerNewProject.php
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?
|
||||
/**
|
||||
* @var Project $project
|
||||
* @var string $role
|
||||
*/
|
||||
?><?= Template::EmailHeader(['hasDataTable' => true, 'letterhead' => true]) ?>
|
||||
<p>You’ve been assigned a new ebook project to <strong><?= $role ?></strong>:</p>
|
||||
<table class="data-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Title:</td>
|
||||
<td><a href="<?= SITE_URL ?><?= $project->Ebook->Url ?>"><?= Formatter::EscapeHtml($project->Ebook->Title) ?></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Producer:</td>
|
||||
<td>
|
||||
<? if($project->ProducerEmail !== null){ ?>
|
||||
<a href="mailto:<?= Formatter::EscapeHtml($project->ProducerEmail) ?>"><?= Formatter::EscapeHtml($project->ProducerName) ?></a>
|
||||
<? }elseif($project->DiscussionUrl !== null){ ?>
|
||||
<a href="<?= Formatter::EscapeHtml($project->DiscussionUrl) ?>"><?= Formatter::EscapeHtml($project->ProducerName) ?></a>
|
||||
<? }else{ ?>
|
||||
<?= Formatter::EscapeHtml($project->ProducerName) ?>
|
||||
<? } ?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Manager:</td>
|
||||
<td>
|
||||
<a href="<?= SITE_URL ?><?= $project->Manager->Url ?>/projects"><?= Formatter::EscapeHtml($project->Manager->DisplayName) ?></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Reviewer:</td>
|
||||
<td>
|
||||
<a href="<?= SITE_URL ?><?= $project->Reviewer->Url ?>/projects"><?= Formatter::EscapeHtml($project->Reviewer->DisplayName) ?></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Repository:</td>
|
||||
<td>
|
||||
<a href="<?= Formatter::EscapeHtml($project->VcsUrl) ?>">GitHub</a>
|
||||
</td>
|
||||
</tr>
|
||||
<? if($project->DiscussionUrl !== null){ ?>
|
||||
<tr>
|
||||
<td>Discussion:</td>
|
||||
<td>
|
||||
<a href="<?= Formatter::EscapeHtml($project->DiscussionUrl) ?>">Google Groups</a>
|
||||
</td>
|
||||
</tr>
|
||||
<? } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>If you’re unable to <?= $role ?> this ebook project, <a href="mailto:<?= EDITOR_IN_CHIEF_EMAIL_ADDRESS ?>">email the Editor-in-Chief</a> and we’ll reassign it.</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
<a href="<?= SITE_URL ?><?= $project->Manager->Url ?>/projects">See all of the ebook projects you’re currently assigned to.</a>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<a href="<?= SITE_URL ?>/projects">See all ebook projects.</a>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<?= Template::EmailFooter(['includeLinks' => false]) ?>
|
28
templates/EmailManagerNewProjectText.php
Normal file
28
templates/EmailManagerNewProjectText.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?
|
||||
/**
|
||||
* @var Project $project
|
||||
* @var string $role
|
||||
*/
|
||||
?>
|
||||
You’ve been assigned a new ebook project to **<?= $role ?>**:
|
||||
|
||||
- Title: [<?= Formatter::EscapeMarkdown($project->Ebook->Title) ?>](<?= Formatter::EscapeMarkdown(SITE_URL . $project->Ebook->Url) ?>)
|
||||
|
||||
- Producer: <? if($project->ProducerEmail !== null){ ?>[<?= Formatter::EscapeMarkdown($project->ProducerName) ?>](mailto:<?= Formatter::EscapeMarkdown($project->ProducerEmail) ?>)<? }elseif($project->DiscussionUrl !== null){ ?>[<?= Formatter::EscapeMarkdown($project->ProducerName) ?>](<?= Formatter::EscapeMarkdown($project->DiscussionUrl) ?>)<? }else{ ?><?= Formatter::EscapeMarkdown($project->ProducerName) ?><? } ?>
|
||||
|
||||
|
||||
- Manager: [<?= Formatter::EscapeMarkdown($project->Manager->DisplayName) ?>](<?= Formatter::EscapeMarkdown(SITE_URL . $project->Manager->Url . '/projects') ?>)
|
||||
|
||||
- Reviewer: [<?= Formatter::EscapeMarkdown($project->Reviewer->DisplayName) ?>](<?= Formatter::EscapeMarkdown(SITE_URL . $project->Reviewer->Url . '/projects') ?>)
|
||||
|
||||
- Repository: [GitHub](<?= Formatter::EscapeMarkdown($project->VcsUrl) ?>)
|
||||
|
||||
<? if($project->DiscussionUrl !== null){ ?>
|
||||
- Discussion: [Google Groups](<?= Formatter::EscapeMarkdown($project->DiscussionUrl) ?>)
|
||||
|
||||
<? } ?>
|
||||
If you’re unable to <?= $role ?> this ebook project, [email the Editor-in-Chief](mailto:<?= Formatter::EscapeMarkdown(EDITOR_IN_CHIEF_EMAIL_ADDRESS) ?>) and we’ll reassign it.
|
||||
|
||||
- [See all of the ebook projects you’re currently assigned to.](<?= SITE_URL ?><?= $project->Manager->Url ?>/projects)
|
||||
|
||||
- [See all ebook projects.](<?= SITE_URL ?>/projects)
|
11
templates/EmailProjectAbandoned.php
Normal file
11
templates/EmailProjectAbandoned.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Hi there, I haven't heard from you in a while, and your ebook's Github repo hasn't had any activity for a while either. So, I'm assuming production is either stalled or abandoned, and I'm putting your ebook back on our wanted list so someone else can work on it.</p>
|
||||
<p>If you're still actively making timely progress on your ebook, please let me know and I'm happy to put your ebook back in the "in production" list for as long as you need. Otherwise, there's nothing else you need to do.</p>
|
||||
<p>Have a good one!</p>
|
||||
</body>
|
||||
</html>
|
5
templates/EmailProjectAbandonedText.php
Normal file
5
templates/EmailProjectAbandonedText.php
Normal file
|
@ -0,0 +1,5 @@
|
|||
Hi there, I haven't heard from you in a while, and your ebook's Github repo hasn't had any activity for a while either. So, I'm assuming production is either stalled or abandoned, and I'm putting your ebook back on our wanted list so someone else can work on it.
|
||||
|
||||
If you're still actively making timely progress on your ebook, please let me know and I'm happy to put your ebook back in the "in production" list for as long as you need. Otherwise, there's nothing else you need to do.
|
||||
|
||||
Have a good one!
|
11
templates/EmailProjectStalled.php
Normal file
11
templates/EmailProjectStalled.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Hi there, just wanted to check in to see how your ebook production was going. I noticed there hasn't been much activity in the Github repo lately. Are you continuing to make progress?</p>
|
||||
<p>The toolset has had several updates recently. You can update it by running `pipx upgrade standardebooks`.</p>
|
||||
<p>Let me know how things are coming along!</p>
|
||||
</body>
|
||||
</html>
|
5
templates/EmailProjectStalledText.php
Normal file
5
templates/EmailProjectStalledText.php
Normal file
|
@ -0,0 +1,5 @@
|
|||
Hi there, just wanted to check in to see how your ebook production was going. I noticed there hasn't been much activity in the Github repo lately. Are you continuing to make progress?
|
||||
|
||||
The toolset has had several updates recently. You can update it by running `pipx upgrade standardebooks`.
|
||||
|
||||
Let me know how things are coming along!
|
|
@ -55,6 +55,16 @@ $reviewers = User::GetAllByCanReviewProjects();
|
|||
</span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>Automatically update status?</span>
|
||||
<input type="hidden" name="project-is-status-automatically-updated" value="false" />
|
||||
<input
|
||||
type="checkbox"
|
||||
name="project-is-status-automatically-updated"
|
||||
<? if($project->IsStatusAutomaticallyUpdated){ ?>checked="checked"<? } ?>
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>VCS URL</span>
|
||||
<input
|
||||
|
|
|
@ -16,7 +16,6 @@ $includeStatus = $includeStatus ?? true;
|
|||
<th scope="col">Producer</th>
|
||||
<th scope="col">Manager</th>
|
||||
<th scope="col">Reviewer</th>
|
||||
<th scope="col">Started</th>
|
||||
<th scope="col">Last activity</th>
|
||||
<? if($includeStatus){ ?>
|
||||
<th scope="col">Status</th>
|
||||
|
@ -43,13 +42,10 @@ $includeStatus = $includeStatus ?? true;
|
|||
<? } ?>
|
||||
</td>
|
||||
<td>
|
||||
<a href="<?= $project->ManagerUser->Url ?>/projects"><?= Formatter::EscapeHtml($project->ManagerUser->DisplayName) ?></a>
|
||||
<a href="<?= $project->Manager->Url ?>/projects"><?= Formatter::EscapeHtml($project->Manager->DisplayName) ?></a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="<?= $project->ReviewerUser->Url ?>/projects"><?= Formatter::EscapeHtml($project->ReviewerUser->DisplayName) ?></a>
|
||||
</td>
|
||||
<td>
|
||||
<?= $project->Started->format(Enums\DateTimeFormat::ShortDate->value) ?>
|
||||
<a href="<?= $project->Reviewer->Url ?>/projects"><?= Formatter::EscapeHtml($project->Reviewer->DisplayName) ?></a>
|
||||
</td>
|
||||
<td>
|
||||
<?= $project->LastActivityTimestamp->format(Enums\DateTimeFormat::ShortDate->value) ?>
|
||||
|
@ -60,7 +56,7 @@ $includeStatus = $includeStatus ?? true;
|
|||
</td>
|
||||
<? } ?>
|
||||
<td>
|
||||
<a href="<?= Formatter::EscapeHtml($project->VcsUrl) ?>">GitHub</a>
|
||||
<a href="<?= Formatter::EscapeHtml($project->VcsUrl) ?>">Respository</a>
|
||||
</td>
|
||||
<td>
|
||||
<? if($project->DiscussionUrl !== null){ ?>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue