diff --git a/code/BlogEntry.php b/code/BlogEntry.php index be1a71c..331fdbc 100644 --- a/code/BlogEntry.php +++ b/code/BlogEntry.php @@ -87,25 +87,40 @@ class BlogEntry extends Page { } /** - * Returns the tags added to this blog entry + * Safely split and parse all distinct tags assigned to this BlogEntry + * + * @return array Associative array of lowercase tag to native case tags */ - function TagsCollection() { + public function TagNames() { + $tags = preg_split("/\s*,\s*/", trim($this->Tags)); + $results = array(); + foreach($tags as $tag) { + if($tag) $results[mb_strtolower($tag)] = $tag; + } + return $results; + } + + /** + * Returns the tags added to this blog entry + * + * @return ArrayList List of ArrayData with Tag, Link, and URLTag keys + */ + public function TagsCollection() { - $tags = preg_split(" *, *", trim($this->Tags)); + $tags = $this->TagNames(); $output = new ArrayList(); - $link = $this->getParent() ? $this->getParent()->Link('tag') : ''; - foreach($tags as $tag) { + $link = ($parent = $this->getParent()) ? $parent->Link('tag') : ''; + foreach($tags as $tag => $tagLabel) { + $urlKey = urlencode($tag); $output->push(new ArrayData(array( - 'Tag' => Convert::raw2xml($tag), - 'Link' => $link . '/' . urlencode($tag), - 'URLTag' => urlencode($tag) + 'Tag' => $tagLabel, + 'Link' => Controller::join_links($link, $urlKey), + 'URLTag' => $urlKey ))); } - if($this->Tags) { - return $output; - } + return $output; } function Content() { diff --git a/code/BlogTree.php b/code/BlogTree.php index 9d02bbb..1875708 100644 --- a/code/BlogTree.php +++ b/code/BlogTree.php @@ -46,6 +46,7 @@ class BlogTree extends Page { * * @param $page allows you to force a specific page, otherwise, * uses current + * @return BlogTree */ static function current($page = null) { @@ -147,12 +148,14 @@ class BlogTree extends Page { /** * Get entries in this blog. - * @param string limit A clause to insert into the limit clause. - * @param string tag Only get blog entries with this tag - * @param string date Only get blog entries on this date - either a year, or a year-month eg '2008' or '2008-02' - * @param callback retrieveCallback A function to call with pagetype, filter and limit for custom blog sorting or filtering - * @param string $where - * @return DataObjectSet + * + * @param string $limit A clause to insert into the limit clause. + * @param string $tag Only get blog entries with this tag + * @param string $date Only get blog entries on this date - either a year, or a year-month eg '2008' or '2008-02' + * @param callable $retrieveCallback A function to call with pagetype, filter and limit for custom blog + * sorting or filtering + * @param string $filter Filter condition + * @return PaginatedList The list of entries in a paginated list */ public function Entries($limit = '', $tag = '', $date = '', $retrieveCallback = null, $filter = '') { @@ -229,6 +232,11 @@ class BlogTree_Controller extends Page_Controller { 'date' ); + private static $casting = array( + 'SelectedTag' => 'Text', + 'SelectedAuthor' => 'Text' + ); + function init() { parent::init(); @@ -237,7 +245,13 @@ class BlogTree_Controller extends Page_Controller { Requirements::themedCSS("blog","blog"); } - function BlogEntries($limit = null) { + /** + * Determine selected BlogEntry items to show on this page + * + * @param int $limit + * @return PaginatedList + */ + public function BlogEntries($limit = null) { require_once('Zend/Date.php'); if($limit === null) $limit = BlogTree::$default_entries_limit; @@ -275,14 +289,14 @@ class BlogTree_Controller extends Page_Controller { /** * This will create a tag point to the RSS feed */ - function IncludeBlogRSS() { + public function IncludeBlogRSS() { RSSFeed::linkToFeed($this->Link('rss'), _t('BlogHolder.RSSFEED',"RSS feed of these blogs")); } /** * Get the rss feed for this blog holder's entries */ - function rss() { + public function rss() { global $project_name; $blogName = $this->Title; @@ -299,7 +313,7 @@ class BlogTree_Controller extends Page_Controller { /** * Protection against infinite loops when an RSS widget pointing to this page is added to this page */ - function defaultAction($action) { + public function defaultAction($action) { if(stristr($_SERVER['HTTP_USER_AGENT'], 'SimplePie')) return $this->rss(); return parent::defaultAction($action); @@ -308,23 +322,22 @@ class BlogTree_Controller extends Page_Controller { /** * Return the currently viewing tag used in the template as $Tag * - * @return String + * @return string */ - function SelectedTag() { + public function SelectedTag() { if ($this->request->latestParam('Action') == 'tag') { $tag = $this->request->latestParam('ID'); - $tag = urldecode($tag); - return Convert::raw2xml($tag); - } + return urldecode($tag); + } return ''; } /** * Return the selected date from the blog tree * - * @return Date + * @return string */ - function SelectedDate() { + public function SelectedDate() { if($this->request->latestParam('Action') == 'date') { $year = $this->request->latestParam('ID'); $month = $this->request->latestParam('OtherID'); @@ -344,21 +357,23 @@ class BlogTree_Controller extends Page_Controller { } /** - * @return String + * @return string */ - function SelectedAuthor() { + public function SelectedAuthor() { if($this->request->getVar('author')) { $hasAuthor = BlogEntry::get()->filter('Author', $this->request->getVar('author'))->Count(); - return $hasAuthor ? Convert::raw2xml($this->request->getVar('author')) : null; + return $hasAuthor + ? $this->request->getVar('author') + : null; } elseif($this->request->getVar('authorID')) { $hasAuthor = BlogEntry::get()->filter('AuthorID', $this->request->getVar('authorID'))->Count(); if($hasAuthor) { $member = Member::get()->byId($this->request->getVar('authorID')); if($member) { if($member->hasMethod('BlogAuthorTitle')) { - return Convert::raw2xml($member->BlogAuthorTitle); + return $member->BlogAuthorTitle; } else { - return Convert::raw2xml($member->Title); + return $member->Title; } } else { return null; @@ -367,7 +382,11 @@ class BlogTree_Controller extends Page_Controller { } } - function SelectedNiceDate(){ + /** + * + * @return string + */ + public function SelectedNiceDate(){ $date = $this->SelectedDate(); if(strpos($date, '-')) { diff --git a/code/widgets/TagCloudWidget.php b/code/widgets/TagCloudWidget.php index 33f5b8e..08eb437 100644 --- a/code/widgets/TagCloudWidget.php +++ b/code/widgets/TagCloudWidget.php @@ -22,127 +22,134 @@ if(class_exists('Widget')) { ); private static $cmsTitle = "Tag Cloud"; + private static $description = "Shows a tag cloud of tags on your blog."; - private static $popularities = array( 'not-popular', 'not-very-popular', 'somewhat-popular', 'popular', 'very-popular', 'ultra-popular' ); + /** + * List of popularity classes in order of least to most popular + * + * @config + * @var array + */ + private static $popularities = array( + 'not-popular', + 'not-very-popular', + 'somewhat-popular', + 'popular', + 'very-popular', + 'ultra-popular' + ); - function getCMSFields() { - $fields = parent::getCMSFields(); + public function getCMSFields() { + + $this->beforeUpdateCMSFields(function($fields) { + $fields->merge( + new FieldList( + new TextField("Title", _t("TagCloudWidget.TILE", "Title")), + new TextField("Limit", _t("TagCloudWidget.LIMIT", "Limit number of tags")), + new OptionsetField( + "Sortby", + _t("TagCloudWidget.SORTBY", "Sort by"), + array( + "alphabet" => _t("TagCloudWidget.SBAL", "alphabet"), + "frequency" => _t("TagCloudWidget.SBFREQ", "frequency") + ) + ) + ) + ); + }); - $fields->merge( - - new FieldList( - new TextField("Title", _t("TagCloudWidget.TILE", "Title")), - new TextField("Limit", _t("TagCloudWidget.LIMIT", "Limit number of tags")), - new OptionsetField("Sortby",_t("TagCloudWidget.SORTBY","Sort by"),array("alphabet"=>_t("TagCloudWidget.SBAL", "alphabet"),"frequency"=>_t("TagCloudWidget.SBFREQ", "frequency"))) - ) - ); - - $this->extend('updateCMSFields', $fields); - - return $fields; + return parent::getCMSFields(); } function Title() { return $this->Title ? $this->Title : _t('TagCloudWidget.DEFAULTTITLE', 'Tag Cloud'); } + + /** + * Current BlogTree used as the container for this tagcloud. + * Used by {@link TagCloudWidgetTest} for testing + * + * @var BlogTree + */ + public static $container = null; + /** + * Return all sorted tags in the system + * + * @return ArrayList + */ function getTagsCollection() { Requirements::themedCSS("tagcloud"); - $allTags = array(); - $max = 0; - $container = BlogTree::current(); + // Ensure there is a valid BlogTree with entries + $container = BlogTree::current(self::$container); + if( !$container + || !($entries = $container->Entries()) + || $entries->count() == 0 + ) return null; - $entries = $container->Entries(); - - if($entries) { - foreach($entries as $entry) { - $theseTags = preg_split(" *, *", mb_strtolower(trim($entry->Tags))); - foreach($theseTags as $tag) { - if($tag != "") { - $allTags[$tag] = isset($allTags[$tag]) ? $allTags[$tag] + 1 : 1; //getting the count into key => value map - $max = ($allTags[$tag] > $max) ? $allTags[$tag] : $max; - } - } + // Extract all tags from each entry + $tagCounts = array(); // Mapping of tag => frequency + $tagLabels = array(); // Mapping of tag => label + foreach($entries as $entry) { + $theseTags = $entry->TagNames(); + foreach($theseTags as $tag => $tagLabel) { + $tagLabels[$tag] = $tagLabel; + //getting the count into key => value map + $tagCounts[$tag] = isset($tagCounts[$tag]) ? $tagCounts[$tag] + 1 : 1; } + } + if(empty($tagCounts)) return null; + $minCount = min($tagCounts); + $maxCount = max($tagCounts); - if($allTags) { - //TODO: move some or all of the sorts to the database for more efficiency - if($this->Limit > 0) $allTags = array_slice($allTags, 0, $this->Limit, true); + // Apply sorting mechanism + if($this->Sortby == "alphabet") { + // Sort by name + ksort($tagCounts); + } else { + // Sort by frequency + uasort($tagCounts, function($a, $b) { + return $b - $a; + }); + } + + // Apply limiting + if($this->Limit > 0) $tagCounts = array_slice($tagCounts, 0, $this->Limit, true); - if($this->Sortby == "alphabet"){ - $this->natksort($allTags); - } else{ - uasort($allTags, array($this, "column_sort_by_popularity")); // sort by frequency - } + // Calculate buckets of popularities + $numsizes = count(array_unique($tagCounts)); //Work out the number of different sizes + $popularities = self::config()->popularities; + $buckets = count($popularities); - $sizes = array(); - foreach ($allTags as $tag => $count) $sizes[$count] = true; + // If there are more frequencies than buckets, divide frequencies into buckets + if ($numsizes > $buckets) $numsizes = $buckets; + + // Adjust offset to use central buckets (if using a subset of available buckets) + $offset = round(($buckets - $numsizes)/2); - $offset = 0; - $numsizes = count($sizes)-1; //Work out the number of different sizes - $buckets = count(self::$popularities)-1; - - // If there are more frequencies than buckets, divide frequencies into buckets - if ($numsizes > $buckets) { - $numsizes = $buckets; - } - // Otherwise center use central buckets - else { - $offset = round(($buckets-$numsizes)/2); - } - - foreach($allTags as $tag => $count) { - $popularity = round($count / $max * $numsizes) + $offset; $popularity=min($buckets,$popularity); - $class = self::$popularities[$popularity]; - - $allTags[$tag] = array( - "Tag" => $tag, - "Count" => $count, - "Class" => $class, - "Link" => $container->Link('tag') . '/' . urlencode($tag) - ); - } + $output = new ArrayList(); + foreach($tagCounts as $tag => $count) { + + // Find position of $count in the selected range, adjusted for bucket range used + if($maxCount == $minCount) { + $popularities = $offset; + } else { + $popularity = round( + ($count-$minCount) / ($maxCount-$minCount) * ($numsizes-1) + ) + $offset; } + $class = $popularities[$popularity]; - $output = new ArrayList(); - foreach($allTags as $tag => $fields) { - $output->push(new ArrayData($fields)); - } - - return $output; + $output->push(new ArrayData(array( + "Tag" => $tagLabels[$tag], + "Count" => $count, + "Class" => $class, + "Link" => Controller::join_links($container->Link('tag'), urlencode($tag)) + ))); } - - return; - } - - /** - * Helper method to compare 2 Vars to work out the results. - * @param mixed - * @param mixed - * @return int - */ - private function column_sort_by_popularity($a, $b){ - if($a == $b) { - $result = 0; - } - else { - $result = $b - $a; - } - return $result; - } - - private function natksort(&$aToBeSorted) { - $aResult = array(); - $aKeys = array_keys($aToBeSorted); - natcasesort($aKeys); - foreach ($aKeys as $sKey) { - $aResult[$sKey] = $aToBeSorted[$sKey]; - } - $aToBeSorted = $aResult; - - return true; + return $output; } } } diff --git a/templates/TagCloudWidget.ss b/templates/TagCloudWidget.ss index 3544269..4e812c8 100644 --- a/templates/TagCloudWidget.ss +++ b/templates/TagCloudWidget.ss @@ -1,5 +1,5 @@
\ No newline at end of file diff --git a/tests/BlogEntryTest.php b/tests/BlogEntryTest.php index b1a3411..99afadf 100644 --- a/tests/BlogEntryTest.php +++ b/tests/BlogEntryTest.php @@ -1,4 +1,5 @@ assertEquals('', $entry->Content()->value); BlogEntry::$allow_wysiwyg_editing = $tmpFlag; } - - function testContent() { + + /** + * Tests BlogEntry::Content method + */ + public function testContent() { $tmpFlag = BlogEntry::$allow_wysiwyg_editing; BlogEntry::$allow_wysiwyg_editing = true; @@ -28,4 +35,24 @@ class BlogEntryTest extends SapphireTest { BlogEntry::$allow_wysiwyg_editing = $tmpFlag; } -} \ No newline at end of file + /** + * Tests TagCollection parsing of tags + */ + public function testTagging() { + $entry = new BlogEntry(); + $entry->Tags = 'damian,Bob, andrew , multiple words, thing,tag,item , Andrew'; + $tags = $entry->TagNames(); + ksort($tags); + + $this->assertEquals(array( + 'andrew' => 'Andrew', + 'bob' => 'Bob', + 'damian' => 'damian', + 'item' => 'item', + 'multiple words' => 'multiple words', + 'tag' => 'tag', + 'thing' => 'thing' + ), $tags); + } + +} diff --git a/tests/BlogHolderTest.php b/tests/BlogHolderTest.php index 00dcb1f..8b36301 100644 --- a/tests/BlogHolderTest.php +++ b/tests/BlogHolderTest.php @@ -73,5 +73,3 @@ class BlogHolderTest extends SapphireTest { } } - -?> diff --git a/tests/BlogTreeTest.php b/tests/BlogTreeTest.php index 7ba88a5..6c14845 100644 --- a/tests/BlogTreeTest.php +++ b/tests/BlogTreeTest.php @@ -104,5 +104,3 @@ class BlogTreeTest extends SapphireTest { } } - -?> diff --git a/tests/TagCloudWidgetTest.php b/tests/TagCloudWidgetTest.php new file mode 100644 index 0000000..ba58bd6 --- /dev/null +++ b/tests/TagCloudWidgetTest.php @@ -0,0 +1,95 @@ +Title = 'Holder'; + $holder->write(); + TagCloudWidget::$container = $holder; + + // Save all pages + $page = new BlogEntry(); + $page->Tags = 'Ultra, Very, Popular, Somewhat, NotVery, NotPopular'; + $page->ParentID = $holder->ID; + $page->write(); + $page = new BlogEntry(); + $page->Tags = 'Ultra, Very, Popular, Somewhat, NotVery'; + $page->ParentID = $holder->ID; + $page->write(); + $page = new BlogEntry(); + $page->Tags = 'Ultra, Very, Popular, Somewhat'; + $page->ParentID = $holder->ID; + $page->write(); + $page = new BlogEntry(); + $page->Tags = 'Ultra, Very, Popular'; + $page->ParentID = $holder->ID; + $page->write(); + $page = new BlogEntry(); + $page->Tags = 'Ultra, Very, Popular'; + $page->ParentID = $holder->ID; + $page->write(); + $page = new BlogEntry(); + $page->Tags = 'Ultra, Very'; + $page->ParentID = $holder->ID; + $page->write(); + $page = new BlogEntry(); + $page->Tags = 'Ultra'; + $page->ParentID = $holder->ID; + $page->write(); + $page = new BlogEntry(); + $page->Tags = ''; + $page->ParentID = $holder->ID; + $page->write(); + } + + public function tearDown() { + parent::tearDown(); + + if(!class_exists('Widget')) return; + + TagCloudWidget::$container = null; + } + + /** + * Test that tags are correctly extracted from a blog tree + */ + public function testGetTags() { + + if(!class_exists('Widget')) $this->markTestSkipped('This test requires the Widget module'); + + // Test sorting by alphabetic + $widget = new TagCloudWidget(); + $widget->Sortby = 'alphabet'; + $tags = $widget->getTagsCollection()->toNestedArray(); + $this->assertEquals($tags[0]['Tag'], 'NotPopular'); + $this->assertEquals($tags[0]['Class'], 'not-popular'); + $this->assertEquals($tags[0]['Count'], 1); + $this->assertEquals($tags[3]['Tag'], 'Somewhat'); + $this->assertEquals($tags[3]['Class'], 'somewhat-popular'); + $this->assertEquals($tags[3]['Count'], 3); + $this->assertEquals($tags[5]['Tag'], 'Very'); + $this->assertEquals($tags[5]['Class'], 'very-popular'); + $this->assertEquals($tags[5]['Count'], 6); + + // Test sorting by frequency + $widget = new TagCloudWidget(); + $widget->Sortby = 'frequency'; + $tags = $widget->getTagsCollection()->toNestedArray(); + $this->assertEquals($tags[0]['Tag'], 'Ultra'); + $this->assertEquals($tags[0]['Class'], 'ultra-popular'); + $this->assertEquals($tags[0]['Count'], 7); + $this->assertEquals($tags[3]['Tag'], 'Somewhat'); + $this->assertEquals($tags[3]['Class'], 'somewhat-popular'); + $this->assertEquals($tags[3]['Count'], 3); + $this->assertEquals($tags[5]['Tag'], 'NotPopular'); + $this->assertEquals($tags[5]['Class'], 'not-popular'); + $this->assertEquals($tags[5]['Count'], 1); + } + +}