diff --git a/.gitignore b/.gitignore index e43b0f9..dde2f7d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .DS_Store +.idea/* \ No newline at end of file diff --git a/code/extensions/ContentReviewCMSExtension.php b/code/extensions/ContentReviewCMSExtension.php index c22b565..444af70 100644 --- a/code/extensions/ContentReviewCMSExtension.php +++ b/code/extensions/ContentReviewCMSExtension.php @@ -28,12 +28,10 @@ class ContentReviewCMSExtension extends LeftAndMainExtension $page = $this->findRecord($data); if (!$page->canEdit()) { return Security::permissionFailure($this->owner); - } - + } $notes = (!empty($data["ReviewNotes"]) ? $data["ReviewNotes"] : _t("ContentReview.NOCOMMENTS", "(no comments)")); - $page->addReviewNote(Member::currentUser(), $notes); + $page->addReviewNote(Member::currentUser(), $notes, $page->ReviewInfo); $page->advanceReviewDate(); - $this->owner->getResponse()->addHeader("X-Status", _t("ContentReview.REVIEWSUCCESSFUL", "Content reviewed successfully")); return $this->owner->redirectBack(); } diff --git a/code/extensions/ContentReviewDefaultSettings.php b/code/extensions/ContentReviewDefaultSettings.php index 5e958fa..936fa55 100644 --- a/code/extensions/ContentReviewDefaultSettings.php +++ b/code/extensions/ContentReviewDefaultSettings.php @@ -17,7 +17,13 @@ class ContentReviewDefaultSettings extends DataExtension 'ReviewPeriodDays' => 'Int', 'ReviewFrom' => 'Varchar(255)', 'ReviewSubject' => 'Varchar(255)', + 'ReviewSubjectReminder' => 'Varchar(255)', 'ReviewBody' => 'HTMLText', + 'ReviewBodyFirstReminder' => 'HTMLText', + 'ReviewBodySecondReminder' => 'HTMLText', + 'ReviewReminderEmail' => 'Text', + 'FirstReviewDaysBefore' => 'Int', + 'SecondReviewDaysBefore' => 'Int' ); /** @@ -26,8 +32,14 @@ class ContentReviewDefaultSettings extends DataExtension * @var array */ private static $defaults = array( + 'ReviewSubjectReminder' => 'Page(s) are approaching content review date', 'ReviewSubject' => 'Page(s) are due for content review', + 'ReviewBodyFirstReminder' => '

Page(s) 1 month from review

There are $FirstReminderPagesCount pages that are due for review by you 1 month from today.

', + 'ReviewBodySecondReminder' => '

Page(s) 1 week from from review

There are $SecondReminderPagesCount pages that are due for review by you 1 week from today.

', 'ReviewBody' => '

Page(s) due for review

There are $PagesCount pages that are due for review today by you.

', + 'ReviewReminderEmail' => 'govt.nz@dia.govt.nz', + 'FirstReviewDaysBefore' => '30', + 'SecondReviewDaysBefore' => '7' ); /** @@ -50,6 +62,7 @@ class ContentReviewDefaultSettings extends DataExtension * @var string */ private static $content_review_template = 'ContentReviewEmail'; + private static $content_review_reminder_template = 'ContentReviewReminderEmail'; /** * @return string @@ -112,6 +125,7 @@ class ContentReviewDefaultSettings extends DataExtension $fields->addFieldToTab('Root.ContentReview', $reviewFrequency); + $users = Permission::get_members_by_permission(array( 'CMS_ACCESS_CMSMain', 'ADMIN', @@ -143,14 +157,31 @@ class ContentReviewDefaultSettings extends DataExtension $fields->addFieldToTab('Root.ContentReview', $groupField); + $FirstReviewDaysBefore = NumericField::create( + 'FirstReviewDaysBefore', + _t('ContentReview.FIRSTREVIEWDAYSBEFORE', 'First review reminder # days before final review') + ); + + $SecondReviewDaysBefore = NumericField::create( + 'SecondReviewDaysBefore', + _t('ContentReview.SECONDREVIEWDAYSBEFORE', 'Second review reminder # days before final review') + ); + // Email content $fields->addFieldsToTab( 'Root.ContentReview', array( TextField::create('ReviewFrom', _t('ContentReview.EMAILFROM', 'From email address')) ->setRightTitle(_t('Review.EMAILFROM_RIGHTTITLE', 'e.g: do-not-reply@site.com')), - TextField::create('ReviewSubject', _t('ContentReview.EMAILSUBJECT', 'Subject line')), - TextAreaField::create('ReviewBody', _t('ContentReview.EMAILTEMPLATE', 'Email template')), + $FirstReviewDaysBefore, + $SecondReviewDaysBefore, + TextField::create('ReviewReminderEmail','Review reminder email address') + ->setRightTitle('e.g: review.reminders@site.com'), + TextField::create('ReviewSubjectReminder', _t('ContentReview.EMAILSUBJECTREMINDER', 'Subject line - reminder')), + TextField::create('ReviewSubject', _t('ContentReview.EMAILSUBJECT', 'Subject line - Review due')), + TextAreaField::create('ReviewBodyFirstReminder', _t('ContentReview.EMAILTEMPLATEFIRSTREMINDER', 'Email body - First reminder')), + TextAreaField::create('ReviewBodySecondReminder', _t('ContentReview.EMAILTEMPLATESECONDREMINDER', 'Email body - Second reminder')), + TextAreaField::create('ReviewBody', _t('ContentReview.EMAILTEMPLATE', 'Email body - Review due')), LiteralField::create('TemplateHelp', $this->owner->renderWith('ContentReviewAdminHelp')), ) ); diff --git a/code/extensions/SiteTreeContentReview.php b/code/extensions/SiteTreeContentReview.php index 381c369..4f0b53d 100644 --- a/code/extensions/SiteTreeContentReview.php +++ b/code/extensions/SiteTreeContentReview.php @@ -25,6 +25,7 @@ class SiteTreeContentReview extends DataExtension implements PermissionProvider "NextReviewDate" => "Date", "LastEditedByName" => "Varchar(255)", "OwnerNames" => "Varchar(255)", + "ReviewInfo" => "Text" ); /** @@ -126,13 +127,22 @@ class SiteTreeContentReview extends DataExtension implements PermissionProvider "" ); + if (strlen($this->owner->ReviewInfo) > 0) { + $reviewInfo = LiteralField::create( + "ReviewContentInfo", + "

" . $this->owner->ReviewInfo . "

" + ); + } else { + $reviewInfo = ''; + } + $ReviewNotes = LiteralField::create("ReviewNotes", ""); $quickReviewAction = FormAction::create("savereview", _t("ContentReview.MARKREVIEWED", "Mark as reviewed")) ->setAttribute("data-icon", "pencil") ->setAttribute("data-text-alternate", _t("ContentReview.MARKREVIEWED", "Mark as reviewed")); - $allFields = CompositeField::create($reviewTitle, $ReviewNotes, $quickReviewAction) + $allFields = CompositeField::create($reviewTitle, $reviewInfo, $ReviewNotes, $quickReviewAction) ->addExtraClass('review-notes field'); $reviewTab = Tab::create('ReviewContent', $allFields); @@ -383,6 +393,8 @@ class SiteTreeContentReview extends DataExtension implements PermissionProvider ->addExtraClass('custom-setting') ->setDescription(_t("ContentReview.REVIEWFREQUENCYDESCRIPTION", "The review date will be set to this far in the future whenever the page is published")); + $reviewInfoField = TextareaField::create("ReviewInfo", _t("ContentReview.REVIEWINFO", "Review information")); + $notesField = GridField::create("ReviewNotes", "Review Notes", $this->owner->ReviewLogs(), GridFieldConfig_RecordEditor::create()); $fields->addFieldsToTab("Root.ContentReview", array( @@ -395,6 +407,7 @@ class SiteTreeContentReview extends DataExtension implements PermissionProvider $reviewFrequency )->addExtraClass("review-settings"), ReadonlyField::create("ROContentOwners", _t("ContentReview.CONTENTOWNERS", "Content Owners"), $this->getOwnerNames()), + $reviewInfoField, $notesField, )); } @@ -405,11 +418,14 @@ class SiteTreeContentReview extends DataExtension implements PermissionProvider * @param Member $reviewer * @param string $message */ - public function addReviewNote(Member $reviewer, $message) + public function addReviewNote(Member $reviewer, $message, $reviewInfo = null) { $reviewLog = ContentReviewLog::create(); $reviewLog->Note = $message; $reviewLog->ReviewerID = $reviewer->ID; + if ($reviewInfo) { + $reviewLog->ReviewInfo = $reviewInfo; + } $this->owner->ReviewLogs()->add($reviewLog); } @@ -425,16 +441,58 @@ class SiteTreeContentReview extends DataExtension implements PermissionProvider $nextDate = false; $options = $this->getOptions(); - if ($options && $options->ReviewPeriodDays) { + if ($options && $options->ReviewPeriodDays > 0) { $nextDate = date('Y-m-d', strtotime('+ ' . $options->ReviewPeriodDays . ' days', SS_Datetime::now()->format('U'))); $this->owner->NextReviewDate = $nextDate; $this->owner->write(); + } else { + $this->owner->NextReviewDate = null; + $this->owner->write(); } return (bool) $nextDate; } + + public function canRemind(Member $member = null) { + if (!$this->owner->obj("NextReviewDate")->exists()) { + return false; + } + // If today is not the date of the first reminder, return false + $config = SiteConfig::current_site_config(); + $firstReview = $config->FirstReviewDaysBefore; + $now = SS_Datetime::now(); + $notifyDate1 = date('Y-m-d', strtotime($this->owner->NextReviewDate . ' -' . $firstReview . ' days')); + + // If today is not the first reminder date + if (!$notifyDate1 == $now->URLDate()) { + return false; + } + + $options = $this->getOptions(); + + if (!$options) { + return false; + } elseif ($options->OwnerGroups()->count() == 0 && $options->OwnerUsers()->count() == 0) { + return false; + } + + if (!$member) { + return true; + } + + if ($member->inGroups($options->OwnerGroups())) { + return true; + } + + if ($options->OwnerUsers()->find("ID", $member->ID)) { + return true; + } + + return false; + } + /** * Check if a review is due by a member for this owner. * @@ -454,8 +512,10 @@ class SiteTreeContentReview extends DataExtension implements PermissionProvider $options = $this->getOptions(); - if ($options->OwnerGroups()->count() == 0 && $options->OwnerUsers()->count() == 0) { + if (!$options) { return false; + } elseif ($options->OwnerGroups()->count() == 0 && $options->OwnerUsers()->count() == 0) { + return false; } if (!$member) { diff --git a/code/models/ContentReviewLog.php b/code/models/ContentReviewLog.php index 4c890c8..d6caeb6 100644 --- a/code/models/ContentReviewLog.php +++ b/code/models/ContentReviewLog.php @@ -7,6 +7,7 @@ class ContentReviewLog extends DataObject */ private static $db = array( "Note" => "Text", + "ReviewInfo" => "Text" ); /** @@ -21,9 +22,10 @@ class ContentReviewLog extends DataObject * @var array */ private static $summary_fields = array( - "Note" => array("title" => "Note"), - "Created" => array("title" => "Reviewed at"), - "Reviewer.Title" => array("title" => "Reviewed by"), + "ReviewInfo" => array("title" => "Review Information"), + "Note" => array("title" => "Note"), + "Created" => array("title" => "Reviewed at"), + "Reviewer.Title" => array("title" => "Reviewed by"), ); /** diff --git a/code/tasks/ContentReviewEmails.php b/code/tasks/ContentReviewEmails.php index 11fb08f..7b4bded 100644 --- a/code/tasks/ContentReviewEmails.php +++ b/code/tasks/ContentReviewEmails.php @@ -12,18 +12,50 @@ class ContentReviewEmails extends BuildTask { $compatibility = ContentReviewCompatability::start(); + $now = SS_Datetime::now(); + // First grab all the pages with a custom setting $pages = Page::get() - ->filter('NextReviewDate:LessThanOrEqual', SS_Datetime::now()->URLDate()); + ->filter('NextReviewDate:LessThanOrEqual', $now->URLDate()); - $overduePages = $this->getOverduePagesForOwners($pages); - // Lets send one email to one owner with all the pages in there instead of no of pages - // of emails. - foreach ($overduePages as $memberID => $pages) { - $this->notifyOwner($memberID, $pages); + + // Calculate whether today is the date a First or Second review should occur + $config = SiteConfig::current_site_config(); + $firstReview = $config->FirstReviewDaysBefore; + $secondReview = $config->SecondReviewDaysBefore; + // Subtract the number of days prior to the review, from the current date + + // Get all pages where the NextReviewDate is still in the future + $pendingPages = Page::get()->filter('NextReviewDate:GreaterThan', $now->URLDate()); + + // for each of these pages, check if today is the date the First or Second + // reminder should be sent, and if so, add it to the appropriate ArrayList + $firstReminderPages = new ArrayList(); + $secondReminderPages = new ArrayList(); + + foreach ($pendingPages as $page) { + $notifyDate1 = date('Y-m-d', strtotime($page->NextReviewDate . ' -' . $firstReview . ' days')); + $notifyDate2 = date('Y-m-d', strtotime($page->NextReviewDate . ' -' . $secondReview . ' days')); + + if ($notifyDate1 == $now->URLDate()) { + $firstReminderPages->push($page); + } + if ($notifyDate2 == $now->URLDate()) { + $secondReminderPages->push($page); + } } + $overduePages = $this->getNotifiablePagesForOwners($pages); + + // Send one email to one owner with all the pages in there instead of no of pages of emails. + foreach ($overduePages as $memberID => $pages) { + $this->notifyOwner($memberID, $pages, "due"); + } + + // Send an email to the generic address with any first or second reminders + $this->notifyTeam($firstReminderPages, $secondReminderPages); + ContentReviewCompatability::done($compatibility); } @@ -32,15 +64,16 @@ class ContentReviewEmails extends BuildTask * * @return array */ - protected function getOverduePagesForOwners(SS_list $pages) + protected function getNotifiablePagesForOwners(SS_list $pages) { $overduePages = array(); foreach ($pages as $page) { - if (!$page->canBeReviewedBy()) { + + if (!$page->canRemind()) { continue; } - + $option = $page->getOptions(); foreach ($option->ContentReviewOwners() as $owner) { @@ -54,26 +87,66 @@ class ContentReviewEmails extends BuildTask return $overduePages; } + + /** + * Send an email to the configured team indicating which notices are at 'first reminder' or 'second reminder' status + * The 'days before due' value for each of these is configurable in settings. + * ie. a value of 30 for the 'first reminder' setting means a page with a review date exactly 30 days from due, will + * be present in the email sent to the configured address. + */ + protected function notifyTeam($firstReminderPages, $secondReminderPages) { + // Prepare variables + $siteConfig = SiteConfig::current_site_config(); + $templateVariables1 = $this->getTemplateVariables('', $siteConfig, $firstReminderPages, 'reminder1'); + $templateVariables2 = $this->getTemplateVariables('', $siteConfig, $secondReminderPages, 'reminder2'); + + // Build email + $email = new Email(); + $email->setTo($siteConfig->ReviewReminderEmail); + $email->setFrom($siteConfig->ReviewFrom); + + $subject = $siteConfig->ReviewSubjectReminder; + $email->setSubject($subject); + + // Get user-editable body + $bodyFirstReminder = $this->getEmailBody($siteConfig, $templateVariables1, 'reminder1'); + $bodySecondReminder = $this->getEmailBody($siteConfig, $templateVariables2, 'reminder2'); + + // Populate mail body with fixed template + $email->setTemplate($siteConfig->config()->content_review_reminder_template); + $email->populateTemplate($templateVariables1, $templateVariables2); + $email->populateTemplate(array( + 'EmailBodyFirstReminder' => $bodyFirstReminder, + 'EmailBodySecondReminder' => $bodySecondReminder, + 'FirstReminderPages' => $firstReminderPages, + 'SecondReminderPages' => $secondReminderPages, + )); + + $email->send(); + } /** * @param int $ownerID * @param array|SS_List $pages + * @param string $type */ - protected function notifyOwner($ownerID, SS_List $pages) + protected function notifyOwner($ownerID, SS_List $pages, $type) { // Prepare variables $siteConfig = SiteConfig::current_site_config(); $owner = Member::get()->byID($ownerID); - $templateVariables = $this->getTemplateVariables($owner, $siteConfig, $pages); + $templateVariables = $this->getTemplateVariables($owner, $siteConfig, $pages, $type); // Build email $email = new Email(); $email->setTo($owner->Email); $email->setFrom($siteConfig->ReviewFrom); - $email->setSubject($siteConfig->ReviewSubject); + + $subject = $siteConfig->ReviewSubject; + $email->setSubject($subject); // Get user-editable body - $body = $this->getEmailBody($siteConfig, $templateVariables); + $body = $this->getEmailBody($siteConfig, $templateVariables, $type); // Populate mail body with fixed template $email->setTemplate($siteConfig->config()->content_review_template); @@ -83,6 +156,7 @@ class ContentReviewEmails extends BuildTask 'Recipient' => $owner, 'Pages' => $pages, )); + $email->send(); } @@ -94,9 +168,18 @@ class ContentReviewEmails extends BuildTask * * @return HTMLText */ - protected function getEmailBody($config, $variables) + protected function getEmailBody($config, $variables, $type) { - $template = SSViewer::fromString($config->ReviewBody); + if ($type == "reminder1") { + $template = SSViewer::fromString($config->ReviewBodyFirstReminder); + } + if ($type == "reminder2") { + $template = SSViewer::fromString($config->ReviewBodySecondReminder); + } + if ($type == "due") { + $template = SSViewer::fromString($config->ReviewBody); + } + $value = $template->process(new ArrayData($variables)); // Cast to HTML @@ -112,18 +195,33 @@ class ContentReviewEmails extends BuildTask * @param Member $recipient * @param SiteConfig $config * @param SS_List $pages + * @param string $type * * @return array */ - protected function getTemplateVariables($recipient, $config, $pages) + protected function getTemplateVariables($recipient = null, $config, $pages) { - return array( - 'Subject' => $config->ReviewSubject, - 'PagesCount' => $pages->count(), - 'FromEmail' => $config->ReviewFrom, - 'ToFirstName' => $recipient->FirstName, - 'ToSurname' => $recipient->Surname, - 'ToEmail' => $recipient->Email, - ); + if ($recipient != null) { + return array( + 'Subject' => $config->ReviewSubject, + 'PagesCount' => $pages->count(), + 'FromEmail' => $config->ReviewFrom, + 'ToFirstName' => $recipient->FirstName, + 'ToSurname' => $recipient->Surname, + 'ToEmail' => $recipient->Email + ); + } else { + return array( + 'Subject' => $config->ReviewSubjectReminder, + 'FirstReminderPagesCount' => $pages->count(), + 'SecondReminderPagesCount' => $pages->count(), + 'FromEmail' => $config->ReviewFrom, + 'ToEmail' => $config->ReviewReminderEmail + ); + + } + + + } } diff --git a/css/contentreview.css b/css/contentreview.css index 4d44bba..b6bc585 100644 --- a/css/contentreview.css +++ b/css/contentreview.css @@ -11,6 +11,13 @@ margin: 0 0 4px 10px; } +.cms .ss-ui-action-tabset.action-menus.ss-tabset .ui-tabs-panel .review-notes .quick-info { + padding: 6px; + margin: 4px 8px; + border: 1px solid #b3b3b3; + border-radius: 4px; +} + .cms .ss-ui-action-tabset.action-menus.ss-tabset .ui-tabs-panel .review-notes .cms-sitetree-information p.meta-info { color: #f46b00; font-weight: bold; diff --git a/lang/en.yml b/lang/en.yml index b157a98..c616f8f 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -10,8 +10,12 @@ en: DEFAULTSETTINGSHELP: 'These content review settings will apply to all pages that does not have specific Content Review schedule.' DISABLE: 'Disable content review' EMAILFROM: 'From email address' - EMAILSUBJECT: 'Subject line' - EMAILTEMPLATE: 'Email template' + EMAILSUBJECTREMINDER: 'Subject line - Reminder' + EMAILSUBJECT: 'Subject line - Review due' + EMAILTEMPLATEFIRSTREMINDER: 'Email body - First reminder' + EMAILTEMPLATESECONDREMINDER: 'Email body - Second reminder' + EMAILTEMPLATE: 'Email body - Review due' + FIRSTREVIEWDAYSBEFORE: 'First review reminder # days before final review' INHERIT: 'Inherit from parent page' MARKREVIEWED: 'Mark as reviewed' NEXTREVIEWDATADESCRIPTION: 'Leave blank for no review' @@ -25,9 +29,11 @@ en: REVIEWFREQUENCY: 'Review frequency' REVIEWFREQUENCYDESCRIPTION: 'The review date will be set to this far in the future whenever the page is published' REVIEWHEADER: 'Content review' + REVIEWINFO: 'Review information' REVIEWNOTES: 'Review notes' REVIEWSUCCESSFUL: 'Content reviewed successfully' SAVE: Save + SECONDREVIEWDAYSBEFORE: 'Second review reminder # days before final review' SETTINGSFROM: 'Options are' ContentReviewEmails: REVIEWPAGELINK: 'Review the page in the CMS' diff --git a/readme.md b/readme.md index a797eda..2f9ac9e 100644 --- a/readme.md +++ b/readme.md @@ -6,6 +6,20 @@ [![License](http://img.shields.io/packagist/l/silverstripe/contentreview.svg?style=flat-square)](license.md) ![helpfulrobot](https://helpfulrobot.io/silverstripe/contentreview/badge) +**Note:** _Govt.nz customisations to this module are as follows_ + +Prior to the due date, the content review task will send an email to the address configured in the settings. + +This email contains two lists: +- a list of pages whose review date is exactly 1 month in the future. +- another list of pages whose review date is exactly 1 week in the future. + +Both these times (1 month and 1 week) are defaults only, and can be configured in the settings area. + +The email body for these reminder emails is also configurable in the settings. + +--- + This module helps keep your website content accurate and up-to-date, which keeps your users happy. It does so by sending reviewers reminder emails to go in and check the content. For a reviewer this diff --git a/templates/ContentReviewAdminHelp.ss b/templates/ContentReviewAdminHelp.ss index 243a541..5682ac8 100644 --- a/templates/ContentReviewAdminHelp.ss +++ b/templates/ContentReviewAdminHelp.ss @@ -1,10 +1,22 @@ -

This is a list of dynamic variables that you can use in the email templates.

+

This is a list of dynamic variables that you can use in the First and Second reminder email templates.

+ +
+ +

This is a list of dynamic variables that you can use in the Due email template.

+ + diff --git a/templates/ContentReviewReminderEmail.ss b/templates/ContentReviewReminderEmail.ss new file mode 100644 index 0000000..fd4695a --- /dev/null +++ b/templates/ContentReviewReminderEmail.ss @@ -0,0 +1,41 @@ + + + + + + + + + + <% loop $FirstReminderPages %> + + + + + <% end_loop %> + +
+ $EmailBodyFirstReminder +
$Title<% _t('ContentReviewEmails.REVIEWPAGELINK','Review the page in the CMS') %>
+ <% _t('ContentReviewEmails.VIEWPUBLISHEDLINK','View this page on the website') %> +
+ + + + + + + <% loop $SecondReminderPages %> + + + + + <% end_loop %> + +
+ $EmailBodySecondReminder +
$Title<% _t('ContentReviewEmails.REVIEWPAGELINK','Review the page in the CMS') %>
+ <% _t('ContentReviewEmails.VIEWPUBLISHEDLINK','View this page on the website') %> +
+ + \ No newline at end of file