API CMS Editable notification templates

PSR-2 Coding fixup
This commit is contained in:
Damian Mooyman 2015-11-17 14:17:54 +13:00
parent 6138d0b927
commit d9729bf7f1
14 changed files with 320 additions and 141 deletions

View File

@ -6,7 +6,6 @@
*/ */
class ContentReviewCMSExtension extends LeftAndMainExtension class ContentReviewCMSExtension extends LeftAndMainExtension
{ {
/** /**
* @var array * @var array
*/ */

View File

@ -9,23 +9,49 @@
class ContentReviewDefaultSettings extends DataExtension class ContentReviewDefaultSettings extends DataExtension
{ {
/** /**
* @config
*
* @var array * @var array
*/ */
private static $db = 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' => '<h2>Page(s) due for review</h2><p>There are $PagesCount pages that are due for review today by you.</p>',
);
/**
* @config
* *
* @var array * @var array
*/ */
private static $many_many = array( private static $many_many = array(
"ContentReviewGroups" => "Group", 'ContentReviewGroups' => 'Group',
"ContentReviewUsers" => "Member", '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 * @return string
*/ */
public function getOwnerNames() public function getOwnerNames()
@ -33,14 +59,14 @@ class ContentReviewDefaultSettings extends DataExtension
$names = array(); $names = array();
foreach ($this->OwnerGroups() as $group) { foreach ($this->OwnerGroups() as $group) {
$names[] = $group->getBreadcrumbs(" > "); $names[] = $group->getBreadcrumbs(' > ');
} }
foreach ($this->OwnerUsers() as $group) { foreach ($this->OwnerUsers() as $group) {
$names[] = $group->getName(); $names[] = $group->getName();
} }
return implode(", ", $names); return implode(', ', $names);
} }
/** /**
@ -48,7 +74,7 @@ class ContentReviewDefaultSettings extends DataExtension
*/ */
public function OwnerGroups() public function OwnerGroups()
{ {
return $this->owner->getManyManyComponents("ContentReviewGroups"); return $this->owner->getManyManyComponents('ContentReviewGroups');
} }
/** /**
@ -56,54 +82,78 @@ class ContentReviewDefaultSettings extends DataExtension
*/ */
public function OwnerUsers() public function OwnerUsers()
{ {
return $this->owner->getManyManyComponents("ContentReviewUsers"); return $this->owner->getManyManyComponents('ContentReviewUsers');
} }
/** /**
*
* @param FieldList $fields * @param FieldList $fields
*/ */
public function updateCMSFields(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()) $reviewFrequency = DropdownField::create(
->setDescription(_t("ContentReview.REVIEWFREQUENCYDESCRIPTION", "The review date will be set to this far in the future whenever the page is published")); '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( $users = Permission::get_members_by_permission(array(
"CMS_ACCESS_CMSMain", 'CMS_ACCESS_CMSMain',
"ADMIN", 'ADMIN',
)); ));
$usersMap = $users->map("ID", "Title")->toArray(); $usersMap = $users->map('ID', 'Title')->toArray();
asort($usersMap); asort($usersMap);
$userField = ListboxField::create("OwnerUsers", _t("ContentReview.PAGEOWNERUSERS", "Users"), $usersMap) $userField = ListboxField::create('OwnerUsers', _t('ContentReview.PAGEOWNERUSERS', 'Users'), $usersMap)
->setMultiple(true) ->setMultiple(true)
->setAttribute("data-placeholder", _t("ContentReview.ADDUSERS", "Add users")) ->setAttribute('data-placeholder', _t('ContentReview.ADDUSERS', 'Add users'))
->setDescription(_t("ContentReview.OWNERUSERSDESCRIPTION", "Page owners that are responsible for reviews")); ->setDescription(_t('ContentReview.OWNERUSERSDESCRIPTION', 'Page owners that are responsible for reviews'));
$fields->addFieldToTab("Root.ContentReview", $userField); $fields->addFieldToTab('Root.ContentReview', $userField);
$groupsMap = array(); $groupsMap = array();
foreach (Group::get() as $group) { foreach (Group::get() as $group) {
// Listboxfield values are escaped, use ASCII char instead of &raquo; // Listboxfield values are escaped, use ASCII char instead of &raquo;
$groupsMap[$group->ID] = $group->getBreadcrumbs(" > "); $groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
} }
asort($groupsMap); asort($groupsMap);
$groupField = ListboxField::create("OwnerGroups", _t("ContentReview.PAGEOWNERGROUPS", "Groups"), $groupsMap) $groupField = ListboxField::create('OwnerGroups', _t('ContentReview.PAGEOWNERGROUPS', 'Groups'), $groupsMap)
->setMultiple(true) ->setMultiple(true)
->setAttribute("data-placeholder", _t("ContentReview.ADDGROUP", "Add groups")) ->setAttribute('data-placeholder', _t('ContentReview.ADDGROUP', 'Add groups'))
->setDescription(_t("ContentReview.OWNERGROUPSDESCRIPTION", "Page owners that are responsible for reviews")); ->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 * @return ArrayList
*/ */
public function ContentReviewOwners() { public function ContentReviewOwners()
{
return SiteTreeContentReview::merge_owners($this->OwnerGroups(), $this->OwnerUsers()); 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];
}
}
} }

View File

@ -122,7 +122,7 @@ class SiteTreeContentReview extends DataExtension implements PermissionProvider
Requirements::css("contentreview/css/contentreview.css"); Requirements::css("contentreview/css/contentreview.css");
$reviewTitle = LiteralField::create( $reviewTitle = LiteralField::create(
"ReviewContentNotesLabel", "ReviewContentNotesLabel",
"<label class=\"left\" for=\"Form_EditForm_ReviewNotes\">" . _t("ContentReview.CONTENTREVIEW", "Content due for review") . "</label>" "<label class=\"left\" for=\"Form_EditForm_ReviewNotes\">" . _t("ContentReview.CONTENTREVIEW", "Content due for review") . "</label>"
); );

View File

@ -89,7 +89,7 @@ class PagesDueForReviewReport extends SS_Report
), ),
"ContentReviewType" => array( "ContentReviewType" => array(
"title" => "Settings are", "title" => "Settings are",
"formatting" => function ($value, $item) use ($linkPath,$linkQuery) { "formatting" => function ($value, $item) use ($linkPath, $linkQuery) {
if ($item->ContentReviewType == "Inherit") { if ($item->ContentReviewType == "Inherit") {
$options = $item->getOptions(); $options = $item->getOptions();
if ($options && $options instanceof SiteConfig) { if ($options && $options instanceof SiteConfig) {
@ -117,7 +117,8 @@ class PagesDueForReviewReport extends SS_Report
* @param array $params * @param array $params
* *
* @return SS_List * @return SS_List
*/public function sourceRecords($params = array()) */
public function sourceRecords($params = array())
{ {
Versioned::reading_stage("Stage"); Versioned::reading_stage("Stage");

View File

@ -5,13 +5,6 @@
*/ */
class ContentReviewEmails extends BuildTask 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 * @param SS_HTTPRequest $request
*/ */
@ -19,10 +12,9 @@ class ContentReviewEmails extends BuildTask
{ {
$compatibility = ContentReviewCompatability::start(); $compatibility = ContentReviewCompatability::start();
$now = class_exists("SS_Datetime") ? SS_Datetime::now()->URLDate() : SSDatetime::now()->URLDate();
// First grab all the pages with a custom setting // 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); $overduePages = $this->getOverduePagesForOwners($pages);
@ -52,10 +44,6 @@ class ContentReviewEmails extends BuildTask
$option = $page->getOptions(); $option = $page->getOptions();
foreach ($option->ContentReviewOwners() as $owner) { foreach ($option->ContentReviewOwners() as $owner) {
if (!isset(self::$member_cache[$owner->ID])) {
self::$member_cache[$owner->ID] = $owner;
}
if (!isset($overduePages[$owner->ID])) { if (!isset($overduePages[$owner->ID])) {
$overduePages[$owner->ID] = new ArrayList(); $overduePages[$owner->ID] = new ArrayList();
} }
@ -73,22 +61,69 @@ class ContentReviewEmails extends BuildTask
*/ */
protected function notifyOwner($ownerID, SS_List $pages) protected function notifyOwner($ownerID, SS_List $pages)
{ {
$owner = self::$member_cache[$ownerID]; // Prepare variables
$sender = Security::findAnAdministrator(); $siteConfig = SiteConfig::current_site_config();
$senderEmail = ($sender->Email) ? $sender->Email : Config::inst()->get("Email", "admin_email"); $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 = new Email();
$email->setTo($owner->Email); $email->setTo($owner->Email);
$email->setFrom($senderEmail); $email->setFrom($siteConfig->ReviewFrom);
$email->setTemplate("ContentReviewEmail"); $email->setSubject($siteConfig->ReviewSubject);
$email->setSubject($subject);
$email->populateTemplate(array(
"Recipient" => $owner,
"Sender" => $sender,
"Pages" => $pages,
));
// 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(); $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,
);
}
} }

BIN
docs/.DS_Store vendored

Binary file not shown.

BIN
docs/en/.DS_Store vendored

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

View File

@ -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. * 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. * 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 ## 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 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

View File

@ -0,0 +1,10 @@
<p>This is a list of dynamic variables that you can use in the email templates.</p>
<ul>
<li>&#36;Subject - Email subject line</li>
<li>&#36;PagesCount - Number of pages pending review</li>
<li>&#36;FromEmail - Sender email address</li>
<li>&#36;ToFirstName - The email receivers first name</li>
<li>&#36;ToSurname - The email receivers surname</li>
<li>&#36;ToEmail - The email receivers email</li>
</ul>

View File

@ -6,8 +6,7 @@
<tbody> <tbody>
<tr> <tr>
<td scope="row" colspan="2" class="typography"> <td scope="row" colspan="2" class="typography">
<h2><% _t('ContentReviewEmails.EMAIL_HEADING','Page(s) due for review') %></h2> $EmailBody
<p>There are $Pages.Count pages that are due for review today by you.</p>
</td> </td>
</tr> </tr>
<% loop Pages %> <% loop Pages %>

View File

@ -6,7 +6,6 @@
*/ */
abstract class ContentReviewBaseTest extends FunctionalTest abstract class ContentReviewBaseTest extends FunctionalTest
{ {
/** /**
* @var bool * @var bool
*/ */

View File

@ -8,39 +8,57 @@ class ContentReviewNotificationTest extends SapphireTest
/** /**
* @var string * @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 * @var array
*/ */
protected $requiredExtensions = array( protected $requiredExtensions = array(
"SiteTree" => array("SiteTreeContentReview"), 'SiteTree' => array('SiteTreeContentReview'),
"Group" => array("ContentReviewOwner"), 'Group' => array('ContentReviewOwner'),
"Member" => array("ContentReviewOwner"), 'Member' => array('ContentReviewOwner'),
"CMSPageEditController" => array("ContentReviewCMSExtension"), 'CMSPageEditController' => array('ContentReviewCMSExtension'),
"SiteConfig" => array("ContentReviewDefaultSettings"), 'SiteConfig' => array('ContentReviewDefaultSettings'),
); );
public function testContentReviewEmails() 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 */ /** @var Page|SiteTreeContentReview $childParentPage */
$childParentPage = $this->objFromFixture("Page", "contact"); $childParentPage = $this->objFromFixture('Page', 'contact');
$childParentPage->NextReviewDate = "2010-02-23"; $childParentPage->NextReviewDate = '2010-02-23';
$childParentPage->write(); $childParentPage->write();
$task = new ContentReviewEmails(); $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"); // Set template variables (as per variable case)
$email = $this->findEmail("author@example.com", null, $expectedSubject); $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->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(
$this->assertContains("Staff", $email["htmlContent"]); "<h1>$Subject</h1><p>There are $PagesCount pages that are due for review today by you, $ToFirstName.</p><p>This email was sent to $ToEmail</p>",
$this->assertContains("Contact Us", $email["htmlContent"]); $email['htmlContent']
$this->assertContains("Contact Us Child", $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(); SS_Datetime::clear_mock_now();
} }

View File

@ -1,76 +1,82 @@
SiteConfig:
mysiteconfig:
ReviewFrom: sender@silverstripe.com
ReviewSubject: 'Please log in to review some content!'
ReviewBody: '<h1>$Subject</h1><p>There are $PagesCount pages that are due for review today by you, $ToFirstName.</p><p>This email was sent to $ToEmail</p>'
Permission: Permission:
cmsmain1: cmsmain1:
Code: CMS_ACCESS_CMSMain Code: CMS_ACCESS_CMSMain
cmsmain2: cmsmain2:
Code: CMS_ACCESS_CMSMain Code: CMS_ACCESS_CMSMain
setreviewdates: setreviewdates:
Code: EDIT_CONTENT_REVIEW_FIELDS Code: EDIT_CONTENT_REVIEW_FIELDS
workflowadmin1: workflowadmin1:
Code: IS_WORKFLOW_ADMIN Code: IS_WORKFLOW_ADMIN
workflowadmin2: workflowadmin2:
Code: IS_WORKFLOW_ADMIN Code: IS_WORKFLOW_ADMIN
Group: Group:
editorgroup: editorgroup:
Title: Edit existing pages Title: Edit existing pages
Code: editorgroup Code: editorgroup
Permissions: =>Permission.cmsmain1,=>Permission.workflowadmin1,=>Permission.setreviewdates Permissions: =>Permission.cmsmain1,=>Permission.workflowadmin1,=>Permission.setreviewdates
authorgroup: authorgroup:
Title: Author existing pages Title: Author existing pages
Code: authorgroup Code: authorgroup
Permissions: =>Permission.cmsmain2,=>Permission.workflowadmin2 Permissions: =>Permission.cmsmain2,=>Permission.workflowadmin2
Member: Member:
author: author:
FirstName: Test FirstName: Test
Surname: Author Surname: Author
Email: author@example.com Email: author@example.com
Groups: =>Group.authorgroup Groups: =>Group.authorgroup
editor: editor:
FirstName: Test FirstName: Test
Surname: Editor Surname: Editor
Groups: =>Group.editorgroup Groups: =>Group.editorgroup
visitor: visitor:
FirstName: Kari FirstName: Kari
Surname: Visitor Surname: Visitor
Email: visitor@example.com Email: visitor@example.com
Page: Page:
# Cant be reviewed, no owners # Cant be reviewed, no owners
home: home:
Title: Home Title: Home
ContentReviewType: Custom ContentReviewType: Custom
NextReviewDate: 2010-02-01 NextReviewDate: 2010-02-01
ReviewPeriodDays: 10 ReviewPeriodDays: 10
# Cant be reviewed, no owners # Cant be reviewed, no owners
about: about:
Title: About Us Title: About Us
ContentReviewType: Custom ContentReviewType: Custom
NextReviewDate: 2010-02-07 NextReviewDate: 2010-02-07
ReviewPeriodDays: 10 ReviewPeriodDays: 10
staff: staff:
Title: Staff Title: Staff
ContentReviewType: Custom ContentReviewType: Custom
NextReviewDate: 2010-02-14 NextReviewDate: 2010-02-14
ReviewPeriodDays: 10 ReviewPeriodDays: 10
ContentReviewUsers: =>Member.author ContentReviewUsers: =>Member.author
contact: contact:
Title: Contact Us Title: Contact Us
ContentReviewType: Custom ContentReviewType: Custom
ReviewPeriodDays: 10 ReviewPeriodDays: 10
NextReviewDate: 2010-02-21 NextReviewDate: 2010-02-21
ContentReviewGroups: =>Group.authorgroup ContentReviewGroups: =>Group.authorgroup
contact-child: contact-child:
Title: Contact Us Child Title: Contact Us Child
ContentReviewType: Inherit ContentReviewType: Inherit
ParentID: =>Page.contact ParentID: =>Page.contact
# Cant be reviewed, no NextReviewDate # Cant be reviewed, no NextReviewDate
no-review: no-review:
Title: Page without review date Title: Page without review date
ContentReviewType: Custom ContentReviewType: Custom
ContentReviewUsers: =>Member.author ContentReviewUsers: =>Member.author
# Cant be reviewed, no NextReviewDate # Cant be reviewed, no NextReviewDate
group-owned: group-owned:
Title: Page owned by group Title: Page owned by group
ContentReviewType: Custom ContentReviewType: Custom
ContentReviewGroups: =>Group.authorgroup ContentReviewGroups: =>Group.authorgroup