Merge pull request #594 from tractorcow/pull/4/categorisation-rewrite

Category / Tag Rewrite - Remove dependency on Blog
This commit is contained in:
Robbie Averill 2020-03-26 11:05:47 -07:00 committed by GitHub
commit 3472295f62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 710 additions and 429 deletions

View File

@ -12,11 +12,11 @@ use SilverStripe\ORM\SS_List;
class GridFieldCategorisationConfig extends GridFieldConfig_RecordEditor class GridFieldCategorisationConfig extends GridFieldConfig_RecordEditor
{ {
/** /**
* @param int $itemsPerPage * @param int $itemsPerPage
* @param array|SS_List $mergeRecords * @param array|SS_List $mergeRecords
* @param string $parentType * @param string $parentType
* @param string $parentMethod * @param string $parentMethod
* @param string $childMethod * @param string $childMethod
*/ */
public function __construct($itemsPerPage, $mergeRecords, $parentType, $parentMethod, $childMethod) public function __construct($itemsPerPage, $mergeRecords, $parentType, $parentMethod, $childMethod)
{ {
@ -39,31 +39,21 @@ class GridFieldCategorisationConfig extends GridFieldConfig_RecordEditor
$columns->setFieldFormatting( $columns->setFieldFormatting(
[ [
'BlogPostsCount' => function ($value, CategorisationObject $item) { 'BlogPostsCount' => function ($value, CategorisationObject $item) {
return $item->getBlogCount();
},
'BlogPostsAllCount' => function ($value, CategorisationObject $item) {
return $item->BlogPosts()->Count(); return $item->BlogPosts()->Count();
} },
] ]
); );
$this->changeColumnOrder();
}
/**
* Reorders GridField columns so that Actions is last.
*/
protected function changeColumnOrder()
{
/**
* @var GridFieldDataColumns $columns
*/
$columns = $this->getComponentByType(GridFieldDataColumns::class);
$columns->setDisplayFields( $columns->setDisplayFields(
[ [
'Title' => _t(__CLASS__ . '.Title', 'Title'), 'Title' => _t(__CLASS__ . '.Title', 'Title'),
'BlogPostsCount' => _t(__CLASS__ . '.Posts', 'Posts'), 'BlogPostsCount' => _t(__CLASS__ . '.PostsThisBlog', 'Posts (This Blog)'),
'MergeAction' => 'MergeAction', 'BlogPostsAllCount' => _t(__CLASS__ . '.PostsAllBlogs', 'Posts (All Blogs)'),
'Actions' => 'Actions' 'MergeAction' => 'MergeAction',
'Actions' => 'Actions'
] ]
); );
} }

View File

@ -99,6 +99,7 @@ class GridFieldAddByDBField implements GridField_ActionProvider, GridField_HTMLP
$obj->setCastedField($dbField, $data['gridfieldaddbydbfield'][$obj->ClassName][$dbField]); $obj->setCastedField($dbField, $data['gridfieldaddbydbfield'][$obj->ClassName][$dbField]);
if ($obj->canCreate()) { if ($obj->canCreate()) {
$obj->write();
$id = $gridField->getList()->add($obj); $id = $gridField->getList()->add($obj);
if (!$id) { if (!$id) {
$gridField->setCustomValidationMessage( $gridField->setCustomValidationMessage(

View File

@ -2,10 +2,10 @@
namespace SilverStripe\Blog\Model; namespace SilverStripe\Blog\Model;
use Exception;
use Page; use Page;
use SilverStripe\Blog\Admin\GridFieldCategorisationConfig; use SilverStripe\Blog\Admin\GridFieldCategorisationConfig;
use SilverStripe\Blog\Forms\GridField\GridFieldConfigBlogPost; use SilverStripe\Blog\Forms\GridField\GridFieldConfigBlogPost;
use SilverStripe\CMS\Controllers\RootURLController;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
@ -14,13 +14,15 @@ use SilverStripe\Forms\GridField\GridFieldConfig;
use SilverStripe\Forms\ListboxField; use SilverStripe\Forms\ListboxField;
use SilverStripe\Forms\LiteralField; use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\NumericField; use SilverStripe\Forms\NumericField;
use SilverStripe\Forms\Tab;
use SilverStripe\Forms\TabSet;
use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\ORM\HasManyList;
use SilverStripe\ORM\ManyManyList; use SilverStripe\ORM\ManyManyList;
use SilverStripe\ORM\SS_List; use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\UnsavedRelationList; use SilverStripe\ORM\UnsavedRelationList;
use SilverStripe\ORM\ValidationException;
use SilverStripe\Security\Group; use SilverStripe\Security\Group;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
@ -31,8 +33,7 @@ use SilverStripe\View\Requirements;
/** /**
* Blog Holder * Blog Holder
* *
* @method HasManyList Tags() List of tags in this blog * @property int $PostsPerPage
* @method HasManyList Categories() List of categories in this blog
* @method ManyManyList Editors() List of editors * @method ManyManyList Editors() List of editors
* @method ManyManyList Writers() List of writers * @method ManyManyList Writers() List of writers
* @method ManyManyList Contributors() List of contributors * @method ManyManyList Contributors() List of contributors
@ -88,20 +89,12 @@ class Blog extends Page implements PermissionProvider
'PostsPerPage' => 'Int', 'PostsPerPage' => 'Int',
]; ];
/**
* @var array
*/
private static $has_many = [
'Tags' => BlogTag::class,
'Categories' => BlogCategory::class,
];
/** /**
* @var array * @var array
*/ */
private static $many_many = [ private static $many_many = [
'Editors' => Member::class, 'Editors' => Member::class,
'Writers' => Member::class, 'Writers' => Member::class,
'Contributors' => Member::class, 'Contributors' => Member::class,
]; ];
@ -134,6 +127,48 @@ class Blog extends Page implements PermissionProvider
private static $icon_class = 'font-icon-p-posts'; private static $icon_class = 'font-icon-p-posts';
/**
* Gets the list of all tags attached to this blog
*
* @param bool $hideEmpty Set to false to include tags without posts
* @return DataList|BlogTag[]
*/
public function Tags($hideEmpty = true)
{
$tags = BlogTag::get()->setDataQueryParam('BlogID', $this->ID);
// Conditionally hide empty tags
if ($this->ID && $hideEmpty) {
$tags = $tags->filter([
'BlogPosts.ParentID' => $this->ID,
]);
}
$this->extend('updateBlogTags', $tags);
return $tags;
}
/**
* Gets the list of all categories attached to this blog
*
* @param bool $hideEmpty Set to false to include categories without posts
* @return DataList|BlogCategory[]
*/
public function Categories($hideEmpty = true)
{
$tags = BlogCategory::get()->setDataQueryParam('BlogID', $this->ID);
// Conditionally hide empty categories
if ($this->ID && $hideEmpty) {
$tags = $tags->filter([
'BlogPosts.ParentID' => $this->ID,
]);
}
$this->extend('updateBlogCategories', $tags);
return $tags;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@ -141,18 +176,18 @@ class Blog extends Page implements PermissionProvider
{ {
$this->addCMSRequirements(); $this->addCMSRequirements();
$this->beforeUpdateCMSFields(function ($fields) { $this->beforeUpdateCMSFields(function (FieldList $fields) {
if (!$this->canEdit()) { if (!$this->canEdit()) {
return; return;
} }
$categories = GridField::create( $categories = GridField::create(
'Categories', 'Categories',
_t(__CLASS__ . '.Categories', 'Categories'), _t(__CLASS__ . '.Categories', 'Categories'),
$this->Categories(), $this->Categories(false),
GridFieldCategorisationConfig::create( GridFieldCategorisationConfig::create(
15, 15,
$this->Categories()->sort('Title'), $this->Categories(false)->sort('Title'),
BlogCategory::class, BlogCategory::class,
'Categories', 'Categories',
'BlogPosts' 'BlogPosts'
@ -162,30 +197,32 @@ class Blog extends Page implements PermissionProvider
$tags = GridField::create( $tags = GridField::create(
'Tags', 'Tags',
_t(__CLASS__ . '.Tags', 'Tags'), _t(__CLASS__ . '.Tags', 'Tags'),
$this->Tags(), $this->Tags(false),
GridFieldCategorisationConfig::create( GridFieldCategorisationConfig::create(
15, 15,
$this->Tags()->sort('Title'), $this->Tags(false)->sort('Title'),
BlogTag::class, BlogTag::class,
'Tags', 'Tags',
'BlogPosts' 'BlogPosts'
) )
); );
/** $fields->addFieldToTab(
* @var FieldList $fields 'Root',
*/ TabSet::create('Categorisation', _t(__CLASS__ . '.Categorisation', 'Categorisation'))
$fields->addFieldsToTab( ->addExtraClass('blog-cms-categorisation')
);
$fields->addFieldToTab(
'Root.Categorisation', 'Root.Categorisation',
[ Tab::create('Categories', _t(__CLASS__ . '.Categories', 'Categories'))
$categories, );
$tags $fields->addFieldToTab(
] 'Root.Categorisation',
Tab::create('Tags', _t(__CLASS__ . '.Tags', 'Tags'))
); );
$fields->fieldByName('Root.Categorisation') $fields->addFieldToTab('Root.Categorisation.Categories', $categories);
->addExtraClass('blog-cms-categorisation') $fields->addFieldToTab('Root.Categorisation.Tags', $tags);
->setTitle(_t(__CLASS__ . '.Categorisation', 'Categorisation'));
}); });
return parent::getCMSFields(); return parent::getCMSFields();
@ -199,6 +236,7 @@ class Blog extends Page implements PermissionProvider
Requirements::css('silverstripe/blog:client/dist/styles/main.css'); Requirements::css('silverstripe/blog:client/dist/styles/main.css');
Requirements::javascript('silverstripe/blog:client/dist/js/main.bundle.js'); Requirements::javascript('silverstripe/blog:client/dist/js/main.bundle.js');
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@ -249,7 +287,7 @@ class Blog extends Page implements PermissionProvider
/** /**
* Determine if the given member belongs to the given relation. * Determine if the given member belongs to the given relation.
* *
* @param Member $member * @param Member $member
* @param DataList $relation * @param DataList $relation
* *
* @return bool * @return bool
@ -527,7 +565,7 @@ class Blog extends Page implements PermissionProvider
/** /**
* Returns BlogPosts for a given date period. * Returns BlogPosts for a given date period.
* *
* @param int $year * @param int $year
* @param null|int $month * @param null|int $month
* @param null|int $day * @param null|int $day
* *
@ -592,13 +630,7 @@ class Blog extends Page implements PermissionProvider
*/ */
public function ProfileLink($urlSegment) public function ProfileLink($urlSegment)
{ {
$baseLink = $this->Link(); return Controller::join_links($this->Link('Profile'), $urlSegment);
if ($baseLink === '/') {
// Handle homepage blogs
$baseLink = RootURLController::get_homepage_link();
}
return Controller::join_links($baseLink, 'profile', $urlSegment);
} }
/** /**
@ -628,22 +660,22 @@ class Blog extends Page implements PermissionProvider
{ {
return [ return [
Blog::MANAGE_USERS => [ Blog::MANAGE_USERS => [
'name' => _t( 'name' => _t(
__CLASS__ . '.PERMISSION_MANAGE_USERS_DESCRIPTION', __CLASS__ . '.PERMISSION_MANAGE_USERS_DESCRIPTION',
'Manage users for individual blogs' 'Manage users for individual blogs'
), ),
'help' => _t( 'help' => _t(
__CLASS__ . '.PERMISSION_MANAGE_USERS_HELP', __CLASS__ . '.PERMISSION_MANAGE_USERS_HELP',
'Allow assignment of Editors, Writers, or Contributors to blogs' 'Allow assignment of Editors, Writers, or Contributors to blogs'
), ),
'category' => _t(__CLASS__ . '.PERMISSIONS_CATEGORY', 'Blog permissions'), 'category' => _t(__CLASS__ . '.PERMISSIONS_CATEGORY', 'Blog permissions'),
'sort' => 100 'sort' => 100
] ]
]; ];
} }
/** /**
* {@inheritdoc} * @throws ValidationException
*/ */
protected function onBeforeWrite() protected function onBeforeWrite()
{ {
@ -653,6 +685,9 @@ class Blog extends Page implements PermissionProvider
/** /**
* Assign users as necessary to the blog group. * Assign users as necessary to the blog group.
*
* @throws ValidationException
* @throws Exception
*/ */
protected function assignGroup() protected function assignGroup()
{ {
@ -665,6 +700,7 @@ class Blog extends Page implements PermissionProvider
// Must check if the method exists or else an error occurs when changing page type // Must check if the method exists or else an error occurs when changing page type
if ($this->hasMethod('Editors')) { if ($this->hasMethod('Editors')) {
foreach ([$this->Editors(), $this->Writers(), $this->Contributors()] as $levels) { foreach ([$this->Editors(), $this->Writers(), $this->Contributors()] as $levels) {
/** @var Member $user */
foreach ($levels as $user) { foreach ($levels as $user) {
if (!$user->inGroup($group)) { if (!$user->inGroup($group)) {
$user->Groups()->add($group); $user->Groups()->add($group);
@ -678,13 +714,14 @@ class Blog extends Page implements PermissionProvider
* Gets or creates the group used to assign CMS access. * Gets or creates the group used to assign CMS access.
* *
* @return Group * @return Group
* @throws ValidationException
*/ */
protected function getUserGroup() protected function getUserGroup()
{ {
$code = $this->config()->get('grant_user_group'); $code = $this->config()->get('grant_user_group');
/** @var Group $group */
$group = Group::get()->filter('Code', $code)->first(); $group = Group::get()->filter('Code', $code)->first();
if ($group) { if ($group) {
return $group; return $group;
} }

View File

@ -3,16 +3,14 @@
namespace SilverStripe\Blog\Model; namespace SilverStripe\Blog\Model;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ManyManyList;
/** /**
* A blog category for generalising blog posts. * A blog category for generalising blog posts.
* *
* * @method ManyManyList|BlogPost[] BlogPosts()
* @method Blog Blog()
*
* @property string $Title * @property string $Title
* @property string $URLSegment * @property string $URLSegment
* @property int $BlogID
*/ */
class BlogCategory extends DataObject implements CategorisationObject class BlogCategory extends DataObject implements CategorisationObject
{ {
@ -44,8 +42,8 @@ class BlogCategory extends DataObject implements CategorisationObject
/** /**
* @var array * @var array
*/ */
private static $has_one = [ private static $indexes = [
'Blog' => Blog::class, 'URLSegment' => true,
]; ];
/** /**
@ -55,6 +53,11 @@ class BlogCategory extends DataObject implements CategorisationObject
'BlogPosts' => BlogPost::class, 'BlogPosts' => BlogPost::class,
]; ];
/**
* @var string
*/
private static $default_sort = '"BlogCategory"."Title" ASC';
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */

View File

@ -6,14 +6,17 @@ use PageController;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPResponse_Exception; use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Control\RSS\RSSFeed; use SilverStripe\Control\RSS\RSSFeed;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataList;
use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\ORM\PaginatedList; use SilverStripe\ORM\PaginatedList;
use SilverStripe\ORM\SS_List;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\View\Parsers\URLSegmentFilter; use SilverStripe\View\Parsers\URLSegmentFilter;
use SilverStripe\Control\HTTPRequest;
/**
* @method Blog data()
*/
class BlogController extends PageController class BlogController extends PageController
{ {
/** /**
@ -31,17 +34,17 @@ class BlogController extends PageController
* @var array * @var array
*/ */
private static $url_handlers = [ private static $url_handlers = [
'tag/$Tag!/$Rss' => 'tag', 'tag/$Tag!/$Rss' => 'tag',
'category/$Category!/$Rss' => 'category', 'category/$Category!/$Rss' => 'category',
'archive/$Year!/$Month/$Day' => 'archive', 'archive/$Year!/$Month/$Day' => 'archive',
'profile/$URLSegment!' => 'profile' 'profile/$Profile!' => 'profile'
]; ];
/** /**
* @var array * @var array
*/ */
private static $casting = [ private static $casting = [
'MetaTitle' => 'Text', 'MetaTitle' => 'Text',
'FilterDescription' => 'Text' 'FilterDescription' => 'Text'
]; ];
@ -60,27 +63,11 @@ class BlogController extends PageController
*/ */
protected $blogPosts; protected $blogPosts;
/**
* @return string
*/
public function index(HTTPRequest $request)
{
/**
* @var Blog $dataRecord
*/
$dataRecord = $this->dataRecord;
$this->blogPosts = $dataRecord->getBlogPosts();
return $this->render();
}
/** /**
* Renders a Blog Member's profile. * Renders a Blog Member's profile.
* *
* @throws HTTPResponse_Exception * @throws HTTPResponse_Exception
* * @return $this
* @return string
*/ */
public function profile() public function profile()
{ {
@ -88,45 +75,59 @@ class BlogController extends PageController
$this->httpError(404, 'Not Found'); $this->httpError(404, 'Not Found');
} }
$profile = $this->getCurrentProfile(); // Get profile posts
$posts = $this->getCurrentProfilePosts();
if (!$profile) { if (!$posts) {
return $this->httpError(404, 'Not Found'); $this->httpError(404, 'Not Found');
} }
$this->blogPosts = $this->getCurrentProfilePosts(); $this->setFilteredPosts($posts);
return $this;
return $this->render();
} }
/** /**
* Get the Member associated with the current URL segment. * Get the Member associated with the current URL segment.
* *
* @return null|Member * @return null|Member|BlogMemberExtension
*/ */
public function getCurrentProfile() public function getCurrentProfile()
{ {
$urlSegment = $this->request->param('URLSegment'); $segment = $this->getCurrentProfileURLSegment();
if ($urlSegment) { if (!$segment) {
$filter = URLSegmentFilter::create(); return null;
// url encode unless it's multibyte (already pre-encoded in the database)
// see https://github.com/silverstripe/silverstripe-cms/pull/2384
if (!$filter->getAllowMultibyte()) {
$urlSegment = rawurlencode($urlSegment);
}
return Member::get()
->filter('URLSegment', $urlSegment)
->first();
} }
return null; /** @var Member $profile */
$profile = Member::get()
->find('URLSegment', $segment);
return $profile;
}
/**
* Get URL Segment of current profile
*
* @return null|string
*/
public function getCurrentProfileURLSegment()
{
$segment = isset($this->urlParams['Profile'])
? $this->urlParams['Profile']
: null;
if (!$segment) {
return null;
}
// url encode unless it's multibyte (already pre-encoded in the database)
// see https://github.com/silverstripe/silverstripe-cms/pull/2384
return URLSegmentFilter::singleton()->getAllowMultibyte()
? $segment
: rawurlencode($segment);
} }
/** /**
* Get posts related to the current Member profile. * Get posts related to the current Member profile.
* *
* @return null|DataList * @return null|DataList|BlogPost[]
*/ */
public function getCurrentProfilePosts() public function getCurrentProfilePosts()
{ {
@ -142,166 +143,200 @@ class BlogController extends PageController
/** /**
* Renders an archive for a specified date. This can be by year or year/month. * Renders an archive for a specified date. This can be by year or year/month.
* *
* @return null|string * @return $this
* @throws HTTPResponse_Exception
*/ */
public function archive() public function archive()
{ {
/**
* @var Blog $dataRecord
*/
$dataRecord = $this->dataRecord;
$year = $this->getArchiveYear(); $year = $this->getArchiveYear();
$month = $this->getArchiveMonth(); $month = $this->getArchiveMonth();
$day = $this->getArchiveDay(); $day = $this->getArchiveDay();
if ($this->request->param('Month') && !$month) { // Validate all values
if ($year === false || $month === false || $day === false) {
$this->httpError(404, 'Not Found'); $this->httpError(404, 'Not Found');
} }
if ($month && $this->request->param('Day') && !$day) { $posts = $this->data()->getArchivedBlogPosts($year, $month, $day);
$this->httpError(404, 'Not Found'); $this->setFilteredPosts($posts);
} return $this;
if ($year) {
$this->blogPosts = $dataRecord->getArchivedBlogPosts($year, $month, $day);
return $this->render();
}
$this->httpError(404, 'Not Found');
return null;
} }
/** /**
* Fetches the archive year from the url. * Fetches the archive year from the url.
* *
* @return int * Returns int if valid, current year if not provided, false if invalid value
*
* @return int|false
*/ */
public function getArchiveYear() public function getArchiveYear()
{ {
if ($this->request->param('Year')) { if (isset($this->urlParams['Year'])
if (preg_match('/^[0-9]{4}$/', $year = $this->request->param('Year'))) { && preg_match('/^[0-9]{4}$/', $this->urlParams['Year'])
return (int) $year; ) {
} return (int)$this->urlParams['Year'];
} elseif ($this->request->param('Action') == 'archive') { }
if (empty($this->urlParams['Year']) &&
$this->urlParams['Action'] === 'archive'
) {
return DBDatetime::now()->Year(); return DBDatetime::now()->Year();
} }
return null; return false;
} }
/** /**
* Fetches the archive money from the url. * Fetches the archive money from the url.
* *
* @return null|int * Returns int if valid, null if not provided, false if invalid value
*
* @return null|int|false
*/ */
public function getArchiveMonth() public function getArchiveMonth()
{ {
$month = $this->request->param('Month'); $month = isset($this->urlParams['Month'])
? $this->urlParams['Month']
: null;
if (preg_match('/^[0-9]{1,2}$/', $month)) { if (!$month) {
if ($month > 0 && $month < 13) { return null;
if (checkdate($month, 01, $this->getArchiveYear())) {
return (int) $month;
}
}
} }
return null; if (preg_match('/^[0-9]{1,2}$/', $month)
&& $month > 0
&& $month < 13
) {
return (int)$month;
}
return false;
} }
/** /**
* Fetches the archive day from the url. * Fetches the archive day from the url.
* *
* @return null|int * Returns int if valid, null if not provided, false if invalid value
*
* @return null|int|false
*/ */
public function getArchiveDay() public function getArchiveDay()
{ {
$day = $this->request->param('Day'); $day = isset($this->urlParams['Day'])
? $this->urlParams['Day']
: null;
if (preg_match('/^[0-9]{1,2}$/', $day)) { if (!$day) {
if (checkdate($this->getArchiveMonth(), $day, $this->getArchiveYear())) { return null;
return (int) $day;
}
} }
return null; // Cannot calculate day without month and year
$month = $this->getArchiveMonth();
$year = $this->getArchiveYear();
if (!$month || !$year) {
return false;
}
if (preg_match('/^[0-9]{1,2}$/', $day) && checkdate($month, $day, $year)) {
return (int)$day;
}
return false;
} }
/** /**
* Renders the blog posts for a given tag. * Renders the blog posts for a given tag.
* *
* @return null|string * @return DBHTMLText|$this
* @throws HTTPResponse_Exception
*/ */
public function tag() public function tag()
{ {
// Ensure tag exists
$tag = $this->getCurrentTag(); $tag = $this->getCurrentTag();
if (!$tag) {
if ($tag) { $this->httpError(404, 'Not Found');
$this->blogPosts = $tag->BlogPosts();
if ($this->isRSS()) {
return $this->rssFeed($this->blogPosts, $tag->getLink());
} else {
return $this->render();
}
} }
$this->httpError(404, 'Not Found'); // Get posts with this tag
$posts = $this
->data()
->getBlogPosts()
->filter(['Tags.URLSegment' => $tag->URLSegment]); // Soft duplicate handling
return null; $this->setFilteredPosts($posts);
// Render as RSS if provided
if ($this->isRSS()) {
return $this->rssFeed($posts, $tag->getLink());
}
return $this;
} }
/** /**
* Tag Getter for use in templates. * Get BlogTag assigned to current filter
* *
* @return null|BlogTag * @return null|BlogTag
*/ */
public function getCurrentTag() public function getCurrentTag()
{ {
/** $segment = $this->getCurrentTagURLSegment();
* @var Blog $dataRecord if (!$segment) {
*/ return null;
$dataRecord = $this->dataRecord;
$tag = $this->request->param('Tag');
if ($tag) {
$filter = URLSegmentFilter::create();
// url encode unless it's multibyte (already pre-encoded in the database)
// see https://github.com/silverstripe/silverstripe-cms/pull/2384
if (!$filter->getAllowMultibyte()) {
$tag = rawurlencode($tag);
}
return $dataRecord->Tags()
->filter('URLSegment', $tag)
->first();
} }
return null;
/** @var BlogTag $tag */
$tag = $this
->data()
->Tags(false)// Show "no results" instead of "404"
->find('URLSegment', $segment);
return $tag;
}
/**
* Get URLSegment of selected category (not: URLEncoded based on multibyte)
*
* @return string|null
*/
public function getCurrentTagURLSegment()
{
$segment = isset($this->urlParams['Tag'])
? $this->urlParams['Tag']
: null;
// url encode unless it's multibyte (already pre-encoded in the database)
// see https://github.com/silverstripe/silverstripe-cms/pull/2384
return URLSegmentFilter::singleton()->getAllowMultibyte()
? $segment
: rawurlencode($segment);
} }
/** /**
* Renders the blog posts for a given category. * Renders the blog posts for a given category.
* *
* @return null|string * @return DBHTMLText|$this
* @throws HTTPResponse_Exception
*/ */
public function category() public function category()
{ {
$category = $this->getCurrentCategory(); $category = $this->getCurrentCategory();
if ($category) { if (!$category) {
$this->blogPosts = $category->BlogPosts(); $this->httpError(404, 'Not Found');
if ($this->isRSS()) {
return $this->rssFeed($this->blogPosts, $category->getLink());
}
return $this->render();
} }
$this->httpError(404, 'Not Found'); // Get posts with this category
$posts = $this
->data()
->getBlogPosts()
->filter(['Categories.URLSegment' => $category->URLSegment]); // Soft duplicate handling
$this->setFilteredPosts($posts);
return null; if ($this->isRSS()) {
return $this->rssFeed($posts, $category->getLink());
}
return $this;
} }
/** /**
@ -311,24 +346,35 @@ class BlogController extends PageController
*/ */
public function getCurrentCategory() public function getCurrentCategory()
{ {
/** $segment = $this->getCurrentCategoryURLSegment();
* @var Blog $dataRecord if (!$segment) {
*/ return null;
$dataRecord = $this->dataRecord;
$category = $this->request->param('Category');
if ($category) {
$filter = URLSegmentFilter::create();
// url encode unless it's multibyte (already pre-encoded in the database)
// see https://github.com/silverstripe/silverstripe-cms/pull/2384
if (!$filter->getAllowMultibyte()) {
$category = rawurlencode($category);
}
return $dataRecord->Categories()
->filter('URLSegment', $category)
->first();
} }
return null;
/** @var BlogCategory $category */
$category = $this
->data()
->Categories(false)// Show "no results" instead of "404"
->find('URLSegment', $segment);
return $category;
}
/**
* Get URLSegment of selected category
*
* @return string|null
*/
public function getCurrentCategoryURLSegment()
{
$segment = isset($this->urlParams['Category'])
? $this->urlParams['Category']
: null;
// url encode unless it's multibyte (already pre-encoded in the database)
// see https://github.com/silverstripe/silverstripe-cms/pull/2384
return URLSegmentFilter::singleton()->getAllowMultibyte()
? $segment
: rawurlencode($segment);
} }
/** /**
@ -406,13 +452,13 @@ class BlogController extends PageController
); );
} }
if ($this->owner->getArchiveYear()) { if ($this->getArchiveYear()) {
if ($this->owner->getArchiveDay()) { if ($this->getArchiveDay()) {
$date = $this->owner->getArchiveDate()->Nice(); $date = $this->getArchiveDate()->Nice();
} elseif ($this->owner->getArchiveMonth()) { } elseif ($this->getArchiveMonth()) {
$date = $this->owner->getArchiveDate()->format('MMMM, y'); $date = $this->getArchiveDate()->format('MMMM, y');
} else { } else {
$date = $this->owner->getArchiveDate()->format('y'); $date = $this->getArchiveDate()->format('y');
} }
$items[] = _t( $items[] = _t(
@ -436,6 +482,28 @@ class BlogController extends PageController
return $result; return $result;
} }
/**
* Get filtered blog posts
*
* @return DataList|BlogPost[]
*/
public function getFilteredPosts()
{
return $this->blogPosts ?: $this->data()->getBlogPosts();
}
/**
* Set filtered posts
*
* @param SS_List|BlogPost[] $posts
* @return $this
*/
public function setFilteredPosts($posts)
{
$this->blogPosts = $posts;
return $this;
}
/** /**
* Returns a list of paginated blog posts based on the BlogPost dataList. * Returns a list of paginated blog posts based on the BlogPost dataList.
* *
@ -443,12 +511,12 @@ class BlogController extends PageController
*/ */
public function PaginatedList() public function PaginatedList()
{ {
$allPosts = $this->blogPosts ?: ArrayList::create(); $allPosts = $this->getFilteredPosts();
$posts = PaginatedList::create($allPosts); $posts = PaginatedList::create($allPosts);
// Set appropriate page size // Set appropriate page size
if ($this->PostsPerPage > 0) { if ($this->data()->PostsPerPage > 0) {
$pageSize = $this->PostsPerPage; $pageSize = $this->data()->PostsPerPage;
} elseif ($count = $allPosts->count()) { } elseif ($count = $allPosts->count()) {
$pageSize = $count; $pageSize = $count;
} else { } else {
@ -482,7 +550,7 @@ class BlogController extends PageController
return null; return null;
} }
/** /**
* Returns the absolute link to the previous page for use in the page meta tags. This helps search engines * Returns the absolute link to the previous page for use in the page meta tags. This helps search engines
* find the pagination and index all pages properly. * find the pagination and index all pages properly.
* *
@ -507,14 +575,7 @@ class BlogController extends PageController
*/ */
public function rss() public function rss()
{ {
/** return $this->rssFeed($this->getFilteredPosts(), $this->Link());
* @var Blog $dataRecord
*/
$dataRecord = $this->dataRecord;
$this->blogPosts = $dataRecord->getBlogPosts();
return $this->rssFeed($this->blogPosts, $this->Link());
} }
/** /**
@ -561,13 +622,18 @@ class BlogController extends PageController
* Displays an RSS feed of the given blog posts. * Displays an RSS feed of the given blog posts.
* *
* @param DataList $blogPosts * @param DataList $blogPosts
* @param string $link * @param string $link
* *
* @return string * @return DBHTMLText
*/ */
protected function rssFeed($blogPosts, $link) protected function rssFeed($blogPosts, $link)
{ {
$rss = RSSFeed::create($blogPosts, $link, $this->MetaTitle, $this->MetaDescription); $rss = RSSFeed::create(
$blogPosts,
$link,
$this->getMetaTitle(),
$this->data()->MetaDescription
);
$this->extend('updateRss', $rss); $this->extend('updateRss', $rss);
@ -581,7 +647,6 @@ class BlogController extends PageController
*/ */
protected function isRSS() protected function isRSS()
{ {
$rss = $this->request->param('Rss'); return isset($this->urlParams['RSS']) && strcasecmp($this->urlParams['RSS'], 'rss') == 0;
return (is_string($rss) && strcasecmp($rss, 'rss') == 0);
} }
} }

View File

@ -10,6 +10,7 @@ use SilverStripe\Forms\GridField\GridFieldAddNewButton;
use SilverStripe\Forms\Tab; use SilverStripe\Forms\Tab;
use SilverStripe\Forms\TextareaField; use SilverStripe\Forms\TextareaField;
use SilverStripe\ORM\DataExtension; use SilverStripe\ORM\DataExtension;
use SilverStripe\ORM\ManyManyList;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\View\Parsers\URLSegmentFilter; use SilverStripe\View\Parsers\URLSegmentFilter;
use SilverStripe\View\Requirements; use SilverStripe\View\Requirements;
@ -17,6 +18,10 @@ use SilverStripe\View\Requirements;
/** /**
* This class is responsible for add Blog specific behaviour to Members. * This class is responsible for add Blog specific behaviour to Members.
* *
* @property string $URLSegment
* @property string $BlogProfileSummary
* @method Image BlogProfileImage()
* @method ManyManyList|BlogPost[] BlogPosts()
*/ */
class BlogMemberExtension extends DataExtension class BlogMemberExtension extends DataExtension
{ {

View File

@ -8,6 +8,7 @@ use SilverStripe\Forms\Tab;
use SilverStripe\Forms\TabSet; use SilverStripe\Forms\TabSet;
use SilverStripe\Forms\TextField; use SilverStripe\Forms\TextField;
use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataList;
use SilverStripe\ORM\ManyManyList;
use SilverStripe\ORM\ValidationResult; use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
@ -20,7 +21,7 @@ use SilverStripe\View\Parsers\URLSegmentFilter;
trait BlogObject trait BlogObject
{ {
/** /**
* @return DataList * @return ManyManyList|BlogPost[]
*/ */
public function BlogPosts() public function BlogPosts()
{ {
@ -31,6 +32,22 @@ trait BlogObject
return $blogPosts; return $blogPosts;
} }
/**
* Get blog this tag was queried from
*
* @return Blog|null
*/
public function Blog()
{
$blogID = $this->getBlogID();
if ($blogID) {
/** @var Blog $blog */
$blog = Blog::get()->byID($blogID);
return $blog;
}
return null;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@ -50,6 +67,24 @@ trait BlogObject
return $fields; return $fields;
} }
/**
* Number of times this object has blog posts in the current blog
*
* @return int
*/
public function getBlogCount()
{
$blog = $this->Blog();
if (!$blog) {
return 0;
}
return $this
->BlogPosts()
->filter(['ParentID' => $blog->ID])
->Count();
}
/** /**
* {@inheritdoc} * {@inheritdoc}
* @return ValidationResult * @return ValidationResult
@ -62,11 +97,6 @@ trait BlogObject
return $validation; return $validation;
} }
$blog = $this->Blog();
if (!$blog || !$blog->exists()) {
return $validation;
}
if ($this->getDuplicatesByField('Title')->count() > 0) { if ($this->getDuplicatesByField('Title')->count() > 0) {
$validation->addError($this->getDuplicateError(), self::DUPLICATE_EXCEPTION); $validation->addError($this->getDuplicateError(), self::DUPLICATE_EXCEPTION);
} }
@ -75,14 +105,18 @@ trait BlogObject
} }
/** /**
* Returns a relative link to this category. * Returns a relative link to this category or tag
* *
* @return string * @return string
*/ */
public function getLink() public function getLink()
{ {
$blog = $this->Blog();
if (!$blog) {
return null;
}
return Controller::join_links( return Controller::join_links(
$this->Blog()->Link(), $blog->Link(),
$this->getListUrlSegment(), $this->getListUrlSegment(),
$this->URLSegment $this->URLSegment
); );
@ -103,7 +137,8 @@ trait BlogObject
return $extended; return $extended;
} }
return $this->Blog()->canView($member); $blog = $this->Blog();
return $blog && $blog->canView($member);
} }
/** /**
@ -137,7 +172,8 @@ trait BlogObject
return $extended; return $extended;
} }
return $this->Blog()->canDelete($member); $blog = $this->Blog();
return $blog && $blog->canDelete($member);
} }
/** /**
@ -155,17 +191,36 @@ trait BlogObject
return $extended; return $extended;
} }
return $this->Blog()->canEdit($member); $blog = $this->Blog();
return $blog && $blog->canEdit($member);
} }
/** /**
* {@inheritdoc} * @return mixed
*/ */
public function getBlogID()
{
return $this->getSourceQueryParam('BlogID');
}
/**
* Set a blog ID for this record
*
* @param int $id
* @return $this
*/
public function setBlogID($id)
{
$this->setSourceQueryParam('BlogID', $id);
return $this;
}
protected function onBeforeWrite() protected function onBeforeWrite()
{ {
parent::onBeforeWrite(); parent::onBeforeWrite();
if ($this->exists() || empty($this->URLSegment)) { if ($this->exists() || empty($this->URLSegment)) {
return $this->generateURLSegment(); $this->generateURLSegment();
} }
} }
@ -178,7 +233,7 @@ trait BlogObject
*/ */
public function generateURLSegment($increment = 0) public function generateURLSegment($increment = 0)
{ {
$increment = (int) $increment; $increment = (int)$increment;
$filter = URLSegmentFilter::create(); $filter = URLSegmentFilter::create();
// Setting this to on. Because of the UI flow, it would be quite a lot of work // Setting this to on. Because of the UI flow, it would be quite a lot of work
@ -209,12 +264,7 @@ trait BlogObject
protected function getDuplicatesByField($field) protected function getDuplicatesByField($field)
{ {
$duplicates = DataList::create(self::class) $duplicates = DataList::create(self::class)
->filter( ->filter([$field => $this->$field]);
[
$field => $this->$field,
'BlogID' => (int) $this->BlogID
]
);
if ($this->ID) { if ($this->ID) {
$duplicates = $duplicates->exclude('ID', $this->ID); $duplicates = $duplicates->exclude('ID', $this->ID);

View File

@ -313,14 +313,6 @@ class BlogPost extends Page
} }
// Get categories and tags // Get categories and tags
$parent = $this->Parent();
$categories = $parent instanceof Blog
? $parent->Categories()
: BlogCategory::get();
$tags = $parent instanceof Blog
? $parent->Tags()
: BlogTag::get();
// @todo: Reimplement the sidebar // @todo: Reimplement the sidebar
// $options = BlogAdminSidebar::create( // $options = BlogAdminSidebar::create(
$fields->addFieldsToTab( $fields->addFieldsToTab(
@ -330,7 +322,7 @@ class BlogPost extends Page
TagField::create( TagField::create(
'Categories', 'Categories',
_t(__CLASS__ . '.Categories', 'Categories'), _t(__CLASS__ . '.Categories', 'Categories'),
$categories, BlogCategory::get(),
$this->Categories() $this->Categories()
) )
->setCanCreate($this->canCreateCategories()) ->setCanCreate($this->canCreateCategories())
@ -338,7 +330,7 @@ class BlogPost extends Page
TagField::create( TagField::create(
'Tags', 'Tags',
_t(__CLASS__ . '.Tags', 'Tags'), _t(__CLASS__ . '.Tags', 'Tags'),
$tags, BlogTag::get(),
$this->Tags() $this->Tags()
) )
->setCanCreate($this->canCreateTags()) ->setCanCreate($this->canCreateTags())
@ -498,32 +490,6 @@ class BlogPost extends Page
} }
} }
/**
* {@inheritdoc}
*
* Sets blog relationship on all categories and tags assigned to this post.
*/
public function onAfterWrite()
{
parent::onAfterWrite();
foreach ($this->Categories() as $category) {
/**
* @var BlogCategory $category
*/
$category->BlogID = $this->ParentID;
$category->write();
}
foreach ($this->Tags() as $tag) {
/**
* @var BlogTag $tag
*/
$tag->BlogID = $this->ParentID;
$tag->write();
}
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@ -816,4 +782,16 @@ class BlogPost extends Page
$this->Authors()->add($member); $this->Authors()->add($member);
} }
} }
/**
* So that tags / categories queried through this post generate the correct Link()
*
* @return array
*/
public function getInheritableQueryParams()
{
$params = parent::getInheritableQueryParams();
$params['BlogID'] = $this->ParentID;
return $params;
}
} }

View File

@ -3,6 +3,7 @@
namespace SilverStripe\Blog\Model; namespace SilverStripe\Blog\Model;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ManyManyList;
/** /**
* A blog tag for keyword descriptions of a blog post. * A blog tag for keyword descriptions of a blog post.
@ -10,9 +11,9 @@ use SilverStripe\ORM\DataObject;
* *
* @method Blog Blog() * @method Blog Blog()
* *
* @method ManyManyList|BlogPost[] BlogPosts()
* @property string $Title * @property string $Title
* @property string $URLSegment * @property string $URLSegment
* @property int $BlogID
*/ */
class BlogTag extends DataObject implements CategorisationObject class BlogTag extends DataObject implements CategorisationObject
{ {
@ -44,8 +45,8 @@ class BlogTag extends DataObject implements CategorisationObject
/** /**
* @var array * @var array
*/ */
private static $has_one = [ private static $indexes = [
'Blog' => Blog::class 'URLSegment' => true,
]; ];
/** /**
@ -55,6 +56,11 @@ class BlogTag extends DataObject implements CategorisationObject
'BlogPosts' => BlogPost::class 'BlogPosts' => BlogPost::class
]; ];
/**
* @var string
*/
private static $default_sort = '"BlogTag"."Title" ASC';
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */

View File

@ -2,10 +2,17 @@
namespace SilverStripe\Blog\Model; namespace SilverStripe\Blog\Model;
use SilverStripe\ORM\ManyManyList;
/** /**
* @method ManyManyList BlogPosts * @method ManyManyList|BlogPost[] BlogPosts()
*/ */
interface CategorisationObject interface CategorisationObject
{ {
/**
* Number of times this object has blog posts in the current blog
*
* @return int
*/
public function getBlogCount();
} }

View File

@ -0,0 +1,140 @@
<?php
namespace SilverStripe\Blog\Tasks;
use SilverStripe\Blog\Model\BlogCategory;
use SilverStripe\Blog\Model\BlogTag;
use SilverStripe\Control\Director;
use SilverStripe\Core\Convert;
use SilverStripe\Dev\BuildTask;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\Queries\SQLSelect;
use SilverStripe\View\HTML;
class FixBlogDuplicatesTask extends BuildTask
{
private static $segment = 'FixBlogDuplicatesTask';
protected $title = 'Fix blog duplicate categories / tags';
protected $description = 'Merge categories and tags with the same title';
public function run($request)
{
$this->dedupe(BlogTag::class, 'BlogPost_Tags', 'BlogTagID');
$this->dedupe(BlogCategory::class, 'BlogPost_Categories', 'BlogCategoryID');
}
/**
* Log message to console / page
*
* @param string $message
*/
protected function message($message)
{
if (Director::is_cli()) {
echo "{$message}\n";
} else {
echo HTML::createTag('p', [], Convert::raw2xml($message));
}
}
/**
* Progress used for CLI output
*
* @var int
*/
protected $printedDots = 0;
/**
* Render progress dots (20 dots per column)
*
* @param $current
* @param int $total
* @param string $character
*/
protected function progress($current, $total = 0, $character = '.')
{
if (!Director::is_cli()) {
return;
}
while ($this->printedDots < $current) {
echo $character;
$this->printedDots++;
if ($this->printedDots % 80 === 0) {
if ($total) {
$len = strlen($total);
echo str_pad("{$this->printedDots}/{$total}", $len * 2 + 2, ' ', STR_PAD_LEFT);
}
echo "\n";
}
}
}
/**
* Deduplicate the given class
*
* @param string $class Class name to dedupe
* @param string $mappingTable Table name mapping
* @param string $relationField Name of foreign key relation field on mapping table
*/
protected function dedupe($class, $mappingTable, $relationField)
{
$this->printedDots = 0;
// Find all duplicates
$itemTable = DataObject::getSchema()->tableName($class);
$duplicates = SQLSelect::create()
->setSelect([
'Title' => '"Title"',
'Count' => 'COUNT(*)',
'UseID' => 'MIN("ID")',
])
->setFrom($itemTable)
->setGroupBy('"Title"')
->setHaving('"Count" > 1');
$count = $duplicates->count();
$this->message("Found {$count} items with duplicates for type {$class}");
if (!$count) {
return;
}
$done = 0;
foreach ($duplicates->execute() as $duplicate) {
$title = $duplicate['Title'];
$id = $duplicate['UseID'];
DB::prepared_query(
<<<SQL
UPDATE "{$mappingTable}"
INNER JOIN "{$itemTable}" ON "{$mappingTable}"."{$relationField}" = "{$itemTable}"."ID"
SET "{$mappingTable}"."{$relationField}" = ?
WHERE "{$itemTable}"."Title" = ?
SQL
,
[$id, $title]
);
// Delete duplicates
$duplicateItems = DataObject::get($class)->filter([
'Title' => $title,
'ID:not' => $id,
]);
/** @var DataObject $duplicateItem */
foreach ($duplicateItems as $duplicateItem) {
$duplicateItem->delete();
}
// Update progress bar
$done++;
$this->progress($done, $count);
}
$this->progress("");
$this->progress("Completed cleaning duplicates for {$class}");
}
}

View File

@ -3,11 +3,10 @@
namespace SilverStripe\Blog\Widgets; namespace SilverStripe\Blog\Widgets;
use SilverStripe\Blog\Model\Blog; use SilverStripe\Blog\Model\Blog;
use SilverStripe\Core\Convert; use SilverStripe\Blog\Model\BlogTag;
use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\DropdownField;
use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataObject; use SilverStripe\View\ArrayData;
use SilverStripe\ORM\DB;
use SilverStripe\Widgets\Model\Widget; use SilverStripe\Widgets\Model\Widget;
if (!class_exists(Widget::class)) { if (!class_exists(Widget::class)) {
@ -73,53 +72,42 @@ class BlogTagsCloudWidget extends Widget
} }
/** /**
* @return array * @return ArrayList
*/ */
public function getTags() public function getTags()
{ {
if ($blog = $this->Blog()) { // Check blog exists
$escapedID = Convert::raw2sql($blog->ID); $blog = $this->Blog();
$sql = 'SELECT DISTINCT "BlogTag"."URLSegment","BlogTag"."Title",Count("BlogTagID") AS "TagCount" if (!$blog) {
from "BlogPost_Tags" return ArrayList::create([]);
INNER JOIN "BlogPost"
ON "BlogPost"."ID" = "BlogPost_Tags"."BlogPostID"
INNER JOIN "BlogTag"
ON "BlogTag"."ID" = "BlogPost_Tags"."BlogTagID"
WHERE "BlogID" = ' . $escapedID
. ' GROUP By "BlogTag"."URLSegment","BlogTag"."Title"
ORDER BY "Title"';
$records = DB::query($sql);
$bloglink = $blog->Link();
$maxTagCount = 0;
// create DataObjects that can be used to render the tag cloud
$tags = ArrayList::create();
foreach ($records as $record) {
$tag = DataObject::create();
$tag->TagName = $record['Title'];
$link = $bloglink.'tag/'.$record['URLSegment'];
$tag->Link = $link;
if ($record['TagCount'] > $maxTagCount) {
$maxTagCount = $record['TagCount'];
}
$tag->TagCount = $record['TagCount'];
$tags->push($tag);
}
// normalize the tag counts from 1 to 10
if ($maxTagCount) {
$tagfactor = 10 / $maxTagCount;
foreach ($tags->getIterator() as $tag) {
$normalized = round($tagfactor * ($tag->TagCount));
$tag->NormalizedTag = $normalized;
}
}
return $tags;
} }
return []; // create ArrayData that can be used to render the tag cloud
$maxTagCount = 0;
$tags = ArrayList::create();
/** @var BlogTag $record */
foreach ($blog->Tags() as $record) {
// Remember max count found
$count = $record->getBlogCount();
$maxTagCount = $maxTagCount > $count ? $maxTagCount : $count;
// Save
$tags->push(ArrayData::create([
'TagName' => $record->Title,
'Link' => $record->getLink(),
'TagCount' => $count,
]));
}
// normalize the tag counts from 1 to 10
if ($maxTagCount) {
$tagfactor = 10 / $maxTagCount;
foreach ($tags->getIterator() as $tag) {
$normalized = round($tagfactor * ($tag->TagCount));
$tag->NormalizedTag = $normalized;
}
}
return $tags;
} }
} }

View File

@ -5,7 +5,6 @@ namespace SilverStripe\Blog\Tests;
use SilverStripe\Blog\Model\Blog; use SilverStripe\Blog\Model\Blog;
use SilverStripe\Blog\Model\BlogCategory; use SilverStripe\Blog\Model\BlogCategory;
use SilverStripe\Blog\Model\BlogPost; use SilverStripe\Blog\Model\BlogPost;
use SilverStripe\Blog\Model\BlogTag;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Dev\FunctionalTest; use SilverStripe\Dev\FunctionalTest;
use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBDatetime;
@ -70,15 +69,19 @@ class BlogCategoryTest extends FunctionalTest
*/ */
public function testAllowMultibyteUrlSegment() public function testAllowMultibyteUrlSegment()
{ {
/** @var Blog $blog */
$blog = $this->objFromFixture(Blog::class, 'FirstBlog'); $blog = $this->objFromFixture(Blog::class, 'FirstBlog');
$cat = new BlogCategory(); $cat = new BlogCategory();
$cat->BlogID = $blog->ID;
$cat->Title = 'تست'; $cat->Title = 'تست';
$cat->write(); $cat->write();
// urlencoded // urlencoded
$this->assertEquals('%D8%AA%D8%B3%D8%AA', $cat->URLSegment); $this->assertEquals('%D8%AA%D8%B3%D8%AA', $cat->URLSegment);
$link = Controller::join_links($cat->Blog()->Link(), 'category', '%D8%AA%D8%B3%D8%AA'); $expectedLink = Controller::join_links($blog->Link('category'), '%D8%AA%D8%B3%D8%AA');
$this->assertEquals($link, $cat->getLink()); $actualLink = $blog->Categories(false)->byID($cat->ID)->getLink();
$this->assertEquals($expectedLink, $actualLink);
} }
public function testCanView() public function testCanView()
@ -88,8 +91,9 @@ class BlogCategoryTest extends FunctionalTest
$this->objFromFixture(Member::class, 'Admin'); $this->objFromFixture(Member::class, 'Admin');
$editor = $this->objFromFixture(Member::class, 'Editor'); $editor = $this->objFromFixture(Member::class, 'Editor');
$category = $this->objFromFixture(BlogCategory::class, 'SecondCategory'); /** @var Blog $secondBlog */
$secondBlog = $this->objFromFixture(Blog::class, 'SecondBlog');
$category = $secondBlog->Categories(false)->find('URLSegment', 'second-category');
$this->assertFalse($category->canView($editor), 'Editor should not be able to view category.'); $this->assertFalse($category->canView($editor), 'Editor should not be able to view category.');
} }
@ -103,20 +107,26 @@ class BlogCategoryTest extends FunctionalTest
$admin = $this->objFromFixture(Member::class, 'Admin'); $admin = $this->objFromFixture(Member::class, 'Admin');
$editor = $this->objFromFixture(Member::class, 'Editor'); $editor = $this->objFromFixture(Member::class, 'Editor');
$category = $this->objFromFixture(BlogCategory::class, 'FirstCategory'); /** @var Blog $firstBlog */
$firstBlog = $this->objFromFixture(Blog::class, 'FirstBlog');
$firstCategory = $firstBlog->Categories(false)->find('URLSegment', 'first-category');
$this->assertTrue($category->canEdit($admin), 'Admin should be able to edit category.'); $this->assertTrue($firstCategory->canEdit($admin), 'Admin should be able to edit category.');
$this->assertTrue($category->canEdit($editor), 'Editor should be able to edit category.'); $this->assertTrue($firstCategory->canEdit($editor), 'Editor should be able to edit category.');
$category = $this->objFromFixture(BlogCategory::class, 'SecondCategory'); /** @var Blog $secondBlog */
$secondBlog = $this->objFromFixture(Blog::class, 'SecondBlog');
$secondCategory = $secondBlog->Categories(false)->find('URLSegment', 'second-category');
$this->assertTrue($category->canEdit($admin), 'Admin should be able to edit category.'); $this->assertTrue($secondCategory->canEdit($admin), 'Admin should be able to edit category.');
$this->assertFalse($category->canEdit($editor), 'Editor should not be able to edit category.'); $this->assertFalse($secondCategory->canEdit($editor), 'Editor should not be able to edit category.');
$category = $this->objFromFixture(BlogCategory::class, 'ThirdCategory'); /** @var Blog $secondBlog */
$thirdBlog = $this->objFromFixture(Blog::class, 'ThirdBlog');
$thirdCategory = $thirdBlog->Categories(false)->find('URLSegment', 'third-category');
$this->assertTrue($category->canEdit($admin), 'Admin should always be able to edit category.'); $this->assertTrue($thirdCategory->canEdit($admin), 'Admin should always be able to edit category.');
$this->assertTrue($category->canEdit($editor), 'Editor should be able to edit category.'); $this->assertTrue($thirdCategory->canEdit($editor), 'Editor should be able to edit category.');
} }
public function testCanCreate() public function testCanCreate()
@ -126,7 +136,7 @@ class BlogCategoryTest extends FunctionalTest
$admin = $this->objFromFixture(Member::class, 'Admin'); $admin = $this->objFromFixture(Member::class, 'Admin');
$editor = $this->objFromFixture(Member::class, 'Editor'); $editor = $this->objFromFixture(Member::class, 'Editor');
$category = singleton(BlogCategory::class); $category = BlogCategory::singleton();
$this->assertTrue($category->canCreate($admin), 'Admin should be able to create category.'); $this->assertTrue($category->canCreate($admin), 'Admin should be able to create category.');
$this->assertTrue($category->canCreate($editor), 'Editor should be able to create category.'); $this->assertTrue($category->canCreate($editor), 'Editor should be able to create category.');
@ -139,43 +149,45 @@ class BlogCategoryTest extends FunctionalTest
$admin = $this->objFromFixture(Member::class, 'Admin'); $admin = $this->objFromFixture(Member::class, 'Admin');
$editor = $this->objFromFixture(Member::class, 'Editor'); $editor = $this->objFromFixture(Member::class, 'Editor');
$category = $this->objFromFixture(BlogCategory::class, 'FirstCategory'); /** @var Blog $firstBlog */
$firstBlog = $this->objFromFixture(Blog::class, 'FirstBlog');
$firstCategory = $firstBlog->Categories(false)->find('URLSegment', 'first-category');
$this->assertTrue($category->canDelete($admin), 'Admin should be able to delete category.'); $this->assertTrue($firstCategory->canDelete($admin), 'Admin should be able to delete category.');
$this->assertTrue($category->canDelete($editor), 'Editor should be able to category category.'); $this->assertTrue($firstCategory->canDelete($editor), 'Editor should be able to category category.');
$category = $this->objFromFixture(BlogCategory::class, 'SecondCategory'); /** @var Blog $secondBlog */
$this->assertTrue($category->canDelete($admin), 'Admin should be able to delete category.'); $secondBlog = $this->objFromFixture(Blog::class, 'SecondBlog');
$this->assertFalse($category->canDelete($editor), 'Editor should not be able to delete category.'); $secondCategory = $secondBlog->Categories(false)->find('URLSegment', 'second-category');
$category = $this->objFromFixture(BlogCategory::class, 'ThirdCategory'); $this->assertTrue($secondCategory->canDelete($admin), 'Admin should be able to delete category.');
$this->assertTrue($category->canDelete($admin), 'Admin should always be able to delete category.'); $this->assertFalse($secondCategory->canDelete($editor), 'Editor should not be able to delete category.');
$this->assertTrue($category->canDelete($editor), 'Editor should be able to delete category.');
/** @var Blog $secondBlog */
$thirdBlog = $this->objFromFixture(Blog::class, 'ThirdBlog');
$thirdCategory = $thirdBlog->Categories(false)->find('URLSegment', 'third-category');
$this->assertTrue($thirdCategory->canDelete($admin), 'Admin should always be able to delete category.');
$this->assertTrue($thirdCategory->canDelete($editor), 'Editor should be able to delete category.');
} }
public function testDuplicateCategories() public function testDuplicateCategories()
{ {
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('A blog category already exists with that name.');
$blog = new Blog(); $blog = new Blog();
$blog->Title = 'Testing for duplicate categories'; $blog->Title = 'Testing for duplicate categories';
$blog->write(); $blog->write();
$category = new BlogCategory(); $category = new BlogCategory();
$category->Title = 'Test'; $category->Title = 'Test';
$category->BlogID = $blog->ID;
$category->URLSegment = 'test'; $category->URLSegment = 'test';
$category->write(); $category->write();
$category = new BlogCategory(); $category = new BlogCategory();
$category->Title = 'Test'; $category->Title = 'Test';
$category->URLSegment = 'test'; $category->URLSegment = 'test';
$category->BlogID = $blog->ID; $category->write();
try {
$category->write();
$this->fail('Duplicate BlogCategory written');
} catch (ValidationException $e) {
$messages = $e->getResult()->getMessages();
$this->assertCount(1, $messages);
$this->assertEquals(BlogTag::DUPLICATE_EXCEPTION, $messages[0]['messageType']);
}
} }
} }

View File

@ -61,14 +61,15 @@ class BlogTagTest extends FunctionalTest
*/ */
public function testAllowMultibyteUrlSegment() public function testAllowMultibyteUrlSegment()
{ {
/** @var Blog $blog */
$blog = $this->objFromFixture(Blog::class, 'FirstBlog'); $blog = $this->objFromFixture(Blog::class, 'FirstBlog');
$tag = new BlogTag(); $tag = new BlogTag();
$tag->BlogID = $blog->ID; $tag->setBlogID($blog->ID);
$tag->Title = 'تست'; $tag->Title = 'تست';
$tag->write(); $tag->write();
// urlencoded // urlencoded
$this->assertEquals('%D8%AA%D8%B3%D8%AA', $tag->URLSegment); $this->assertEquals('%D8%AA%D8%B3%D8%AA', $tag->URLSegment);
$link = Controller::join_links($tag->Blog()->Link(), 'tag', '%D8%AA%D8%B3%D8%AA'); $link = Controller::join_links($blog->Link(), 'tag', '%D8%AA%D8%B3%D8%AA');
$this->assertEquals($link, $tag->getLink()); $this->assertEquals($link, $tag->getLink());
} }
@ -82,15 +83,19 @@ class BlogTagTest extends FunctionalTest
$admin = $this->objFromFixture(Member::class, 'Admin'); $admin = $this->objFromFixture(Member::class, 'Admin');
$editor = $this->objFromFixture(Member::class, 'Editor'); $editor = $this->objFromFixture(Member::class, 'Editor');
$tag = $this->objFromFixture(BlogTag::class, 'FirstTag'); /** @var Blog $firstBlog */
$firstBlog = $this->objFromFixture(Blog::class, 'FirstBlog');
$firstTag = $firstBlog->Tags(false)->find('URLSegment', 'first-tag');
$this->assertTrue($tag->canView($admin), 'Admin should be able to view tag.'); $this->assertTrue($firstTag->canView($admin), 'Admin should be able to view tag.');
$this->assertTrue($tag->canView($editor), 'Editor should be able to view tag.'); $this->assertTrue($firstTag->canView($editor), 'Editor should be able to view tag.');
$tag = $this->objFromFixture(BlogTag::class, 'SecondTag'); /** @var Blog $secondBlog */
$secondBlog = $this->objFromFixture(Blog::class, 'SecondBlog');
$secondTag = $secondBlog->Tags(false)->find('URLSegment', 'second-tag');
$this->assertTrue($tag->canView($admin), 'Admin should be able to view tag.'); $this->assertTrue($secondTag->canView($admin), 'Admin should be able to view tag.');
$this->assertFalse($tag->canView($editor), 'Editor should not be able to view tag.'); $this->assertFalse($secondTag->canView($editor), 'Editor should not be able to view tag.');
} }
public function testCanEdit() public function testCanEdit()
@ -100,20 +105,26 @@ class BlogTagTest extends FunctionalTest
$admin = $this->objFromFixture(Member::class, 'Admin'); $admin = $this->objFromFixture(Member::class, 'Admin');
$editor = $this->objFromFixture(Member::class, 'Editor'); $editor = $this->objFromFixture(Member::class, 'Editor');
$tag = $this->objFromFixture(BlogTag::class, 'FirstTag'); /** @var Blog $firstBlog */
$firstBlog = $this->objFromFixture(Blog::class, 'FirstBlog');
$firstTag = $firstBlog->Tags(false)->find('URLSegment', 'first-tag');
$this->assertTrue($tag->canEdit($admin), 'Admin should be able to edit tag.'); $this->assertTrue($firstTag->canEdit($admin), 'Admin should be able to edit tag.');
$this->assertTrue($tag->canEdit($editor), 'Editor should be able to edit tag.'); $this->assertTrue($firstTag->canEdit($editor), 'Editor should be able to edit tag.');
$tag = $this->objFromFixture(BlogTag::class, 'SecondTag'); /** @var Blog $secondBlog */
$secondBlog = $this->objFromFixture(Blog::class, 'SecondBlog');
$secondTag = $secondBlog->Tags(false)->find('URLSegment', 'second-tag');
$this->assertTrue($tag->canEdit($admin), 'Admin should be able to edit tag.'); $this->assertTrue($secondTag->canEdit($admin), 'Admin should be able to edit tag.');
$this->assertFalse($tag->canEdit($editor), 'Editor should not be able to edit tag.'); $this->assertFalse($secondTag->canEdit($editor), 'Editor should not be able to edit tag.');
$tag = $this->objFromFixture(BlogTag::class, 'ThirdTag'); /** @var Blog $thirdBlog */
$thirdBlog = $this->objFromFixture(Blog::class, 'ThirdBlog');
$thirdTag = $thirdBlog->Tags(false)->find('URLSegment', 'third-tag');
$this->assertTrue($tag->canEdit($admin), 'Admin should always be able to edit tags.'); $this->assertTrue($thirdTag->canEdit($admin), 'Admin should always be able to edit tags.');
$this->assertTrue($tag->canEdit($editor), 'Editor should be able to edit tag.'); $this->assertTrue($thirdTag->canEdit($editor), 'Editor should be able to edit tag.');
} }
public function testCanCreate() public function testCanCreate()
@ -136,20 +147,26 @@ class BlogTagTest extends FunctionalTest
$admin = $this->objFromFixture(Member::class, 'Admin'); $admin = $this->objFromFixture(Member::class, 'Admin');
$editor = $this->objFromFixture(Member::class, 'Editor'); $editor = $this->objFromFixture(Member::class, 'Editor');
$tag = $this->objFromFixture(BlogTag::class, 'FirstTag'); /** @var Blog $firstBlog */
$firstBlog = $this->objFromFixture(Blog::class, 'FirstBlog');
$firstTag = $firstBlog->Tags(false)->find('URLSegment', 'first-tag');
$this->assertTrue($tag->canDelete($admin), 'Admin should be able to delete tag.'); $this->assertTrue($firstTag->canDelete($admin), 'Admin should be able to delete tag.');
$this->assertTrue($tag->canDelete($editor), 'Editor should be able to delete tag.'); $this->assertTrue($firstTag->canDelete($editor), 'Editor should be able to delete tag.');
$tag = $this->objFromFixture(BlogTag::class, 'SecondTag'); /** @var Blog $secondBlog */
$secondBlog = $this->objFromFixture(Blog::class, 'SecondBlog');
$secondTag = $secondBlog->Tags(false)->find('URLSegment', 'second-tag');
$this->assertTrue($tag->canDelete($admin), 'Admin should be able to delete tag.'); $this->assertTrue($secondTag->canDelete($admin), 'Admin should be able to delete tag.');
$this->assertFalse($tag->canDelete($editor), 'Editor should not be able to delete tag.'); $this->assertFalse($secondTag->canDelete($editor), 'Editor should not be able to delete tag.');
$tag = $this->objFromFixture(BlogTag::class, 'ThirdTag'); /** @var Blog $thirdBlog */
$thirdBlog = $this->objFromFixture(Blog::class, 'ThirdBlog');
$thirdTag = $thirdBlog->Tags(false)->find('URLSegment', 'third-tag');
$this->assertTrue($tag->canDelete($admin), 'Admin should always be able to delete tags.'); $this->assertTrue($thirdTag->canDelete($admin), 'Admin should always be able to delete tags.');
$this->assertTrue($tag->canDelete($editor), 'Editor should be able to delete tag.'); $this->assertTrue($thirdTag->canDelete($editor), 'Editor should be able to delete tag.');
} }
public function testDuplicateTagsForURLSegment() public function testDuplicateTagsForURLSegment()

View File

@ -12,10 +12,6 @@ SilverStripe\Blog\Model\Blog:
blog_a: blog_a:
URLSegment: my-blog URLSegment: my-blog
Title: My Blog Title: My Blog
Categories:
- =>SilverStripe\Blog\Model\BlogCategory.category_a
Tags:
- =>SilverStripe\Blog\Model\BlogTag.tag_a
SilverStripe\Blog\Model\BlogPost: SilverStripe\Blog\Model\BlogPost:
blogpost_a: blogpost_a:
@ -23,7 +19,3 @@ SilverStripe\Blog\Model\BlogPost:
URLSegment: آبیدآبید URLSegment: آبیدآبید
PublishDate: 2017-08-01 00:00:00 PublishDate: 2017-08-01 00:00:00
Parent: =>SilverStripe\Blog\Model\Blog.blog_a Parent: =>SilverStripe\Blog\Model\Blog.blog_a
Categories:
- =>SilverStripe\Blog\Model\BlogCategory.category_a
Tags:
- =>SilverStripe\Blog\Model\BlogTag.tag_a

View File

@ -93,47 +93,37 @@ SilverStripe\Blog\Model\BlogCategory:
FirstCategory: FirstCategory:
Title: 'First Category' Title: 'First Category'
URLSegment: 'first-category' URLSegment: 'first-category'
BlogID: =>SilverStripe\Blog\Model\Blog.FirstBlog
SecondCategory: SecondCategory:
Title: 'Second Category' Title: 'Second Category'
URLSegment: 'second-category' URLSegment: 'second-category'
BlogID: =>SilverStripe\Blog\Model\Blog.SecondBlog
ThirdCategory: ThirdCategory:
Title: 'Third Category' Title: 'Third Category'
URLSegment: 'third-category' URLSegment: 'third-category'
BlogID: =>SilverStripe\Blog\Model\Blog.ThirdBlog
SilverStripe\Blog\Model\BlogTag: SilverStripe\Blog\Model\BlogTag:
FirstTag: FirstTag:
Title: 'First Tag' Title: 'First Tag'
URLSegment: 'first-tag' URLSegment: 'first-tag'
BlogID: =>SilverStripe\Blog\Model\Blog.FirstBlog
SecondTag: SecondTag:
Title: 'Second Tag' Title: 'Second Tag'
URLSegment: 'second-tag' URLSegment: 'second-tag'
BlogID: =>SilverStripe\Blog\Model\Blog.SecondBlog
ThirdTag: ThirdTag:
Title: 'Third Tag' Title: 'Third Tag'
URLSegment: 'third-tag' URLSegment: 'third-tag'
BlogID: =>SilverStripe\Blog\Model\Blog.ThirdBlog
#Tags for Tag Cloud widget #Tags for Tag Cloud widget
PopularTag: PopularTag:
Title: 'Popular' Title: 'Popular'
URLSegment: 'popular' URLSegment: 'popular'
BlogID: =>SilverStripe\Blog\Model\Blog.FourthBlog
CoolTag: CoolTag:
Title: 'Cool' Title: 'Cool'
URLSegment: 'cool' URLSegment: 'cool'
BlogID: =>SilverStripe\Blog\Model\Blog.FourthBlog
CatTag: CatTag:
Title: 'Cat' Title: 'Cat'
URLSegment: 'cat' URLSegment: 'cat'
BlogID: =>SilverStripe\Blog\Model\Blog.FourthBlog
KiwiTag: KiwiTag:
Title: 'Kiwi' Title: 'Kiwi'
URLSegment: 'kiwi' URLSegment: 'kiwi'
BlogID: =>SilverStripe\Blog\Model\Blog.FourthBlog
SilverStripe\Blog\Model\BlogPost: SilverStripe\Blog\Model\BlogPost:
FirstBlogPost: FirstBlogPost: