From 16c7b2d431932fd12c193151e23f926b5c61948f Mon Sep 17 00:00:00 2001 From: Hamish Friedlander Date: Thu, 30 Apr 2009 22:05:54 +0000 Subject: [PATCH] FEATURE: Add an additional level of blog container, called the tree. You can now use blogs in the previous manner (holder has many entries) or as a deep tree of many holders. Viewing a level up the tree will show all blog entries within that tree. @todo: More Tests Better documentation --- code/ArchiveWidget.php | 34 ++--- code/BlogHolder.php | 167 ++------------------- code/BlogManagementWidget.php | 23 +-- code/BlogTree.php | 268 ++++++++++++++++++++++++++++++++++ code/SubscribeRSSWidget.php | 24 +-- code/TagCloudWidget.php | 19 +-- tests/BlogTreeTest.php | 30 ++++ tests/BlogTreeTest.yml | 53 +++++++ 8 files changed, 392 insertions(+), 226 deletions(-) create mode 100644 code/BlogTree.php create mode 100644 tests/BlogTreeTest.php create mode 100644 tests/BlogTreeTest.yml diff --git a/code/ArchiveWidget.php b/code/ArchiveWidget.php index 7e5db71..7cec3ae 100644 --- a/code/ArchiveWidget.php +++ b/code/ArchiveWidget.php @@ -28,18 +28,6 @@ class ArchiveWidget extends Widget { static $description = 'Show a list of months or years in which there are blog posts, and provide links to them.'; - function getBlogHolder() { - $page = Director::currentPage(); - - if($page instanceof BlogHolder) { - return $page; - } elseif(($page instanceof BlogEntry) && ($page->getParent() instanceof BlogHolder)) { - return $page->getParent(); - } else { - return DataObject::get_one('BlogHolder'); - } - } - function getCMSFields() { return new FieldSet( new OptionsetField( @@ -57,17 +45,27 @@ class ArchiveWidget extends Widget { Requirements::themedCSS('archivewidget'); $results = new DataObjectSet(); - $blogHolder = $this->getBlogHolder(); - $id = $blogHolder->ID; + $container = BlogTree::current(); + $ids = $container->BlogHolderIDs(); $stage = Versioned::current_stage(); $suffix = (!$stage || $stage == 'Stage') ? "" : "_$stage"; if($this->DisplayMode == 'month') { - $sqlResults = DB::query("SELECT DISTINCT MONTH(`Date`) AS `Month`, YEAR(`Date`) AS `Year` FROM `SiteTree$suffix` NATURAL JOIN `BlogEntry$suffix` WHERE `ParentID` = $id ORDER BY `Date` DESC"); + $sqlResults = DB::query(" + SELECT DISTINCT MONTH(`Date`) AS `Month`, YEAR(`Date`) AS `Year` + FROM `SiteTree$suffix` NATURAL JOIN `BlogEntry$suffix` + WHERE `ParentID` in (".implode(', ',$ids).") + ORDER BY `Date` DESC" + ); } else { - $sqlResults = DB::query("SELECT DISTINCT YEAR(`Date`) AS `Year` FROM `SiteTree$suffix` NATURAL JOIN `BlogEntry$suffix` WHERE `ParentID` = $id ORDER BY `Date` DESC"); + $sqlResults = DB::query(" + SELECT DISTINCT YEAR(`Date`) AS `Year` + FROM `SiteTree$suffix` NATURAL JOIN `BlogEntry$suffix` + WHERE `ParentID` in (".implode(', ',$ids).") + ORDER BY `Date` DESC" + ); } if(!$sqlResults) return new DataObjectSet(); @@ -83,9 +81,9 @@ class ArchiveWidget extends Widget { )); if($this->DisplayMode == 'month') { - $link = $blogHolder->Link() . $sqlResult['Year']. '/' . sprintf("%'02d", $sqlResult['Month']); + $link = $container->Link() . $sqlResult['Year']. '/' . sprintf("%'02d", $sqlResult['Month']); } else { - $link = $blogHolder->Link() . $sqlResult['Year']; + $link = $container->Link() . $sqlResult['Year']; } $results->push(new ArrayData(array( diff --git a/code/BlogHolder.php b/code/BlogHolder.php index 58eee37..e766729 100644 --- a/code/BlogHolder.php +++ b/code/BlogHolder.php @@ -5,59 +5,34 @@ */ /** - * Blog holder to display summarised blog entries + * Blog holder to display summarised blog entries. + * + * A blog holder is the leaf end of a BlogTree, but can also be used standalone in simpler circumstances. + * BlogHolders can only hold BlogEntries, BlogTrees can only hold BlogTrees and BlogHolders + * BlogHolders have a form on them for easy posting, and an owner that can post to them, BlogTrees don't */ - -class BlogHolder extends Page { +class BlogHolder extends BlogTree { static $icon = "blog/images/blogholder"; static $db = array( - 'LandingPageFreshness' => 'Varchar', 'Name' => 'Varchar', 'TrackBacksEnabled' => 'Boolean', 'AllowCustomAuthors' => 'Boolean', ); static $has_one = array( - "SideBar" => "WidgetArea", 'Owner' => 'Member', ); - static $has_many = array(); - - static $many_many = array(); - - static $belongs_many_many = array(); - - static $defaults = array(); - static $allowed_children = array( 'BlogEntry' ); function getCMSFields() { $fields = parent::getCMSFields(); - $fields->removeFieldFromTab("Root.Content.Main","Content"); - $fields->addFieldToTab("Root.Content.Widgets", new WidgetAreaEditor("SideBar")); $fields->addFieldToTab("Root.Content.Main", new TextField("Name", "Name of blog")); - - $fields->addFieldToTab('Root.Content.Main', new DropdownField('LandingPageFreshness', 'When you first open the blog, how many entries should I show', array( - "" => "All entries", - "1 MONTH" => "Last month's entries", - "2 MONTH" => "Last 2 months' entries", - "3 MONTH" => "Last 3 months' entries", - "4 MONTH" => "Last 4 months' entries", - "5 MONTH" => "Last 5 months' entries", - "6 MONTH" => "Last 6 months' entries", - "7 MONTH" => "Last 7 months' entries", - "8 MONTH" => "Last 8 months' entries", - "9 MONTH" => "Last 9 months' entries", - "10 MONTH" => "Last 10 months' entries", - "11 MONTH" => "Last 11 months' entries", - "12 MONTH" => "Last year's entries" - ))); - + $fields->addFieldToTab('Root.Content.Main', new CheckboxField('TrackBacksEnabled', 'Enable TrackBacks')); $fields->addFieldToTab('Root.Content.Main', new DropdownField('OwnerID', 'Blog owner', DataObject::get('Member')->toDropDownMap('ID', 'Name', 'None'))); $fields->addFieldToTab('Root.Content.Main', new CheckboxField('AllowCustomAuthors', 'Allow non-admins to have a custom author field')); @@ -65,41 +40,14 @@ class BlogHolder extends Page { return $fields; } - /** - * 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' - * @return DataObjectSet - */ - public function Entries($limit = '', $tag = '', $date = '') { - $tagCheck = ''; - $dateCheck = ''; - - if($tag) { - $SQL_tag = Convert::raw2sql($tag); - $tagCheck = "AND `BlogEntry`.Tags LIKE '%$SQL_tag%'"; - } - - if($date) { - if(strpos($date, '-')) { - $year = (int) substr($date, 0, strpos($date, '-')); - $month = (int) substr($date, strpos($date, '-') + 1); - - if($year && $month) { - $dateCheck = "AND MONTH(`BlogEntry`.Date) = $month AND YEAR(`BlogEntry`.Date) = $year"; - } - } else { - $year = (int) $date; - if($year) { - $dateCheck = "AND YEAR(`BlogEntry`.Date) = $year"; - } - } - } - - return DataObject::get("Page","`ParentID` = $this->ID $tagCheck $dateCheck","`BlogEntry`.Date DESC",'',"$limit"); + public function BlogHolderIDs() { + return array( $this->ID ); } - + + /* + * @todo: These next few functions don't really belong in the model. Can we remove them? + */ + /** * Only display the blog entries that have the specified tag */ @@ -180,88 +128,12 @@ class BlogHolder extends Page { } } -class BlogHolder_Controller extends Page_Controller { +class BlogHolder_Controller extends BlogTree_Controller { function init() { parent::init(); - - // This will create a tag point to the RSS feed - RSSFeed::linkToFeed($this->Link() . "rss", _t('BlogHolder.RSSFEED',"RSS feed of this blog")); - Requirements::themedCSS("blog"); Requirements::themedCSS("bbcodehelp"); - } - - function BlogEntries($limit = 10) { - $start = isset($_GET['start']) ? (int) $_GET['start'] : 0; - $tag = ''; - $date = ''; - if(Director::urlParams()) { - if(Director::urlParam('Action') == 'tag') { - $tag = Director::urlParam('ID'); - } else { - $year = Director::urlParam('Action'); - $month = Director::urlParam('ID'); - - if($month && is_numeric($month) && $month >= 1 && $month <= 12 && is_numeric($year)) { - $date = "$year-$month"; - } else if(is_numeric($year)) { - $date = $year; - } - } - } - - return $this->Entries("$start,$limit", $tag, $date); - } - - /** - * Gets the archived blogs for a particular month or year, in the format /year/month/ eg: /2008/10/ - */ - function showarchive() { - $month = addslashes($this->urlParams['ID']); - return array( - "Children" => DataObject::get('SiteTree', "ParentID = $this->ID AND DATE_FORMAT(`BlogEntry`.`Date`, '%Y-%M') = '$month'"), - ); - } - - function ArchiveMonths() { - $months = DB::query("SELECT DISTINCT DATE_FORMAT(`BlogEntry`.`Date`, '%M') AS `Month`, DATE_FORMAT(`BlogEntry`.`Date`, '%Y') AS `Year` FROM `BlogEntry` ORDER BY `BlogEntry`.`Date` DESC"); - $output = new DataObjectSet(); - foreach($months as $month) { - $month['Link'] = $this->Link() . "showarchive/$month[Year]-$month[Month]"; - $output->push(new ArrayData($month)); - } - - return $output; - } - - function tag() { - if($this->ShowTag()) { - return array( - 'Tag' => $this->ShowTag() - ); - } else { - return array(); - } - } - - /** - * Get the rss feed for this blog holder's entries - */ - function rss() { - global $project; - - $blogName = $this->Name; - $altBlogName = $project . ' blog'; - - $entries = $this->Entries(20); - - if($entries) { - $rss = new RSSFeed($entries, $this->Link() . 'rss', ($blogName ? $blogName : $altBlogName), "", "Title", "ParsedContent"); - $rss->outputToBrowser(); - } - } - /** * Return list of usable tags for help */ @@ -284,16 +156,7 @@ class BlogHolder_Controller extends Page_Controller { return $page->renderWith('Page'); } - - function defaultAction($action) { - // Protection against infinite loops when an RSS widget pointing to this page is added to this page - if(stristr($_SERVER['HTTP_USER_AGENT'], 'SimplePie')) { - return $this->rss(); - } - return parent::defaultAction($action); - } - /** * A simple form for creating blog entries */ diff --git a/code/BlogManagementWidget.php b/code/BlogManagementWidget.php index eca72ec..67503e6 100644 --- a/code/BlogManagementWidget.php +++ b/code/BlogManagementWidget.php @@ -46,28 +46,15 @@ class BlogManagementWidget extends Widget { } function WidgetHolder() { - if($this->getBlogHolder()->canEdit()) { - return $this->renderWith('WidgetHolder'); - } + $container = BlogTree::current(); + + if ($container && $container instanceof BlogHolder && $container->canEdit()) return $this->renderWith('WidgetHolder'); return ''; } function PostLink() { - $blogholder = $this->getBlogHolder(); - - return $blogholder->Link('post'); - } - - function getBlogHolder() { - $page = Director::currentPage(); - - if($page->is_a("BlogHolder")) { - return $page; - } else if($page->is_a("BlogEntry") && $page->getParent()->is_a("BlogHolder")) { - return $page->getParent(); - } else { - return DataObject::get_one("BlogHolder"); - } + $container = BlogTree::current(); + if ($container) return $container->Link('post'); } } diff --git a/code/BlogTree.php b/code/BlogTree.php new file mode 100644 index 0000000..a5f0dfe --- /dev/null +++ b/code/BlogTree.php @@ -0,0 +1,268 @@ + 'Boolean', + 'LandingPageFreshness' => 'Varchar', + ); + + static $defaults = array( + 'InheritSideBar' => True + ); + + static $has_one = array( + "SideBar" => "WidgetArea", + ); + + static $allowed_children = array( + 'BlogTree', 'BlogHolder' + ); + + /* + * Finds the BlogTree object most related to the current page. + * - If this page is a BlogTree, use that + * - If this page is a BlogEntry, use the parent Holder + * - Otherwise, try and find a 'top-level' BlogTree + */ + static function current() { + $page = Director::currentPage(); + + // If we _are_ a BlogTree, use us + if ($page instanceof BlogTree) return $page; + + // Or, if we a a BlogEntry underneath a BlogTree, use our parent + if ($page->is_a("BlogEntry")) { + $parent = $page->getParent(); + if ($parent instanceof BlogTree) return $parent; + } + + // Try to find a top-level BlogTree + $top = DataObject::get_one('BlogTree','ParentId = 0'); + if ($top) return $top; + + // Try to find any BlogTree that is not inside another BlogTree + foreach(DataObject::get('BlogTree') as $tree) { + if (!($tree->getParent() instanceof BlogTree)) return $tree; + } + + // This shouldn't be possible, but assuming the above fails, just return anything you can get + return DataObject::get_one('BlogTree'); + } + + /* ----------- ACCESSOR OVERRIDES -------------- */ + + public function getLandingPageFreshness() { + $freshness = $this->getField('LandingPageFreshness'); + // If we want to inherit freshness, try that first + if ($freshness = "INHERIT" && $this->getParent()) $freshness = $this->getParent()->LandingPageFreshness; + // If we don't have a parent, or the inherited result was still inherit, use default + if ($freshness = "INHERIT") $freshness = ''; + + return $freshness; + } + + function SideBar() { + if ($this->InheritSideBar && $this->getParent()) return $this->getParent()->SideBar() ; + return $this->getComponent('SideBar'); + } + + /* ----------- CMS CONTROL -------------- */ + + function getCMSFields() { + $fields = parent::getCMSFields(); + $fields->addFieldToTab('Root.Content.Main', new DropdownField('LandingPageFreshness', 'When you first open the blog, how many entries should I show', array( + "" => "All entries", + "1 MONTH" => "Last month's entries", + "2 MONTH" => "Last 2 months' entries", + "3 MONTH" => "Last 3 months' entries", + "4 MONTH" => "Last 4 months' entries", + "5 MONTH" => "Last 5 months' entries", + "6 MONTH" => "Last 6 months' entries", + "7 MONTH" => "Last 7 months' entries", + "8 MONTH" => "Last 8 months' entries", + "9 MONTH" => "Last 9 months' entries", + "10 MONTH" => "Last 10 months' entries", + "11 MONTH" => "Last 11 months' entries", + "12 MONTH" => "Last year's entries", + "INHERIT" => "Take value from parent Blog Tree" + ))); + + $fields->addFieldToTab("Root.Content.Widgets", new CheckboxField("InheritSideBar", 'Inherit Sidebar From Parent')); + $fields->addFieldToTab("Root.Content.Widgets", new WidgetAreaEditor("SideBar")); + + return $fields; + } + + /* ----------- New accessors -------------- */ + + public function loadDescendantBlogHolderIDListInto(&$idList) { + if ($children = $this->AllChildren()) { + foreach($children as $child) { + if (in_array($child->ID, $idList)) continue; + + if ($child instanceof BlogHolder) $idList[] = $child->ID; + else $child->loadDescendantBlogHolderIDListInto($idList); + } + } + } + + // Build a list of all IDs for BlogHolders that are children of us + public function BlogHolderIDs() { + $holderIDs = array(); + $this->loadDescendantBlogHolderIDListInto($holderIDs); + return $holderIDs; + } + + /** + * 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 + * @return DataObjectSet + */ + public function Entries($limit = '', $tag = '', $date = '', $retrieveCallback = null) { + $tagCheck = ''; + $dateCheck = ''; + + if ($tag) { + $SQL_tag = Convert::raw2sql($tag); + $tagCheck = "AND `BlogEntry`.Tags LIKE '%$SQL_tag%'"; + } + + if ($date) { + if(strpos($date, '-')) { + $year = (int) substr($date, 0, strpos($date, '-')); + $month = (int) substr($date, strpos($date, '-') + 1); + + if($year && $month) { + $dateCheck = "AND MONTH(`BlogEntry`.Date) = $month AND YEAR(`BlogEntry`.Date) = $year"; + } + } else { + $year = (int) $date; + if($year) { + $dateCheck = "AND YEAR(`BlogEntry`.Date) = $year"; + } + } + } + elseif ($this->LandingPageFreshness) { + $dateCheck = "AND `BlogEntry`.Date > NOW() - INTERVAL " . $this->LandingPageFreshness; + } + + // Build a list of all IDs for BlogHolders that are children of us + $holderIDs = $this->BlogHolderIDs(); + + // If no BlogHolders, no BlogEntries. So return false + if (empty($holderIDs)) return false; + + // Otherwise, do the actual query + $where = 'ParentID IN ('.implode(',', $holderIDs).") $tagCheck $dateCheck"; + + // By specifying a callback, you can alter the SQL, or sort on something other than date. + if ($retrieveCallback) return call_user_func($retrieveCallback, 'BlogEntry', $where, $limit); + else return DataObject::get('BlogEntry', $where, '`BlogEntry`.`Date` DESC', '', $limit); + } +} + +class BlogURL { + static function tag() { + if (Director::urlParam('Action') == 'tag') return Director::urlParam('ID'); + return ''; + } + + static function date() { + $year = Director::urlParam('Action'); + $month = Director::urlParam('ID'); + + if ($month && is_numeric($month) && $month >= 1 && $month <= 12 && is_numeric($year)) { + return "$year-$month"; + } + elseif (is_numeric($year)) { + return $year; + } + + return ''; + } +} + +class BlogTree_Controller extends Page_Controller { + function init() { + parent::init(); + + // This will create a tag point to the RSS feed + RSSFeed::linkToFeed($this->Link() . "rss", _t('BlogHolder.RSSFEED',"RSS feed of these blogs")); + Requirements::themedCSS("blog"); + } + + function BlogEntries($limit = 10) { + $start = isset($_GET['start']) ? (int) $_GET['start'] : 0; + return $this->Entries("$start,$limit", BlogURL::tag(), BlogURL::date()); + } + + /* + * @todo: It doesn't look like these are used. Remove if no-one complains - Hamish + + /** + * Gets the archived blogs for a particular month or year, in the format /year/month/ eg: /2008/10/ + * / + function showarchive() { + $month = addslashes($this->urlParams['ID']); + return array( + "Children" => DataObject::get('SiteTree', "ParentID = $this->ID AND DATE_FORMAT(`BlogEntry`.`Date`, '%Y-%M') = '$month'"), + ); + } + + function ArchiveMonths() { + $months = DB::query("SELECT DISTINCT DATE_FORMAT(`BlogEntry`.`Date`, '%M') AS `Month`, DATE_FORMAT(`BlogEntry`.`Date`, '%Y') AS `Year` FROM `BlogEntry` ORDER BY `BlogEntry`.`Date` DESC"); + $output = new DataObjectSet(); + foreach($months as $month) { + $month['Link'] = $this->Link() . "showarchive/$month[Year]-$month[Month]"; + $output->push(new ArrayData($month)); + } + + return $output; + } + + function tag() { + if (Director::urlParam('Action') == 'tag') { + return array( + 'Tag' => Convert::raw2xml(Director::urlParam('ID')) + ); + } + return array(); + } + */ + + /** + * Get the rss feed for this blog holder's entries + */ + function rss() { + global $project; + + $blogName = $this->Name; + $altBlogName = $project . ' blog'; + + $entries = $this->Entries(20); + + if($entries) { + $rss = new RSSFeed($entries, $this->Link(), ($blogName ? $blogName : $altBlogName), "", "Title", "ParsedContent"); + $rss->outputToBrowser(); + } + } + + function defaultAction($action) { + // Protection against infinite loops when an RSS widget pointing to this page is added to this page + if(stristr($_SERVER['HTTP_USER_AGENT'], 'SimplePie')) return $this->rss(); + + return parent::defaultAction($action); + } +} diff --git a/code/SubscribeRSSWidget.php b/code/SubscribeRSSWidget.php index e3f8c07..f2725e2 100644 --- a/code/SubscribeRSSWidget.php +++ b/code/SubscribeRSSWidget.php @@ -15,24 +15,6 @@ class SubscribeRSSWidget extends Widget { static $description = 'Shows a link allowing a user to subscribe to this blog via RSS.'; - /** - * Get the BlogHolder instance that this widget - * is located on. - * - * @return BlogHolder - */ - function getBlogHolder() { - $page = Director::currentPage(); - - if($page instanceof BlogHolder) { - return $page; - } elseif(($page instanceof BlogEntry) && ($page->getParent() instanceof BlogHolder)) { - return $page->getParent(); - } else { - return DataObject::get_one('BlogHolder'); - } - } - /** * Return an absolute URL based on the BlogHolder * that this widget is located on. @@ -41,10 +23,8 @@ class SubscribeRSSWidget extends Widget { */ function RSSLink() { Requirements::themedCSS('subscribersswidget'); - $blogHolder = $this->getBlogHolder(); - if($blogHolder) { - return $blogHolder->Link() . 'rss'; - } + $container = BlogTree::current(); + if ($container) return $container->Link() . 'rss'; } } diff --git a/code/TagCloudWidget.php b/code/TagCloudWidget.php index 6500c3b..e6925bb 100644 --- a/code/TagCloudWidget.php +++ b/code/TagCloudWidget.php @@ -24,19 +24,6 @@ class TagCloudWidget extends Widget { static $cmsTitle = "Tag Cloud"; static $description = "Shows a tag cloud of tags on your blog."; - function getBlogHolder() { - $page = Director::currentPage(); - - if($page->is_a("BlogHolder")) { - return $page; - } else if($page->is_a("BlogEntry") && $page->getParent()->is_a("BlogHolder")) { - return $page->getParent(); - } else { - return DataObject::get_one("BlogHolder"); - } - } - - function getCMSFields() { return new FieldSet( new TextField("Title", _t("TagCloudWidget.TILE", "Title")), @@ -54,9 +41,9 @@ class TagCloudWidget extends Widget { $allTags = array(); $max = 0; - $blogHolder = $this->getBlogHolder(); + $container = BlogTree::current(); - $entries = $blogHolder->Entries(); + $entries = $container->Entries(); if($entries) { foreach($entries as $entry) { @@ -117,7 +104,7 @@ class TagCloudWidget extends Widget { "Tag" => $tag, "Count" => $count, "Class" => $class, - "Link" => $blogHolder->Link() . 'tag/' . urlencode($tag) + "Link" => $container->Link() . 'tag/' . urlencode($tag) ); } } diff --git a/tests/BlogTreeTest.php b/tests/BlogTreeTest.php new file mode 100644 index 0000000..bb6349e --- /dev/null +++ b/tests/BlogTreeTest.php @@ -0,0 +1,30 @@ +fixture->objFromFixture('BlogTree', 'root'); + $this->assertEquals($node->Entries()->Count(), 3); + + $node = $this->fixture->objFromFixture('BlogTree', 'levela'); + $this->assertEquals($node->Entries()->Count(), 2); + + $node = $this->fixture->objFromFixture('BlogTree', 'levelaa'); + $this->assertEquals($node->Entries()->Count(), 2); + + $node = $this->fixture->objFromFixture('BlogTree', 'levelab'); + $this->assertNull($node->Entries()); + + $node = $this->fixture->objFromFixture('BlogTree', 'levelb'); + $this->assertEquals($node->Entries()->Count(), 1); + + $node = $this->fixture->objFromFixture('BlogTree', 'levelba'); + $this->assertEquals($node->Entries()->Count(), 1); + } + + + +} + +?> diff --git a/tests/BlogTreeTest.yml b/tests/BlogTreeTest.yml new file mode 100644 index 0000000..3112d40 --- /dev/null +++ b/tests/BlogTreeTest.yml @@ -0,0 +1,53 @@ +BlogTree: + root: + Title: Root BlogTree + levela: + Title: Level A + Parent: =>BlogTree.root + levelb: + Title: Level B + Parent: =>BlogTree.root + levelaa: + Title: Level AA + Parent: =>BlogTree.levela + levelab: + Title: Level AB + Parent: =>BlogTree.levela + levelba: + Title: Level BA + Parent: =>BlogTree.levelb +BlogHolder: + levelaa_blog1: + Title: Level AA Blog 1 + Parent: =>BlogTree.levelaa + levelaa_blog2: + Title: Level AA Blog 2 + Parent: =>BlogTree.levelaa + levelab_blog: + Title: Level AB Blog + Parent: =>BlogTree.levelab + levelba_blog: + Title: Level BA Blog + Parent: =>BlogTree.levelba +BlogEntry: + testpost: + Title: Test Post + URLSegment: test-post + Date: 2007-02-17 18:45:00 + Parent: =>BlogHolder.levelaa_blog1 + Tags: tag1,tag2 + testpost2: + Title: Test Post 2 + URLSegment: test-post-2 + Date: 2008-01-31 20:48:00 + Parent: =>BlogHolder.levelaa_blog2 + Tags: tag2,tag3 + testpost3: + Title: Test Post 3 + URLSegment: test-post-3 + Date: 2008-01-17 18:45:00 + Parent: =>BlogHolder.levelba_blog + Tags: tag1,tag2,tag3 + + +