diff --git a/_config/config.yml b/_config/config.yml index 8bb4e99..7f17b79 100644 --- a/_config/config.yml +++ b/_config/config.yml @@ -9,4 +9,7 @@ Member: - ContentReviewOwner CMSPageEditController: extensions: - - ContentReviewCMSExtension \ No newline at end of file + - ContentReviewCMSExtension +SiteConfig: + extensions: + - ContentReviewDefaultSettings \ No newline at end of file diff --git a/code/ContentReviewEmails.php b/code/ContentReviewEmails.php index 0833314..d331219 100644 --- a/code/ContentReviewEmails.php +++ b/code/ContentReviewEmails.php @@ -4,10 +4,19 @@ * Daily task to send emails to the owners of content items * when the review date rolls around * + * + * @todo create a page cache for the inherited so that we dont unneccesary need to look up parent pages * @package contentreview */ 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 @@ -21,52 +30,160 @@ class ContentReviewEmails extends BuildTask { $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') ->leftJoin('Group_SiteTreeContentReview', '"SiteTree"."ID" = "OwnerGroups"."SiteTreeID"', 'OwnerGroups') ->leftJoin('Member_SiteTreeContentReview', '"SiteTree"."ID" = "OwnerUsers"."SiteTreeID"', "OwnerUsers") - ->where('"SiteTree"."NextReviewDate" <= \''.$now.'\' AND' .' ("OwnerGroups"."ID" IS NOT NULL OR "OwnerUsers"."ID" IS NOT NULL)') + ->where('"SiteTree"."ContentReviewType" != \'Custom\' AND "SiteTree"."NextReviewDate" <= \''.$now.'\' AND' . + ' ("OwnerGroups"."ID" IS NOT NULL OR "OwnerUsers"."ID" IS NOT NULL)') ; - if ($pages && $pages->Count()) { - foreach($pages as $page) { - $owners = $page->ContentReviewOwners(); - if(!$owners->count()) { - continue; - } - $sender = Security::findAnAdministrator(); - - foreach($owners as $recipient) { - $subject = sprintf(_t('ContentReviewEmails.SUBJECT', 'Page %s due for content review'), $page->Title); - - $email = new Email(); - $email->setTo($recipient->Email); - $email->setFrom(($sender->Email) ? $sender->Email : Email::getAdminEmail()); - $email->setTemplate('ContentReviewEmails'); - $email->setSubject($subject); - $email->populateTemplate(array( - "PageCMSLink" => "admin/pages/edit/show/".$page->ID, - "Recipient" => $recipient, - "Sender" => $sender, - "Page" => $page, - "StageSiteLink" => Controller::join_links($page->Link(), "?stage=Stage"), - "LiveSiteLink" => Controller::join_links($page->Link(), "?stage=Live"), - )); - $email->send(); - - $message = ''._t('ContentReviewEmails.EMAIL_HEADING','Page due for review').'
'. - 'The page "'.$page->Title.'" is due for review today by you.
- '. _t('ContentReviewEmails.REVIEWPAGELINK','Review the page in the CMS') .' — - '. _t('ContentReviewEmails.VIEWPUBLISHEDLINK','View this page on the website') .''; - if(class_exists('Notification')) { - Notification::notify($recipient, $message); - } - } - } + $this->notify($pages); + + // Then grab all the pages with that inherits their settings + + $pages = Page::get('Page') + ->leftJoin('Group_SiteTreeContentReview', '"SiteTree"."ID" = "OwnerGroups"."SiteTreeID"', 'OwnerGroups') + ->leftJoin('Member_SiteTreeContentReview', '"SiteTree"."ID" = "OwnerUsers"."SiteTreeID"', "OwnerUsers") + ->where('"SiteTree"."ContentReviewType" = \'Inherit\'') + ; + + $overduePages = $this->findInheritedSettings($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->notify_user($memberID, $pages); } // Revert subsite filter (if installed) - if (ClassInfo::exists('Subsite')) { + if(ClassInfo::exists('Subsite')) { Subsite::$disable_subsite_filter = $oldSubsiteState; } } + + /** + * + * @param SS_list $pages + * @return type + */ + protected function findInheritedSettings(SS_list $pages) { + $overduePages = array(); + + foreach($pages as $page) { + + $settings = $this->findContentSettingFor($page); + // This page has a parent with the 'Disabled' option + if(!$settings) { + continue; + } + + $owners = $settings->ContentReviewOwners(); + if(!$owners->count()) { + continue; + } + if(!$settings->ReviewPeriodDays) { + continue; + } + // Calculate next time this page should be reviewed from the LastEdited datea + $nextReviewUnixSec = strtotime($page->LastEdited . ' + '.$settings->ReviewPeriodDays . ' days'); + if($nextReviewUnixSec > time()) { + continue; + } + + foreach($owners as $owner) { + if(!isset(self::$member_cache[$owner->ID])) { + self::$member_cache[$owner->ID] = $owner; + } + if(!isset($overduePages[$owner->ID])) { + $overduePages[$owner->ID] = array(); + } + $overduePages[$owner->ID][] = $page; + } + } + return $overduePages; + } + + /** + * + * @param SiteTree $page + * @return DataObject or false if no settings found + */ + protected function findContentSettingFor($page) { + while($parent = $page->Parent()) { + // Root page, use siteconfig + if(!$parent->exists()) { + return SiteConfig::current_site_config(); + } + if($parent->ContentReviewType == 'Custom') { + return $parent; + } + if($parent->ContentReviewType == 'Disabled') { + return false; + } + $page = $parent; + } + throw new Exception('This shouldnt really happen, as usual.'); + } + + /** + * + * @param int $owner + * @param array $pages + */ + protected function notify_user($ownerID, array $pages) { + $owner = self::$member_cache[$ownerID]; + echo $owner->Email.PHP_EOL; + foreach($pages as $page) { + echo $page->Title.PHP_EOL; + } + } + + /** + * + * @param SS_List $pages + * @return void + */ + protected function notify(SS_List $pages) { + if(!$pages) { + return; + } + if(!$pages->Count()) { + return; + } + + foreach($pages as $page) { + // Resolve the content owner groups and members to a single list of members + $owners = $page->ContentReviewOwners(); + if(!$owners->count()) { + continue; + } + $sender = Security::findAnAdministrator(); + + foreach($owners as $recipient) { + $subject = sprintf(_t('ContentReviewEmails.SUBJECT', 'Page %s due for content review'), $page->Title); + $email = new Email(); + $email->setTo($recipient->Email); + $email->setFrom(($sender->Email) ? $sender->Email : Email::getAdminEmail()); + $email->setTemplate('ContentReviewEmails'); + $email->setSubject($subject); + $email->populateTemplate(array( + "PageCMSLink" => "admin/pages/edit/show/".$page->ID, + "Recipient" => $recipient, + "Sender" => $sender, + "Page" => $page, + "StageSiteLink" => Controller::join_links($page->Link(), "?stage=Stage"), + "LiveSiteLink" => Controller::join_links($page->Link(), "?stage=Live"), + )); + //$email->send(); + $message = ''._t('ContentReviewEmails.EMAIL_HEADING','Page due for review').'
'. + 'The page "'.$page->Title.'" is due for review today by you.
+ '. _t('ContentReviewEmails.REVIEWPAGELINK','Review the page in the CMS') .' — + '. _t('ContentReviewEmails.VIEWPUBLISHEDLINK','View this page on the website') .''; + if(class_exists('Notification')) { + // Notification::notify($recipient, $message); + } + // echo $page->Title.' - '.$recipient->Email.PHP_EOL; + } + } + } } diff --git a/code/extensions/ContentReviewDefaultSettings.php b/code/extensions/ContentReviewDefaultSettings.php new file mode 100644 index 0000000..40f2acd --- /dev/null +++ b/code/extensions/ContentReviewDefaultSettings.php @@ -0,0 +1,110 @@ + "Int", + ); + + /** + * + * @var array + */ + private static $many_many = array( + 'ContentReviewGroups' => 'Group', + 'ContentReviewUsers' => 'Member' + ); + + /** + * @return ManyManyList + */ + public function OwnerGroups() { + return $this->owner->getManyManyComponents('ContentReviewGroups'); + } + + /** + * @return ManyManyList + */ + public function OwnerUsers() { + 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.')); + $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')); + + $fields->addFieldToTab('Root.ContentReview', $reviewFrequency); + + $users = Permission::get_members_by_permission(array("CMS_ACCESS_CMSMain", "ADMIN")); + + $usersMap = $users->map('ID', 'Title')->toArray(); + asort($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')); + $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(' > '); + } + asort($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')); + $fields->addFieldToTab('Root.ContentReview', $groupField); + } + + /** + * Get all Members that are default Content Owners + * + * This includes checking group hierarchy and adding any direct users + * + * @return \ArrayList + */ + public function ContentReviewOwners() { + $contentReviewOwners = new ArrayList(); + $toplevelGroups = $this->OwnerGroups(); + if($toplevelGroups->count()) { + $groupIDs = array(); + foreach($toplevelGroups as $group) { + $familyIDs = $group->collateFamilyIDs(); + if(is_array($familyIDs)) { + $groupIDs = array_merge($groupIDs, array_values($familyIDs)); + } + } + array_unique($groupIDs); + if(count($groupIDs)) { + $groupMembers = DataObject::get('Member')->where("\"Group\".\"ID\" IN (" . implode(",",$groupIDs) . ")") + ->leftJoin("Group_Members", "\"Member\".\"ID\" = \"Group_Members\".\"MemberID\"") + ->leftJoin("Group", "\"Group_Members\".\"GroupID\" = \"Group\".\"ID\""); + $contentReviewOwners->merge($groupMembers); + } + } + $contentReviewOwners->merge($this->OwnerUsers()); + $contentReviewOwners->removeDuplicates(); + return $contentReviewOwners; + } +} diff --git a/code/extensions/SiteTreeContentReview.php b/code/extensions/SiteTreeContentReview.php index ef03fa3..ed0ba76 100644 --- a/code/extensions/SiteTreeContentReview.php +++ b/code/extensions/SiteTreeContentReview.php @@ -12,6 +12,7 @@ class SiteTreeContentReview extends DataExtension implements PermissionProvider * @var array */ private static $db = array( + "ContentReviewType" => "Enum('Inherit, Disabled, Custom', 'Inherit')", "ReviewPeriodDays" => "Int", "NextReviewDate" => "Date", 'LastEditedByName' => 'Varchar(255)', @@ -39,7 +40,7 @@ class SiteTreeContentReview extends DataExtension implements PermissionProvider * * @var array */ - private $daysToString = array( + private static $schedule = array( 0 => "No automatic review date", 1 => "1 day", 7 => "1 week", @@ -110,6 +111,13 @@ class SiteTreeContentReview extends DataExtension implements PermissionProvider return $contentReviewOwners; } + /** + * @return array + */ + public static function get_schedule() { + return self::$schedule; + } + /** * @return ManyManyList */ @@ -130,17 +138,19 @@ class SiteTreeContentReview extends DataExtension implements PermissionProvider * @return void */ public function updateSettingsFields(FieldList $fields) { + + Requirements::javascript('contentreview/javascript/contentreview.js'); $crFields = new FieldList(); // Display read-only version only if(!Permission::check("EDIT_CONTENT_REVIEW_FIELDS")) { - + $schedule = self::get_schedule(); $contentOwners = ReadonlyField::create('ROContentOwners', _t('ContentReview.CONTENTOWNERS', 'Content Owners'), $this->getOwnerNames()); $nextReviewAt = DateField::create('RONextReviewDate', _t("ContentReview.NEXTREVIEWDATE", "Next review date"), $this->owner->NextReviewDate); - if(!isset($this->daysToString[$this->owner->ReviewPeriodDays])) { - $reviewFreq = ReadonlyField::create("ROReviewPeriodDays", _t("ContentReview.REVIEWFREQUENCY", "Review frequency"), $this->daysToString[0]); + if(!isset($schedule[$this->owner->ReviewPeriodDays])) { + $reviewFreq = ReadonlyField::create("ROReviewPeriodDays", _t("ContentReview.REVIEWFREQUENCY", "Review frequency"), $schedule[0]); } else { - $reviewFreq = ReadonlyField::create("ROReviewPeriodDays", _t("ContentReview.REVIEWFREQUENCY", "Review frequency"), $this->daysToString[$this->owner->ReviewPeriodDays]); + $reviewFreq = ReadonlyField::create("ROReviewPeriodDays", _t("ContentReview.REVIEWFREQUENCY", "Review frequency"), $schedule[$this->owner->ReviewPeriodDays]); } $logConfig = GridFieldConfig::create() @@ -162,6 +172,17 @@ class SiteTreeContentReview extends DataExtension implements PermissionProvider return; } + $options = array(); + $options["Disabled"] = _t('ContentReview.DISABLE', "Disable content review"); + $options["Inherit"] = _t('ContentReview.INHERIT', "Inherit from parent page"); + $options["Custom"] = _t('ContentReview.CUSTOM', "Custom settings"); + $viewersOptionsField = OptionsetField::create("ContentReviewType", _t('ContentReview.OPTIONS', "Options"), $options); + + //$viewersOptionsField->setValue($this->owner->ContentReviewType); + + #var_dump($this->owner->ContentReviewType); + #die(); + $users = Permission::get_members_by_permission(array("CMS_ACCESS_CMSMain", "ADMIN")); $usersMap = $users->map('ID', 'Title')->toArray(); @@ -190,19 +211,22 @@ class SiteTreeContentReview extends DataExtension implements PermissionProvider ->setConfig('datavalueformat', 'yyyy-MM-dd') ->setDescription(_t('ContentReview.NEXTREVIEWDATADESCRIPTION', 'Leave blank for no review')); - $reviewFrequency = DropdownField::create("ReviewPeriodDays", _t("ContentReview.REVIEWFREQUENCY", "Review frequency"), $this->daysToString) + $reviewFrequency = DropdownField::create("ReviewPeriodDays", _t("ContentReview.REVIEWFREQUENCY", "Review frequency"), self::get_schedule()) ->setDescription(_t('ContentReview.REVIEWFREQUENCYDESCRIPTION', 'The review date will be set to this far in the future whenever the page is published')); $notesField = GridField::create('ReviewNotes', 'Review Notes', $this->owner->ReviewLogs(), GridFieldConfig_RecordEditor::create()); - $crFields->add(new HeaderField(_t('ContentReview.REVIEWHEADER', "Content review"), 2)); - $crFields->add($userField); - $crFields->add($groupField); - $crFields->add($reviewDate); - $crFields->add($reviewFrequency); - $crFields->add($notesField); - - $fields->addFieldsToTab("Root.ContentReview", $crFields); + $fields->addFieldsToTab("Root.ContentReview", array( + new HeaderField(_t('ContentReview.REVIEWHEADER', "Content review"), 2), + $viewersOptionsField, + CompositeField::create( + $userField, + $groupField, + $reviewDate, + $reviewFrequency + )->addExtraClass('contentReviewSettings'), + $notesField + )); } /** diff --git a/javascript/contentreview.js b/javascript/contentreview.js index afc35a5..38852fb 100644 --- a/javascript/contentreview.js +++ b/javascript/contentreview.js @@ -4,5 +4,43 @@ jQuery(function($) { "use strict"; $.entwine('ss', function($) { + + + /** + * Class: .cms-edit-form #ContentReviewType + * + * Toggle display of group dropdown in "access" tab, + * based on selection of radiobuttons. + */ + $('.cms-edit-form #ContentReviewType').entwine({ + // Constructor: onmatch + onmatch: function() { + // TODO Decouple + var dropdown; + if(this.attr('id') == 'ContentReviewType') dropdown = $('.contentReviewSettings'); + + this.find('.optionset :input').bind('change', function(e) { + var wrapper = $(this).closest('.middleColumn').parent('div'); + if(e.target.value == 'Custom') { + wrapper.addClass('remove-splitter'); + dropdown['show'](); + } + else { + wrapper.removeClass('remove-splitter'); + dropdown['hide'](); + } + }); + + // initial state + var currentVal = this.find('input[name=' + this.attr('id') + ']:checked').val(); + dropdown[currentVal == 'Custom' ? 'show' : 'hide'](); + + this._super(); + }, + onunmatch: function() { + this._super(); + } + }); + }); }); \ No newline at end of file