API CHANGE Removed CMSSiteTreeFilter->showInList(), using custom logic in CMSMain->SearchTreeForm() instead

API CHANGE Returning arrays instead of Query resource from CMSSiteTreeFilter->pagesIncluded()
MINOR Removed unused LeftAndMain->getMarkingFilter() and CMSMainMarkingFilter, now handled by CMSSiteTreeFilter and CMSMain->SearchTreeForm()
ENHANCEMENT Moved 'page tree filter' dropdown logic into an additional option for CMSMain->SearchTreeForm() (originally implemented in r83674)

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/cms/trunk@92938 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Ingo Schommer 2009-11-22 08:23:12 +00:00
parent 7a1e3a9bda
commit a5070b858a
7 changed files with 339 additions and 163 deletions

View File

@ -133,11 +133,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return $this->getSiteTreeFor($this->stat('tree_class'));
}
protected function getMarkingFilter($params) {
return new CMSMainMarkingFilter($params);
}
public function generateDataTreeHints() {
$classes = ClassInfo::subclassesFor( $this->stat('tree_class') );
@ -1055,20 +1051,24 @@ JS;
array_shift($filters);
// add filters to map
foreach($filters as $filter) {
if(!call_user_func(array($filter, 'showInList'))) continue;
$filterMap[$filter] = call_user_func(array($filter, 'title'));
}
// ensure that 'all pages' filter is on top position
uasort($filterMap,
create_function('$a,$b', 'return ($a == "CMSSiteTreeFilter_Search") ? 1 : -1;')
);
$showDefaultFields = array();
$form = new Form(
$this,
'SearchTreeForm',
new FieldSet(
new TextField(
// TODO i18n
$showDefaultFields[] = new DropdownField('FilterClass', 'Pages', $filterMap),
$showDefaultFields[] = new TextField(
'Title',
_t('CMSMain.TITLEOPT', 'Title')
),
new DropdownField('filter', 'Type', $filterMap, null, null, 'Any'),
new TextField('Content', 'Text'),
new CalendarDateField('EditedSince', _t('CMSMain_left.ss.EDITEDSINCE','Edited Since')),
new DropdownField('ClassName', 'Page Type', $pageTypes, null, null, 'Any'),
@ -1100,15 +1100,19 @@ JS;
)
)
);
$form->setFormMethod('GET');
$form->disableSecurityToken();
$form->unsetValidator();
foreach($showDefaultFields as $f) $f->addExtraClass('show-default');
return $form;
}
function doSearchTree($data, $form) {
return $this->getsubtree($this->request);
}
function publishall() {
ini_set("memory_limit", -1);
ini_set('max_execution_time', 0);
@ -1342,46 +1346,4 @@ JS;
}
}
class CMSMainMarkingFilter extends LeftAndMainMarkingFilter{
protected function getQuery($params) {
$where = array();
$SQL_params = Convert::raw2sql($params);
foreach($SQL_params as $name => $val) {
switch($name) {
// Match against URLSegment, Title, MenuTitle & Content
case 'SiteTreeSearchTerm':
$where[] = "\"URLSegment\" LIKE '%$val%' OR \"Title\" LIKE '%$val%' OR \"MenuTitle\" LIKE '%$val%' OR \"Content\" LIKE '%$val%'";
break;
// Match against date
case 'SiteTreeFilterDate':
$val = ((int)substr($val,6,4))
. '-' . ((int)substr($val,3,2))
. '-' . ((int)substr($val,0,2));
$where[] = "\"LastEdited\" > '$val'";
break;
// Match against exact ClassName
case 'ClassName':
if($val && $val != 'All') {
$where[] = "\"ClassName\" = '$val'";
}
break;
default:
// Partial string match against a variety of fields
if(!empty($val) && singleton("SiteTree")->hasDatabaseField($name)) {
$where[] = "\"$name\" LIKE '%$val%'";
}
}
}
return new SQLQuery(
array("ParentID", "ID"),
'SiteTree',
$where
);
}
}
?>
?>

View File

@ -15,150 +15,208 @@
*/
abstract class CMSSiteTreeFilter extends Object {
protected $ids = null;
protected $expanded = array();
/**
* @var Array Search parameters, mostly properties on {@link SiteTree}.
* Caution: Unescaped data.
*/
protected $params = array();
static function showInList() {
return true;
}
function getTree() {
if(method_exists($this, 'pagesIncluded')) {
$this->populateIDs();
}
$leftAndMain = new LeftAndMain();
$tree = $leftAndMain->getSiteTreeFor('SiteTree', isset($_REQUEST['ID']) ? $_REQUEST['ID'] : 0, null, array($this, 'includeInTree'));
// Trim off the outer tag
$tree = ereg_replace('^[ \t\r\n]*<ul[^>]*>','', $tree);
$tree = ereg_replace('</ul[^>]*>[ \t\r\n]*$','', $tree);
return $tree;
/**
* @var Array
*/
protected $_cache_ids = null;
/**
* @var Array
*/
protected $_cache_expanded = array();
/**
* @var String
*/
protected $childrenMethod = null;
function __construct($params = null) {
if($params) $this->params = $params;
parent::__construct();
}
/**
* Populate $this->ids with the IDs of the pages returned by pagesIncluded(), also including
* @return String Method on {@link Hierarchy} objects
* which is used to traverse into children relationships.
*/
function getChildrenMethod() {
return $this->childrenMethod;
}
/**
* @return Array Map of Page IDs to their respective ParentID values.
*/
function pagesIncluded() {}
/**
* Populate the IDs of the pages returned by pagesIncluded(), also including
* the necessary parent helper pages.
*/
protected function populateIDs() {
if($res = $this->pagesIncluded()) {
$parents = array();
$this->_cache_ids = array();
if($pages = $this->pagesIncluded()) {
/* And keep a record of parents we don't need to get parents of themselves, as well as IDs to mark */
foreach($res as $row) {
if ($row['ParentID']) $parents[$row['ParentID']] = true;
$this->ids[$row['ID']] = true;
// And keep a record of parents we don't need to get
// parents of themselves, as well as IDs to mark
foreach($pages as $pageArr) {
$parents[$pageArr['ParentID']] = true;
$this->_cache_ids[$pageArr['ID']] = true;
}
while (!empty($parents)) {
$res = DB::query('SELECT "ParentID", "ID" FROM "SiteTree" WHERE "ID" in ('.implode(',',array_keys($parents)).')');
$parents = array();
if(!empty($parents)) {
$q = new SQLQuery();
$q->select(array('ID','ParentID'))
->from('SiteTree')
->where('"ID" in ('.implode(',',array_keys($parents)).')');
foreach($res as $row) {
foreach($q->execute() as $row) {
if ($row['ParentID']) $parents[$row['ParentID']] = true;
$this->ids[$row['ID']] = true;
$this->expanded[$row['ID']] = true;
$this->_cache_ids[$row['ID']] = true;
$this->_cache_expanded[$row['ID']] = true;
}
}
}
}
/**
* Returns true if the given page should be included in the tree.
* Returns TRUE if the given page should be included in the tree.
* Caution: Does NOT check view permissions on the page.
*
* @param SiteTree $page
* @return Boolean
*/
public function includeInTree($page) {
return isset($this->ids[$page->ID]) && $this->ids[$page->ID] ? true : false;
public function isPageIncluded($page) {
if($this->_cache_ids === NULL) $this->populateIDs();
return (isset($this->_cache_ids[$page->ID]) && $this->_cache_ids[$page->ID]);
}
}
/**
* Works a bit different than the other filters:
* Shows all pages *including* those deleted from stage and live.
* It does not filter out pages still existing in the different stages.
*
* @package cms
* @subpackage content
*/
class CMSSiteTreeFilter_DeletedPages extends CMSSiteTreeFilter {
protected $childrenMethod = "AllHistoricalChildren";
static function title() {
// TODO i18n
return "Deleted pages";
}
function getTree() {
$leftAndMain = new LeftAndMain();
$tree = $leftAndMain->getSiteTreeFor('SiteTree', isset($_REQUEST['ID']) ? $_REQUEST['ID'] : 0, "AllHistoricalChildren");
// Trim off the outer tag
$tree = ereg_replace('^[ \t\r\n]*<ul[^>]*>','', $tree);
$tree = ereg_replace('</ul[^>]*>[ \t\r\n]*$','', $tree);
return $tree;
function pagesIncluded() {
$ids = array();
// TODO Not very memory efficient, but usually not very many deleted pages exist
$pages = Versioned::get_including_deleted('SiteTree');
if($pages) foreach($pages as $page) {
$ids[] = array('ID' => $page->ID, 'ParentID' => $page->ParentID);
}
return $ids;
}
}
/**
* Gets all pages which have changed on stage.
*
* @package cms
* @subpackage content
*/
class CMSSiteTreeFilter_ChangedPages extends CMSSiteTreeFilter {
static function title() {
// TODO i18n
return "Changed pages";
}
function pagesIncluded() {
return DB::query('SELECT "ParentID", "ID" FROM "SiteTree" WHERE "Status" LIKE \'Saved%\'');
$ids = array();
$q = new SQLQuery();
$q->select(array('"SiteTree"."ID"','"SiteTree"."ParentID"'))
->from('SiteTree')
->leftJoin("SiteTree_Live", '"SiteTree_Live"."ID" = "SiteTree"."ID"')
->where('"SiteTree"."Version" > "SiteTree_Live"."Version"');
foreach($q->execute() as $row) {
$ids[] = array('ID'=>$row['ID'],'ParentID'=>$row['ParentID']);
}
return $ids;
}
}
/**
* @package cms
* @subpackage content
*/
class CMSSiteTreeFilter_Search extends CMSSiteTreeFilter {
public $data;
function __construct() {
$this->data = $_REQUEST;
}
static function showInList() { return false; }
static function title() {
return "Search";
// TODO i18n
return "All pages";
}
/**
* Retun an array of maps containing the keys, 'ID' and 'ParentID' for each page to be displayed
* in the search.
*
* @return Array
*/
function pagesIncluded() {
$data = $this->data;
$this->ids = array();
$this->expanded = array();
$ids = array();
$q = new SQLQuery();
$q->select(array('ID','ParentID'))
->from('SiteTree');
$where = array();
// Match against URLSegment, Title, MenuTitle & Content
if (isset($data['SiteTreeSearchTerm'])) {
$term = Convert::raw2sql($data['SiteTreeSearchTerm']);
$where[] = "\"URLSegment\" LIKE '%$term%' OR \"Title\" LIKE '%$term%' OR \"MenuTitle\" LIKE '%$term%' OR \"Content\" LIKE '%$term%'";
}
// Match against date
if (isset($data['SiteTreeFilterDate'])) {
$date = $data['SiteTreeFilterDate'];
$date = ((int)substr($date,6,4)) . '-' . ((int)substr($date,3,2)) . '-' . ((int)substr($date,0,2));
$where[] = "\"LastEdited\" > '$date'";
}
// Match against exact ClassName
if (isset($data['ClassName']) && $data['ClassName'] != 'All') {
$klass = Convert::raw2sql($data['ClassName']);
$where[] = "\"ClassName\" = '$klass'";
}
// Partial string match against a variety of fields
foreach (CMSMain::T_SiteTreeFilterOptions() as $key => $value) {
if (!empty($data[$key])) {
$match = Convert::raw2sql($data[$key]);
$where[] = "\"$key\" LIKE '%$match%'";
$SQL_params = Convert::raw2sql($this->params);
foreach($SQL_params as $name => $val) {
switch($name) {
// Match against URLSegment, Title, MenuTitle & Content
case 'SiteTreeSearchTerm':
$where[] = "\"URLSegment\" LIKE '%$val%' OR \"Title\" LIKE '%$val%' OR \"MenuTitle\" LIKE '%$val%' OR \"Content\" LIKE '%$val%'";
break;
// Match against date
case 'SiteTreeFilterDate':
// TODO Date Parsing
$val = ((int)substr($val,6,4))
. '-' . ((int)substr($val,3,2))
. '-' . ((int)substr($val,0,2));
$where[] = "\"LastEdited\" > '$val'";
break;
// Match against exact ClassName
case 'ClassName':
if($val && $val != 'All') {
$where[] = "\"ClassName\" = '$val'";
}
break;
default:
// Partial string match against a variety of fields
if(!empty($val) && singleton("SiteTree")->hasDatabaseField($name)) {
$where[] = "\"$name\" LIKE '%$val%'";
}
}
}
$q->where(empty($where) ? '' : '(' . implode(') AND (',$where) . ')');
$where = empty($where) ? '' : 'WHERE (' . implode(') AND (',$where) . ')';
foreach($q->execute() as $row) {
$ids[] = array('ID'=>$row['ID'],'ParentID'=>$row['ParentID']);
}
$parents = array();
/* Do the actual search */
$res = DB::query('SELECT "ParentID", "ID" FROM "SiteTree" '.$where);
return $res;
return $ids;
}
}
}

View File

@ -448,18 +448,22 @@ class LeftAndMain extends Controller {
}
}
/**
* @return String HTML
*/
public function SiteTreeAsUL() {
return $this->getSiteTreeFor($this->stat('tree_class'));
}
/**
* Get a site tree displaying the nodes under the given objects.
* Get a site tree HTML listing which displays the nodes under the given criteria.
*
* @param $className The class of the root object
* @param $rootID The ID of the root object. If this is null then a complete tree will be
* shown
* @param $childrenMethod The method to call to get the children of the tree. For example,
* Children, AllChildrenIncludingDeleted, or AllHistoricalChildren
* @return String Nested <ul> list with links to each page
*/
function getSiteTreeFor($className, $rootID = null, $childrenMethod = null, $filterFunction = null, $minNodeCount = 30) {
// Default childrenMethod
@ -518,27 +522,31 @@ class LeftAndMain extends Controller {
* If ID = 0, then get the whole tree.
*/
public function getsubtree($request) {
$tree = $this->getSiteTreeFor(
if($filterClass = $request->requestVar('FilterClass')) {
if(!is_subclass_of($filterClass, 'CMSSiteTreeFilter')) {
throw new Exception(sprintf('Invalid filter class passed: %s', $filterClass));
}
$filter = new $filterClass($request->requestVars());
} else {
$filter = null;
}
$html = $this->getSiteTreeFor(
$this->stat('tree_class'),
$request->getVar('ID'),
null,
null,
$request->getVar('minNodeCount')
($filter) ? array($filter, 'isPageIncluded') : null,
$request->getVar('minNodeCount'),
($filter) ? $filter->getChildrenMethod() : null
);
// Trim off the outer tag
$tree = ereg_replace('^[ \t\r\n]*<ul[^>]*>','', $tree);
$tree = ereg_replace('</ul[^>]*>[ \t\r\n]*$','', $tree);
$html = ereg_replace('^[ \t\r\n]*<ul[^>]*>','', $html);
$html = ereg_replace('</ul[^>]*>[ \t\r\n]*$','', $html);
return $tree;
}
/**
* @param array $params
* @return LeftAndMainMarkingFilter
*/
protected function getMarkingFilter($params) {
return new LeftAndMainMarkingFilter($params);
return $html;
}
/**

View File

@ -37,7 +37,7 @@
var self = this;
// only the first field should be visible by default
this.find('.field').not(':first').hide();
this.find('.field').not('.show-default').hide();
// generate the field dropdown
this.setSelectEl($('<select name="options" class="options"></select>')
@ -46,6 +46,13 @@
);
this._setOptions();
// special case: we can't use CMSSiteTreeFilter together with other options
this.find('select[name=FilterClass]').change(function(e) {
var others = self.find('.field').not($(this).parents('.field')).find(':input,:select');
if(e.target.value == 'CMSSiteTreeFilter_Search') others.removeAttr('disabled');
else others.attr('disabled','disabled');
})
this._super();
},
@ -61,7 +68,7 @@
jQuery('<option value="0">Add Criteria</option>').appendTo(self.getSelectEl());
// populate dropdown values from existing fields
this.find('.field').each(function() {
this.find('.field').not(':visible').each(function() {
$('<option />').appendTo(self.getSelectEl())
.val(this.id)
.text($(this).find('label').text());
@ -100,7 +107,7 @@
onreset: function(e) {
this.find('.field :input').clearFields();
this.find('.field').not(':first').hide();
this.find('.field').not('.show-default').hide();
// Reset URL to default
$('#sitetree')[0].clearCustomURL();

View File

@ -0,0 +1,35 @@
<?php
class CMSMainSearchTreeFormTest extends FunctionalTest {
static $fixture_file = 'cms/tests/CMSMainTest.yml';
protected $autoFollowRedirection = false;
function testTitleFilter() {
$this->session()->inst_set('loggedInAs', $this->idFromFixture('Member', 'admin'));
$response = $this->get(
'admin/SearchTreeForm/?' .
http_build_query(array(
'Title' => 'Page 1',
'FilterClass' => 'CMSSiteTreeFilter_Search',
'action_doSearchTree' => true
))
);
$titles = $this->getPageTitles();
$this->assertEquals(count($titles), 1);
// For some reason the title gets split into two lines
$this->assertContains('Page 1', $titles[0]);
}
protected function getPageTitles() {
$titles = array();
$links = $this->cssParser()->getBySelector('li.class-Page a');
if($links) foreach($links as $link) {
$titles[] = preg_replace('/\n/', ' ', $link->asXML());
}
return $titles;
}
}

View File

@ -0,0 +1,87 @@
<?php
class CMSSiteTreeFilterTest extends SapphireTest {
static $fixture_file = 'cms/tests/CMSSiteTreeFilterTest.yml';
function testSearchFilterEmpty() {
$page1 = $this->objFromFixture('Page', 'page1');
$page2 = $this->objFromFixture('Page', 'page2');
$f = new CMSSiteTreeFilter_Search();
$results = $f->pagesIncluded();
$this->assertTrue($f->isPageIncluded($page1));
$this->assertTrue($f->isPageIncluded($page2));
}
function testSearchFilterByTitle() {
$page1 = $this->objFromFixture('Page', 'page1');
$page2 = $this->objFromFixture('Page', 'page2');
$f = new CMSSiteTreeFilter_Search(array('Title' => 'Page 1'));
$results = $f->pagesIncluded();
$this->assertTrue($f->isPageIncluded($page1));
$this->assertFalse($f->isPageIncluded($page2));
$this->assertEquals(1, count($results));
$this->assertEquals(
array('ID' => $page1->ID, 'ParentID' => 0),
$results[0]
);
}
function testIncludesParentsForNestedMatches() {
$parent = $this->objFromFixture('Page', 'page3');
$child = $this->objFromFixture('Page', 'page3b');
$f = new CMSSiteTreeFilter_Search(array('Title' => 'Page 3b'));
$results = $f->pagesIncluded();
$this->assertTrue($f->isPageIncluded($parent));
$this->assertTrue($f->isPageIncluded($child));
$this->assertEquals(1, count($results));
$this->assertEquals(
array('ID' => $child->ID, 'ParentID' => $parent->ID),
$results[0]
);
}
function testChangedPagesFilter() {
$unchangedPage = $this->objFromFixture('Page', 'page1');
$unchangedPage->doPublish();
$changedPage = $this->objFromFixture('Page', 'page2');
$changedPage->MetaTitle = 'Original';
$changedPage->publish('Stage', 'Live');
$changedPage->MetaTitle = 'Changed';
$changedPage->write();
$f = new CMSSiteTreeFilter_ChangedPages();
$results = $f->pagesIncluded();
$this->assertTrue($f->isPageIncluded($changedPage));
$this->assertFalse($f->isPageIncluded($unchangedPage));
$this->assertEquals(1, count($results));
$this->assertEquals(
array('ID' => $changedPage->ID, 'ParentID' => 0),
$results[0]
);
}
function testDeletedPagesFilter() {
$deletedPage = $this->objFromFixture('Page', 'page2');
$deletedPage->publish('Stage', 'Live');
$deletedPageID = $deletedPage->ID;
$deletedPage->delete();
$deletedPage = Versioned::get_one_by_stage(
'SiteTree',
'Live',
sprintf('"SiteTree_Live"."ID" = %d', $deletedPageID)
);
$f = new CMSSiteTreeFilter_DeletedPages();
$results = $f->pagesIncluded();
$this->assertTrue($f->isPageIncluded($deletedPage));
}
}

View File

@ -0,0 +1,19 @@
Page:
page1:
Title: Page 1
page2:
Title: Page 2
page3:
Title: Page 3
page2a:
Parent: =>Page.page2
Title: Page 2a
page2b:
Parent: =>Page.page2
Title: Page 2b
page3a:
Parent: =>Page.page3
Title: Page 3a
page3b:
Parent: =>Page.page3
Title: Page 3b