diff --git a/code/extensions/ContentReviewCMSExtension.php b/code/extensions/ContentReviewCMSExtension.php index 1504ba5..c22b565 100644 --- a/code/extensions/ContentReviewCMSExtension.php +++ b/code/extensions/ContentReviewCMSExtension.php @@ -6,7 +6,6 @@ */ class ContentReviewCMSExtension extends LeftAndMainExtension { - /** * @var array */ diff --git a/code/extensions/ContentReviewDefaultSettings.php b/code/extensions/ContentReviewDefaultSettings.php index 1631b5a..5e958fa 100644 --- a/code/extensions/ContentReviewDefaultSettings.php +++ b/code/extensions/ContentReviewDefaultSettings.php @@ -9,23 +9,49 @@ class ContentReviewDefaultSettings extends DataExtension { /** + * @config + * * @var array */ private static $db = array( - "ReviewPeriodDays" => "Int", + 'ReviewPeriodDays' => 'Int', + 'ReviewFrom' => 'Varchar(255)', + 'ReviewSubject' => 'Varchar(255)', + 'ReviewBody' => 'HTMLText', ); /** + * @config + * + * @var array + */ + private static $defaults = array( + 'ReviewSubject' => 'Page(s) are due for content review', + 'ReviewBody' => '

Page(s) due for review

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

', + ); + + /** + * @config * * @var array */ private static $many_many = array( - "ContentReviewGroups" => "Group", - "ContentReviewUsers" => "Member", + 'ContentReviewGroups' => 'Group', + 'ContentReviewUsers' => 'Member', ); /** + * Template to use for content review emails. * + * This should contain an $EmailBody variable as a placeholder for the user-defined copy + * + * @config + * + * @var string + */ + private static $content_review_template = 'ContentReviewEmail'; + + /** * @return string */ public function getOwnerNames() @@ -33,14 +59,14 @@ class ContentReviewDefaultSettings extends DataExtension $names = array(); foreach ($this->OwnerGroups() as $group) { - $names[] = $group->getBreadcrumbs(" > "); + $names[] = $group->getBreadcrumbs(' > '); } foreach ($this->OwnerUsers() as $group) { $names[] = $group->getName(); } - return implode(", ", $names); + return implode(', ', $names); } /** @@ -48,7 +74,7 @@ class ContentReviewDefaultSettings extends DataExtension */ public function OwnerGroups() { - return $this->owner->getManyManyComponents("ContentReviewGroups"); + return $this->owner->getManyManyComponents('ContentReviewGroups'); } /** @@ -56,54 +82,78 @@ class ContentReviewDefaultSettings extends DataExtension */ public function OwnerUsers() { - return $this->owner->getManyManyComponents("ContentReviewUsers"); + return $this->owner->getManyManyComponents('ContentReviewUsers'); } /** - * * @param FieldList $fields */ public function updateCMSFields(FieldList $fields) { - $helpText = LiteralField::create("ContentReviewHelp", _t("ContentReview.DEFAULTSETTINGSHELP", "These content review settings will apply to all pages that does not have specific Content Review schedule.")); + $helpText = LiteralField::create( + 'ContentReviewHelp', + _t( + 'ContentReview.DEFAULTSETTINGSHELP', + 'These content review settings will apply to all pages that does not have specific Content Review schedule.' + ) + ); - $fields->addFieldToTab("Root.ContentReview", $helpText); + $fields->addFieldToTab('Root.ContentReview', $helpText); - $reviewFrequency = DropdownField::create("ReviewPeriodDays", _t("ContentReview.REVIEWFREQUENCY", "Review frequency"), SiteTreeContentReview::get_schedule()) - ->setDescription(_t("ContentReview.REVIEWFREQUENCYDESCRIPTION", "The review date will be set to this far in the future whenever the page is published")); + $reviewFrequency = DropdownField::create( + 'ReviewPeriodDays', + _t('ContentReview.REVIEWFREQUENCY', 'Review frequency'), + SiteTreeContentReview::get_schedule() + ) + ->setDescription(_t( + 'ContentReview.REVIEWFREQUENCYDESCRIPTION', + 'The review date will be set to this far in the future whenever the page is published' + )); - $fields->addFieldToTab("Root.ContentReview", $reviewFrequency); + $fields->addFieldToTab('Root.ContentReview', $reviewFrequency); $users = Permission::get_members_by_permission(array( - "CMS_ACCESS_CMSMain", - "ADMIN", + 'CMS_ACCESS_CMSMain', + 'ADMIN', )); - $usersMap = $users->map("ID", "Title")->toArray(); + $usersMap = $users->map('ID', 'Title')->toArray(); asort($usersMap); - $userField = ListboxField::create("OwnerUsers", _t("ContentReview.PAGEOWNERUSERS", "Users"), $usersMap) + $userField = ListboxField::create('OwnerUsers', _t('ContentReview.PAGEOWNERUSERS', 'Users'), $usersMap) ->setMultiple(true) - ->setAttribute("data-placeholder", _t("ContentReview.ADDUSERS", "Add users")) - ->setDescription(_t("ContentReview.OWNERUSERSDESCRIPTION", "Page owners that are responsible for reviews")); + ->setAttribute('data-placeholder', _t('ContentReview.ADDUSERS', 'Add users')) + ->setDescription(_t('ContentReview.OWNERUSERSDESCRIPTION', 'Page owners that are responsible for reviews')); - $fields->addFieldToTab("Root.ContentReview", $userField); + $fields->addFieldToTab('Root.ContentReview', $userField); $groupsMap = array(); foreach (Group::get() as $group) { // Listboxfield values are escaped, use ASCII char instead of » - $groupsMap[$group->ID] = $group->getBreadcrumbs(" > "); + $groupsMap[$group->ID] = $group->getBreadcrumbs(' > '); } asort($groupsMap); - $groupField = ListboxField::create("OwnerGroups", _t("ContentReview.PAGEOWNERGROUPS", "Groups"), $groupsMap) + $groupField = ListboxField::create('OwnerGroups', _t('ContentReview.PAGEOWNERGROUPS', 'Groups'), $groupsMap) ->setMultiple(true) - ->setAttribute("data-placeholder", _t("ContentReview.ADDGROUP", "Add groups")) - ->setDescription(_t("ContentReview.OWNERGROUPSDESCRIPTION", "Page owners that are responsible for reviews")); + ->setAttribute('data-placeholder', _t('ContentReview.ADDGROUP', 'Add groups')) + ->setDescription(_t('ContentReview.OWNERGROUPSDESCRIPTION', 'Page owners that are responsible for reviews')); - $fields->addFieldToTab("Root.ContentReview", $groupField); + $fields->addFieldToTab('Root.ContentReview', $groupField); + + // 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')), + LiteralField::create('TemplateHelp', $this->owner->renderWith('ContentReviewAdminHelp')), + ) + ); } /** @@ -112,7 +162,64 @@ class ContentReviewDefaultSettings extends DataExtension * * @return ArrayList */ - public function ContentReviewOwners() { + public function ContentReviewOwners() + { return SiteTreeContentReview::merge_owners($this->OwnerGroups(), $this->OwnerUsers()); } + + /** + * Get the review body, falling back to the default if left blank. + * + * @return string HTML text + */ + public function getReviewBody() + { + return $this->getWithDefault('ReviewBody'); + } + + /** + * Get the review subject line, falling back to the default if left blank. + * + * @return string plain text value + */ + public function getReviewSubject() + { + return $this->getWithDefault('ReviewSubject'); + } + + /** + * Get the "from" field for review emails. + * + * @return string + */ + public function getReviewFrom() + { + $from = $this->owner->getField('ReviewFrom'); + if ($from) { + return $from; + } + + // Fall back to admin email + return Config::inst()->get('Email', 'admin_email'); + } + + /** + * Get the value of a user-configured field, falling back to the default if left blank. + * + * @param string $field + * + * @return string + */ + protected function getWithDefault($field) + { + $value = $this->owner->getField($field); + if ($value) { + return $value; + } + // fallback to default value + $defaults = $this->owner->config()->defaults; + if (isset($defaults[$field])) { + return $defaults[$field]; + } + } } diff --git a/code/extensions/SiteTreeContentReview.php b/code/extensions/SiteTreeContentReview.php index b990543..15fc6cc 100644 --- a/code/extensions/SiteTreeContentReview.php +++ b/code/extensions/SiteTreeContentReview.php @@ -122,7 +122,7 @@ class SiteTreeContentReview extends DataExtension implements PermissionProvider Requirements::css("contentreview/css/contentreview.css"); $reviewTitle = LiteralField::create( - "ReviewContentNotesLabel", + "ReviewContentNotesLabel", "" ); diff --git a/code/reports/PagesDueForReviewReport.php b/code/reports/PagesDueForReviewReport.php index 27dd42a..96e2004 100644 --- a/code/reports/PagesDueForReviewReport.php +++ b/code/reports/PagesDueForReviewReport.php @@ -89,7 +89,7 @@ class PagesDueForReviewReport extends SS_Report ), "ContentReviewType" => array( "title" => "Settings are", - "formatting" => function ($value, $item) use ($linkPath,$linkQuery) { + "formatting" => function ($value, $item) use ($linkPath, $linkQuery) { if ($item->ContentReviewType == "Inherit") { $options = $item->getOptions(); if ($options && $options instanceof SiteConfig) { @@ -117,7 +117,8 @@ class PagesDueForReviewReport extends SS_Report * @param array $params * * @return SS_List - */public function sourceRecords($params = array()) + */ + public function sourceRecords($params = array()) { Versioned::reading_stage("Stage"); diff --git a/code/tasks/ContentReviewEmails.php b/code/tasks/ContentReviewEmails.php index a9f2343..11fb08f 100644 --- a/code/tasks/ContentReviewEmails.php +++ b/code/tasks/ContentReviewEmails.php @@ -5,13 +5,6 @@ */ class ContentReviewEmails extends BuildTask { - /** - * Holds a cached array for looking up members via their ID. - * - * @var array - */ - protected static $member_cache = array(); - /** * @param SS_HTTPRequest $request */ @@ -19,10 +12,9 @@ class ContentReviewEmails extends BuildTask { $compatibility = ContentReviewCompatability::start(); - $now = class_exists("SS_Datetime") ? SS_Datetime::now()->URLDate() : SSDatetime::now()->URLDate(); - // First grab all the pages with a custom setting - $pages = Page::get("Page")->where("\"SiteTree\".\"NextReviewDate\" <= '{$now}'"); + $pages = Page::get() + ->filter('NextReviewDate:LessThanOrEqual', SS_Datetime::now()->URLDate()); $overduePages = $this->getOverduePagesForOwners($pages); @@ -52,10 +44,6 @@ class ContentReviewEmails extends BuildTask $option = $page->getOptions(); foreach ($option->ContentReviewOwners() as $owner) { - if (!isset(self::$member_cache[$owner->ID])) { - self::$member_cache[$owner->ID] = $owner; - } - if (!isset($overduePages[$owner->ID])) { $overduePages[$owner->ID] = new ArrayList(); } @@ -73,22 +61,69 @@ class ContentReviewEmails extends BuildTask */ protected function notifyOwner($ownerID, SS_List $pages) { - $owner = self::$member_cache[$ownerID]; - $sender = Security::findAnAdministrator(); - $senderEmail = ($sender->Email) ? $sender->Email : Config::inst()->get("Email", "admin_email"); + // Prepare variables + $siteConfig = SiteConfig::current_site_config(); + $owner = Member::get()->byID($ownerID); + $templateVariables = $this->getTemplateVariables($owner, $siteConfig, $pages); - $subject = _t("ContentReviewEmails.SUBJECT", "Page(s) are due for content review"); + // Build email $email = new Email(); $email->setTo($owner->Email); - $email->setFrom($senderEmail); - $email->setTemplate("ContentReviewEmail"); - $email->setSubject($subject); - $email->populateTemplate(array( - "Recipient" => $owner, - "Sender" => $sender, - "Pages" => $pages, - )); + $email->setFrom($siteConfig->ReviewFrom); + $email->setSubject($siteConfig->ReviewSubject); + // Get user-editable body + $body = $this->getEmailBody($siteConfig, $templateVariables); + + // Populate mail body with fixed template + $email->setTemplate($siteConfig->config()->content_review_template); + $email->populateTemplate($templateVariables); + $email->populateTemplate(array( + 'EmailBody' => $body, + 'Recipient' => $owner, + 'Pages' => $pages, + )); $email->send(); } + + /** + * Get string value of HTML body with all variable evaluated. + * + * @param SiteConfig $config + * @param array List of safe template variables to expose to this template + * + * @return HTMLText + */ + protected function getEmailBody($config, $variables) + { + $template = SSViewer::fromString($config->ReviewBody); + $value = $template->process(new ArrayData($variables)); + + // Cast to HTML + return DBField::create_field('HTMLText', (string) $value); + } + + /** + * Gets list of safe template variables and their values which can be used + * in both the static and editable templates. + * + * {@see ContentReviewAdminHelp.ss} + * + * @param Member $recipient + * @param SiteConfig $config + * @param SS_List $pages + * + * @return array + */ + protected function getTemplateVariables($recipient, $config, $pages) + { + return array( + 'Subject' => $config->ReviewSubject, + 'PagesCount' => $pages->count(), + 'FromEmail' => $config->ReviewFrom, + 'ToFirstName' => $recipient->FirstName, + 'ToSurname' => $recipient->Surname, + 'ToEmail' => $recipient->Email, + ); + } } diff --git a/docs/.DS_Store b/docs/.DS_Store deleted file mode 100644 index b3c4ac8..0000000 Binary files a/docs/.DS_Store and /dev/null differ diff --git a/docs/en/.DS_Store b/docs/en/.DS_Store deleted file mode 100644 index dad1ae1..0000000 Binary files a/docs/en/.DS_Store and /dev/null differ diff --git a/docs/en/images/content-review-siteconfig-settings.png b/docs/en/images/content-review-siteconfig-settings.png new file mode 100644 index 0000000..5b2776e Binary files /dev/null and b/docs/en/images/content-review-siteconfig-settings.png differ diff --git a/docs/en/index.md b/docs/en/index.md index 271824b..52fa433 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -49,6 +49,11 @@ In order for the contentreview module to send emails, you need to *either*: * Setup the DailyTask script to run daily via cron. See framework/tasks/ScheduledTask.php for more information on setup. * Install the queuedjobs module, and follow the configuration steps to create a cron job for that module. Once installed, you can just run dev/build to have a job created, which will run at 9am every day by default. +Global settings can be configured via the global settings admin in the CMS under the "Content Review" tab. +This includes global groups, users, as well as a template editor that supports a limited number of variables. + +![settings](docs/en/images/content-review-siteconfig-settings.png) + ## Usage To set up content review schedules you need to log in as a user with the 'Set content owners and review dates' permission. This can either diff --git a/templates/ContentReviewAdminHelp.ss b/templates/ContentReviewAdminHelp.ss new file mode 100644 index 0000000..243a541 --- /dev/null +++ b/templates/ContentReviewAdminHelp.ss @@ -0,0 +1,10 @@ +

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

+ + diff --git a/templates/ContentReviewEmail.ss b/templates/ContentReviewEmail.ss index 41cf052..6b45554 100644 --- a/templates/ContentReviewEmail.ss +++ b/templates/ContentReviewEmail.ss @@ -6,8 +6,7 @@ -

<% _t('ContentReviewEmails.EMAIL_HEADING','Page(s) due for review') %>

-

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

+ $EmailBody <% loop Pages %> diff --git a/tests/ContentReviewBaseTest.php b/tests/ContentReviewBaseTest.php index 219b674..d5c9f28 100644 --- a/tests/ContentReviewBaseTest.php +++ b/tests/ContentReviewBaseTest.php @@ -6,7 +6,6 @@ */ abstract class ContentReviewBaseTest extends FunctionalTest { - /** * @var bool */ diff --git a/tests/ContentReviewNotificationTest.php b/tests/ContentReviewNotificationTest.php index c8f16fa..ebb8a7f 100644 --- a/tests/ContentReviewNotificationTest.php +++ b/tests/ContentReviewNotificationTest.php @@ -8,39 +8,57 @@ class ContentReviewNotificationTest extends SapphireTest /** * @var string */ - public static $fixture_file = "contentreview/tests/ContentReviewTest.yml"; + public static $fixture_file = 'contentreview/tests/ContentReviewTest.yml'; + + public function setUp() + { + parent::setUp(); + + // Hack to ensure only desired siteconfig is scaffolded + $desiredID = $this->idFromFixture('SiteConfig', 'mysiteconfig'); + foreach (SiteConfig::get()->exclude('ID', $desiredID) as $config) { + $config->delete(); + } + } /** * @var array */ protected $requiredExtensions = array( - "SiteTree" => array("SiteTreeContentReview"), - "Group" => array("ContentReviewOwner"), - "Member" => array("ContentReviewOwner"), - "CMSPageEditController" => array("ContentReviewCMSExtension"), - "SiteConfig" => array("ContentReviewDefaultSettings"), + 'SiteTree' => array('SiteTreeContentReview'), + 'Group' => array('ContentReviewOwner'), + 'Member' => array('ContentReviewOwner'), + 'CMSPageEditController' => array('ContentReviewCMSExtension'), + 'SiteConfig' => array('ContentReviewDefaultSettings'), ); public function testContentReviewEmails() { - SS_Datetime::set_mock_now("2010-02-24 12:00:00"); + SS_Datetime::set_mock_now('2010-02-24 12:00:00'); /** @var Page|SiteTreeContentReview $childParentPage */ - $childParentPage = $this->objFromFixture("Page", "contact"); - $childParentPage->NextReviewDate = "2010-02-23"; + $childParentPage = $this->objFromFixture('Page', 'contact'); + $childParentPage->NextReviewDate = '2010-02-23'; $childParentPage->write(); $task = new ContentReviewEmails(); - $task->run(new SS_HTTPRequest("GET", "/dev/tasks/ContentReviewEmails")); + $task->run(new SS_HTTPRequest('GET', '/dev/tasks/ContentReviewEmails')); - $expectedSubject = _t("ContentReviewEmails.SUBJECT", "Page(s) are due for content review"); - $email = $this->findEmail("author@example.com", null, $expectedSubject); + // Set template variables (as per variable case) + $ToEmail = 'author@example.com'; + $Subject = 'Please log in to review some content!'; + $PagesCount = 3; + $ToFirstName = 'Test'; + $email = $this->findEmail($ToEmail, null, $Subject); $this->assertNotNull($email, "Email haven't been sent."); - $this->assertContains("There are 3 pages that are due for review today by you.", $email["htmlContent"]); - $this->assertContains("Staff", $email["htmlContent"]); - $this->assertContains("Contact Us", $email["htmlContent"]); - $this->assertContains("Contact Us Child", $email["htmlContent"]); + $this->assertContains( + "

$Subject

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

This email was sent to $ToEmail

", + $email['htmlContent'] + ); + $this->assertContains('Staff', $email['htmlContent']); + $this->assertContains('Contact Us', $email['htmlContent']); + $this->assertContains('Contact Us Child', $email['htmlContent']); SS_Datetime::clear_mock_now(); } diff --git a/tests/ContentReviewTest.yml b/tests/ContentReviewTest.yml index 5b6c9a5..580c353 100644 --- a/tests/ContentReviewTest.yml +++ b/tests/ContentReviewTest.yml @@ -1,76 +1,82 @@ +SiteConfig: + mysiteconfig: + ReviewFrom: sender@silverstripe.com + ReviewSubject: 'Please log in to review some content!' + ReviewBody: '

$Subject

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

This email was sent to $ToEmail

' + Permission: - cmsmain1: - Code: CMS_ACCESS_CMSMain - cmsmain2: - Code: CMS_ACCESS_CMSMain - setreviewdates: - Code: EDIT_CONTENT_REVIEW_FIELDS - workflowadmin1: - Code: IS_WORKFLOW_ADMIN - workflowadmin2: - Code: IS_WORKFLOW_ADMIN + cmsmain1: + Code: CMS_ACCESS_CMSMain + cmsmain2: + Code: CMS_ACCESS_CMSMain + setreviewdates: + Code: EDIT_CONTENT_REVIEW_FIELDS + workflowadmin1: + Code: IS_WORKFLOW_ADMIN + workflowadmin2: + Code: IS_WORKFLOW_ADMIN Group: - editorgroup: - Title: Edit existing pages - Code: editorgroup - Permissions: =>Permission.cmsmain1,=>Permission.workflowadmin1,=>Permission.setreviewdates - authorgroup: - Title: Author existing pages - Code: authorgroup - Permissions: =>Permission.cmsmain2,=>Permission.workflowadmin2 + editorgroup: + Title: Edit existing pages + Code: editorgroup + Permissions: =>Permission.cmsmain1,=>Permission.workflowadmin1,=>Permission.setreviewdates + authorgroup: + Title: Author existing pages + Code: authorgroup + Permissions: =>Permission.cmsmain2,=>Permission.workflowadmin2 Member: - author: - FirstName: Test - Surname: Author - Email: author@example.com - Groups: =>Group.authorgroup - editor: - FirstName: Test - Surname: Editor - Groups: =>Group.editorgroup - visitor: - FirstName: Kari - Surname: Visitor - Email: visitor@example.com - + author: + FirstName: Test + Surname: Author + Email: author@example.com + Groups: =>Group.authorgroup + editor: + FirstName: Test + Surname: Editor + Groups: =>Group.editorgroup + visitor: + FirstName: Kari + Surname: Visitor + Email: visitor@example.com + Page: # Cant be reviewed, no owners - home: - Title: Home - ContentReviewType: Custom - NextReviewDate: 2010-02-01 - ReviewPeriodDays: 10 + home: + Title: Home + ContentReviewType: Custom + NextReviewDate: 2010-02-01 + ReviewPeriodDays: 10 # Cant be reviewed, no owners - about: - Title: About Us - ContentReviewType: Custom - NextReviewDate: 2010-02-07 - ReviewPeriodDays: 10 - staff: - Title: Staff - ContentReviewType: Custom - NextReviewDate: 2010-02-14 - ReviewPeriodDays: 10 - ContentReviewUsers: =>Member.author - contact: - Title: Contact Us - ContentReviewType: Custom - ReviewPeriodDays: 10 - NextReviewDate: 2010-02-21 - ContentReviewGroups: =>Group.authorgroup - contact-child: - Title: Contact Us Child - ContentReviewType: Inherit - ParentID: =>Page.contact + about: + Title: About Us + ContentReviewType: Custom + NextReviewDate: 2010-02-07 + ReviewPeriodDays: 10 + staff: + Title: Staff + ContentReviewType: Custom + NextReviewDate: 2010-02-14 + ReviewPeriodDays: 10 + ContentReviewUsers: =>Member.author + contact: + Title: Contact Us + ContentReviewType: Custom + ReviewPeriodDays: 10 + NextReviewDate: 2010-02-21 + ContentReviewGroups: =>Group.authorgroup + contact-child: + Title: Contact Us Child + ContentReviewType: Inherit + ParentID: =>Page.contact # Cant be reviewed, no NextReviewDate - no-review: - Title: Page without review date - ContentReviewType: Custom - ContentReviewUsers: =>Member.author + no-review: + Title: Page without review date + ContentReviewType: Custom + ContentReviewUsers: =>Member.author # Cant be reviewed, no NextReviewDate - group-owned: - Title: Page owned by group - ContentReviewType: Custom - ContentReviewGroups: =>Group.authorgroup \ No newline at end of file + group-owned: + Title: Page owned by group + ContentReviewType: Custom + ContentReviewGroups: =>Group.authorgroup \ No newline at end of file