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. * You can use the extend() method of FieldSet to create customised forms for your other * data objects. * * @uses getReportField() to render a table, or similar field for the report. This * method should be defined on the SSReport subclasses. * * @return FieldSet */ function getCMSFields() { $fields = new FieldSet( 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 */ function canView($member = null) { if(!$member && $member !== FALSE) { $member = Member::currentUser(); } return true; } /** * Return the name of this report, which * is used by the templates to render the * name of the report in the report tree, * the left hand pane inside ReportAdmin. * * @return string */ function TreeTitle() { 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 * use the class name as the ID. * * @return string */ 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; } } /** * 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(); } } ?>