From 983849449ed36fd0a359732a6dea6fe4e4779d2b Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Thu, 4 Feb 2010 05:00:19 +0000 Subject: [PATCH] API CHANGE: Introduced new API for SS_Report git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/cms/branches/2.4@98215 467b73ca-7a2a-4603-9d3b-597d59a354a9 --- _config.php | 10 +- code/Report.php | 472 +++++++++++++++++++++++++++++++++++++---- tests/SSReportTest.php | 2 +- 3 files changed, 436 insertions(+), 48 deletions(-) diff --git a/_config.php b/_config.php index 98bfb724..b73ceed1 100644 --- a/_config.php +++ b/_config.php @@ -51,11 +51,11 @@ HtmlEditorConfig::get('cms')->removeButtons('tablecontrols'); HtmlEditorConfig::get('cms')->addButtonsToLine(3, 'tablecontrols'); // Register default side reports -SSReport::register("SideReport", "SideReport_EmptyPages"); -SSReport::register("SideReport", "SideReport_RecentlyEdited"); -SSReport::register("SideReport", "SideReport_ToDo"); -if (class_exists('Subsite')) SSReport::register('ReportAdmin', 'SubsiteReportWrapper("BrokenLinksReport")',-20); -else SSReport::register('ReportAdmin', 'BrokenLinksReport',-20); +SS_Report::register("SideReport", "SideReport_EmptyPages"); +SS_Report::register("SideReport", "SideReport_RecentlyEdited"); +SS_Report::register("SideReport", "SideReport_ToDo"); +if (class_exists('Subsite')) SS_Report::register('ReportAdmin', 'SubsiteReportWrapper("BrokenLinksReport")',-20); +else SS_Report::register('ReportAdmin', 'BrokenLinksReport',-20); ?> diff --git a/code/Report.php b/code/Report.php index 71ff6ade..3fab8221 100644 --- a/code/Report.php +++ b/code/Report.php @@ -1,32 +1,52 @@ getReportField() + * Creating a new report is a matter overloading a few key methods * - * getReportField() should return a FormField instance, - * such as a ComplexTableField, or TableListField. This - * is the "meat" of the report, as it's designed to - * show the actual data for the function of the report. - * For example, if this was a report that should show - * all orders that aren't printed, then it would show - * a TableListField listing orders that have the property - * "Unprinted = 1". + * {@link title()}: Return the title - i18n is your responsibility + * {@link description()}: Return the description - i18n is your responsibility + * {@link sourceQuery()}: Return a DataObjectSet of the search results + * {@link columns()}: Return information about the columns in this report. + * {@link parameterFields()}: Return a FieldSet of the fields that can be used to filter this + * report. * - * @see ReportAdmin for where SS_Report instances are - * used in the CMS. + * If you can't express your report as a query, you can implement the this method instead: + * + * // Return an array of fields that can be used to sort the data + * public function sourceRecords($params, $sort, $limit) { ... } + * + * The $sort value will be set to the corresponding key of the columns() array. If you wish to + * make only a subset of the columns sortable, then you can override `sortColumns()` to return a + * subset of the array keys. + * + * Note that this implementation is less efficient and should only be used when necessary. + * + * If you wish to modify the report in more extreme ways, you could overload these methods instead. + * + * {@link getReportField()}: Return a FormField in the place where your report's TableListField + * usually appears. + * {@link getCMSFields()}: Return the FieldSet representing the complete right-hand area of the + * report, including the title, description, parameter fields, and results. + * + * Showing reports to the user + * =========================== + * + * Right now, all subclasses of SSReport will be shown in the ReportAdmin. However, we are planning + * on adding an explicit registration mechanism, so that you can decide which reports go in the + * report admin, and which go elsewhere (such as the side panel in the CMS). * * @package cms * @subpackage reports */ class SS_Report extends ViewableData { + /** + * Report registry populated by {@link SSReport::register()} + */ + private static $registered_reports = array(); /** * This is the title of the report, @@ -44,6 +64,98 @@ class SS_Report extends ViewableData { * @var string */ protected $description = ''; + + /** + * The class of object being managed by this report. + * Set by overriding in your subclass. + */ + protected $dataClass = 'SiteTree'; + + /** + * Return the title of this report. + * + * You have two ways of specifying the description: + * - overriding description(), which lets you support i18n + * - defining the $description property + */ + function title() { + return $this->title; + } + + /** + * Return the description of this report. + * + * You have two ways of specifying the description: + * - overriding description(), which lets you support i18n + * - defining the $description property + */ + function description() { + return $this->description; + } + + /** + * Return a FieldSet specifying the search criteria for this report. + * + * Override this method to define search criteria. + */ + function parameterFields() { + return null; + } + + /** + * Return the {@link SQLQuery} that provides your report data. + */ + function sourceQuery($params) { + if($this->hasMethod('sourceRecords')) { + $query = new SSReport_FakeQuery($this, 'sourceRecords', $params); + $query->setSortColumnMethod('sortColumns'); + return $query; + } else { + user_error("Please override sourceQuery()/sourceRecords() and columns() or, if necessary, override getReportField()", E_USER_ERROR); + } + } + + /** + * Return a DataObjectSet records for this report. + */ + function records($params) { + if($this->hasMethod('sourceRecords')) return $this->sourceRecords($params, null, null); + else { + $query = $this->sourceQuery(); + return singleton($this->dataClass())->buildDataObjectSet($query->execute(), "DataObjectSet", $query); + } + } + + /** + * Return an map of columns for your report. + * - The map keys will be the source columns for your report (in TableListField dot syntax) + * - The values can either be a string (the column title), or a map containing the following + * column parameters: + * - title: The column title + * - formatting: A formatting string passed to {@link TableListField::setFieldFormatting()} + */ + function columns() { + user_error("Please override sourceQuery() and columns() or, if necessary, override getReportField()", E_USER_ERROR); + } + + function sortColumns() { + return array_keys($this->columns()); + } + + /** + * Return the number of records in this report with no filters applied. + */ + function count() { + return (int)$this->sourceQuery(array())->unlimitedRowCount(); + } + + /** + * Return the data class for this report + */ + function dataClass() { + return $this->dataClass; + } + /** * Returns a FieldSet with which to create the CMS editing form. @@ -51,24 +163,93 @@ class SS_Report extends ViewableData { * data objects. * * @uses getReportField() to render a table, or similar field for the report. This - * method should be defined on the SS_Report subclasses. + * method should be defined on the SSReport subclasses. * * @return FieldSet */ function getCMSFields() { $fields = new FieldSet( - new TabSet('Root', - new Tab('Report', - new LiteralField('ReportTitle', "

{$this->title}

"), - new LiteralField('ReportDescription', "

{$this->description}

"), - $this->getReportField() - ) - ) + new LiteralField('ReportTitle', "

{$this->title()}

All times are in the server timezone, unless otherwise specified

") ); + + // $fields->push(); + + if($this->description) $fields->push( + new LiteralField('ReportDescription', "

{$this->description}

")); + + // Add search fields is available + if($params = $this->parameterFields()) { + $filters = new FieldGroup('Filters'); + foreach($params as $param) { + if ($param instanceof HiddenField) $fields->push($param); + else $filters->push($param); + } + $fields->push($filters); + // Add a search button + $fields->push(new FormAction('updatereport', 'Filter')); + } + + $fields->push($this->getReportField()); + + $this->extend('updateCMSFields', $fields); + return $fields; } + function getCMSActions() { + // getCMSActions() can be extended with updateCMSActions() on a decorator + $actions = new FieldSet(); + $this->extend('updateCMSActions', $actions); + return $actions; + } + + /** + * Return a field, such as a {@link ComplexTableField} that is + * used to show and manipulate data relating to this report. + * + * Generally, you should override {@link columns()} and {@link records()} to make your report, + * but if they aren't sufficiently flexible, then you can override this method. + * + * @return FormField subclass + */ + function getReportField() { + $columnTitles = array(); + $fieldFormatting = array(); + $csvFieldFormatting = array(); + $fieldCasting = array(); + + // Parse the column information + foreach($this->columns() as $source => $info) { + if(is_string($info)) $info = array('title' => $info); + + if(isset($info['formatting'])) $fieldFormatting[$source] = $info['formatting']; + if(isset($info['csvFormatting'])) $csvFieldFormatting[$source] = $info['csvFormatting']; + if(isset($info['casting'])) $fieldCasting[$source] = $info['casting']; + $columnTitles[$source] = isset($info['title']) ? $info['title'] : $source; + } + + // To do: implement pagination + $query = $this->sourceQuery($_REQUEST); + + $tlf = new TableListField('ReportContent', $this->dataClass(), $columnTitles); + $tlf->setCustomQuery($query); + $tlf->setShowPagination(true); + $tlf->setPageSize(50); + $tlf->setPermissions(array('export', 'print')); + + // Hack to figure out if we are printing + if (isset($_REQUEST['url']) && array_pop(explode('/', $_REQUEST['url'])) == 'printall') { + $tlf->setTemplate('SSReportTableField'); + } + + if($fieldFormatting) $tlf->setFieldFormatting($fieldFormatting); + if($csvFieldFormatting) $tlf->setCSVFieldFormatting($csvFieldFormatting); + if($fieldCasting) $tlf->setFieldCasting($fieldCasting); + + return $tlf; + } + /** * @param Member $member * @return boolean @@ -80,19 +261,7 @@ class SS_Report extends ViewableData { return true; } - - /** - * Return a field, such as a {@link ComplexTableField} that is - * used to show and manipulate data relating to this report. - * - * For example, if this were an "Unprinted Orders" report, this - * field would return a table that shows all Orders with "Unprinted = 1". - * - * @return FormField subclass - */ - function getReportField() { - user_error('Please implement getReportField() on ' . $this->class, E_USER_ERROR); - } + /** * Return the name of this report, which @@ -103,9 +272,9 @@ class SS_Report extends ViewableData { * @return string */ function TreeTitle() { - return $this->title; + return $this->title();/* . ' (' . $this->count() . ')'; - this is too slow atm */ } - + /** * Return the ID of this Report class. * Because it doesn't have a number, we @@ -116,7 +285,226 @@ class SS_Report extends ViewableData { function ID() { return $this->class; } + + ///////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Register a report. + * @param $list The list to add the report to: "ReportAdmin" or "SideReports" + * @param $reportClass The class of the report to add. + * @param $priority The priority. Higher numbers will appear furhter up in the reports list. + * The default value is zero. + */ + static function register($list, $reportClass, $priority = 0) { + if(strpos($reportClass, '(') === false && (!class_exists($reportClass) || !is_subclass_of($reportClass,'SSReport'))) { + user_error("SSReport::register(): '$reportClass' is not a subclass of SSReport", E_USER_WARNING); + return; + } + + self::$registered_reports[$list][$reportClass] = $priority; + } + + /** + * Unregister a report, removing it from the list + */ + static function unregister($list, $identifier) { + unset(self::$registered_reports[$list][$reportClass]); + } + + /** + * Return the SSReport objects making up the given list. + * @return An array of SSReport objects + */ + static function get_reports($list) { + $output = array(); + + if(isset(self::$registered_reports[$list])) { + $listItems = self::$registered_reports[$list]; + + // Sort by priority, preserving internal order of items with the same priority + $groupedItems = array(); + foreach($listItems as $k => $v) { + $groupedItems[$v][] = $k; + } + krsort($groupedItems); + $sortedListItems = call_user_func_array('array_merge', $groupedItems); + + foreach($sortedListItems as $report) { + if(strpos($report,'(') === false) $reportObj = new $report; + else $reportObj = eval("return new $report;"); + + $output[$reportObj->ID()] = $reportObj; + } + } + + return $output; + } } -?> \ No newline at end of file +/** + * This is an object that can be used to dress up a more complex querying mechanism in the clothing + * of a SQLQuery object. This means that you can inject it into a TableListField. + * + * Use it like this: + * + * function sourceQuery($params) { + * return new SSReport_FakeQuery($this, 'sourceRecords', $params) + * } + * function sourceRecords($params, $sort, $limit) { + * // Do some stuff + * // Return a DataObjectSet of actual objects. + * } + * + * This object is used by the default implementation of sourceQuery() on SSReport, to make use of + * a sourceReords() method if one exists. + */ +class SSReport_FakeQuery extends SQLQuery { + public $orderby; + public $limit; + + protected $obj, $method, $params; + + function __construct($obj, $method, $params) { + $this->obj = $obj; + $this->method = $method; + $this->params = $params; + } + + /** + * Provide a method that will return a list of columns that can be used to sort. + */ + function setSortColumnMethod($sortColMethod) { + $this->sortColMethod = $sortColMethod; + } + + function limit($limit) { + $this->limit = $limit; + } + + function unlimitedRowCount() { + $source = $this->obj->{$this->method}($this->params, null, null); + return $source ? $source->Count() : 0; + } + + function execute() { + $output = array(); + $source = $this->obj->{$this->method}($this->params, $this->orderby, $this->limit); + if($source) foreach($source as $item) { + $mapItem = $item->toMap(); + $mapItem['RecordClassName'] = get_class($item); + $output[] = $mapItem; + } + return $output; + } + + function canSortBy($fieldName) { + $fieldName = preg_replace('/(\s+?)(A|DE)SC$/', '', $fieldName); + if($this->sortColMethod) { + $columns = $this->obj->{$this->sortColMethod}(); + return in_array($fieldName, $columns); + } else { + return false; + } + } +} + +/** + * SSReportWrapper is a base class for creating report wappers. + * + * Wrappers encapsulate an existing report to alter their behaviour - they are implementations of + * the standard GoF decorator pattern. + * + * This base class ensure that, by default, wrappers behave in the same way as the report that is + * being wrapped. You should override any methods that need to behave differently in your subclass + * of SSReportWrapper. + * + * It also makes calls to 2 empty methods that you can override {@link beforeQuery()} and + * {@link afterQuery()} + */ + +abstract class SSReportWrapper extends SSReport { + protected $baseReport; + + function __construct($baseReport) { + $this->baseReport = is_string($baseReport) ? new $baseReport : $baseReport; + $this->dataClass = $this->baseReport->dataClass(); + parent::__construct(); + } + + function ID() { + return get_class($this->baseReport) . '_' . get_class($this); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Filtering + + function parameterFields() { + return $this->baseReport->parameterFields(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Columns + + function columns() { + return $this->baseReport->columns(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Querying + + /** + * Override this method to perform some actions prior to querying. + */ + function beforeQuery($params) { + } + + /** + * Override this method to perform some actions after querying. + */ + function afterQuery() { + } + + function sourceQuery($params) { + if($this->baseReport->hasMethod('sourceRecords')) { + // The default implementation will create a fake query from our sourceRecords() method + return parent::sourceQuery($params); + + } else if($this->baseReport->hasMethod('sourceQuery')) { + $this->beforeQuery($params); + $query = $this->baseReport->sourceQuery($params); + $this->afterQuery(); + return $query; + + } else { + user_error("Please override sourceQuery()/sourceRecords() and columns() in your base report", E_USER_ERROR); + } + + } + + function sourceRecords($params, $sort, $limit) { + $this->beforeQuery($params); + $records = $this->baseReport->sourceRecords($params, $sort, $limit); + $this->afterQuery(); + return $records; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Pass-through + + function title() { + return $this->baseReport->title(); + } + + function description() { + return $this->baseReport->title(); + } + + function canView() { + return $this->baseReport->canView(); + } + +} + +?> diff --git a/tests/SSReportTest.php b/tests/SSReportTest.php index 2f637e44..14ec8a8e 100644 --- a/tests/SSReportTest.php +++ b/tests/SSReportTest.php @@ -10,7 +10,7 @@ class SSReportTest extends SapphireTest { } } -class SSReportTest_FakeTest extends SSReport implements TestOnly { +class SSReportTest_FakeTest extends SS_Report implements TestOnly { function title() { return 'Report title'; }