├── .editorconfig ├── .gitattributes ├── CHANGELOG ├── LICENSE ├── README.md ├── VERSION ├── _config.php ├── _config ├── config.yml ├── extensions.yml └── injector.yml ├── code ├── admin │ ├── AdvancedReportsAdmin.php │ └── AdvancedReportsAdminItemRequest.php ├── dataobjects │ ├── AdvancedReport.php │ ├── CombinedReport.php │ ├── DataObjectReport.php │ ├── FreeformReport.php │ └── RelatedReport.php ├── extensions │ ├── ScheduledAdvancedReportExtension.php │ └── ScheduledReportExtension.php ├── filters │ └── IsNullFilter.php ├── formatters │ ├── CsvReportFormatter.php │ ├── DateFromTimestampFormatter.php │ ├── DecimalHoursFormatter.php │ ├── HtmlReportFormatter.php │ ├── ReportFieldFormatter.php │ ├── ReportFormatter.php │ └── SecondsToHoursFormatter.php ├── jobs │ └── ScheduledReportJob.php ├── pages │ ├── ReportHolder.php │ └── ReportPage.php └── services │ ├── AdvancedReportsService.php │ └── AdvancedReportsServiceInterface.php ├── composer.json ├── css ├── cms.css └── html-report.css ├── images ├── bar-chart.png ├── csv.png ├── html.png └── pdf.png ├── javascript ├── advanced-report-settings.js └── scheduled-report-settings.js └── templates ├── AdvancedReport_csv.ss ├── AdvancedReport_html.ss ├── CombinedReport_csv.ss ├── CombinedReport_html.ss └── Layout ├── ReportHolder.ss └── ReportPage.ss /.editorconfig: -------------------------------------------------------------------------------- 1 | # For more information about the properties used in this file, 2 | # please see the EditorConfig documentation: 3 | # http://editorconfig.org 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [{*.yml,package.json}] 14 | indent_size = 2 15 | 16 | # The indent size used in the package.json file cannot be changed: 17 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /docs export-ignore 2 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 2013-10-07 v2.1.1 2 | ----------------- 3 | 4 | * Fixed can* methods for RelatedReport 5 | * Fixed how default parameters are passed through for combined report 6 | 7 | 8 | 2013-09-30 v2.1.0 9 | ----------------- 10 | 11 | * Added further parsing of default param value through the 'filter' 12 | mechanism for param based conditions 13 | 14 | 2013-08-26 v1.3.1 15 | ----------------- 16 | 17 | * Added support for custom field formatters to allowed displayed values to be 18 | overridden from code 19 | 20 | 2013-08-25 v2.0.0 21 | ----------------- 22 | 23 | * Upgrade to SS3 compatibility. 24 | * Report settings fields are now generated in a `getSettingsFields` method, 25 | rather than using the `updateReportsSettingsFields` system. 26 | * The field formatting system has changed from an using evals to using 27 | callbacks. 28 | 29 | 2013-08-04 v1.2.1 30 | ----------------- 31 | 32 | * Fixed reference to LastModified that should be LastEdited 33 | 34 | 2013-07-31 v1.2.0 35 | ----------------- 36 | 37 | * Added emailing of scheduled reports 38 | * Included has_ones in the fields available for a generic data object report 39 | to assist in filtering 40 | 41 | 42 | 2011-11-28 v1.1.2 43 | ----------------- 44 | 45 | * Dotted fields are escaped properly on pagination vars 46 | 47 | 2011-11-21 v1.1.1 48 | ----------------- 49 | 50 | * Added ability to specify your own addition method for items being summed in 51 | rows so to provide the ability to handle adding non-numeric types more 52 | gracefully (eg times) 53 | 54 | 2011-11-18 v1.1.0 55 | ----------------- 56 | 57 | * Added ability to specify parameters for conditions that get bound 58 | by either a GET value, or by specifying a default in the 59 | ReportParams key value field 60 | 61 | 2011-08-25 v1.0.0 62 | ----------------- 63 | 64 | * Added the ability to filter / bind condition values at the last minute for 65 | conditions. Simply provide a prefix and a callable in getConditionFilters; 66 | see AdvancedReport::getConditionFilters() for an example. 67 | * Moved the settings of the report to a separate tab 68 | 69 | 2011-04-05 v0.1.2 70 | ----------------- 71 | 72 | * Fixed problem where empty values were not being passed through to formatting functions 73 | * Fixed formatting of table in PDF 74 | * Allow PDF previews 75 | 76 | 2011-03-28 v0.1.1 77 | ----------------- 78 | 79 | * Ability to specific IS NOT NULL and IS NULL conditions 80 | 81 | 2011-02-15 v0.1.0 82 | ----------------- 83 | 84 | * First release 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, SilverStripe Australia. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | Neither the name of the organisation nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SilverStripe Advanced Reports Module 2 | 3 | ## Maintainer Contact 4 | 5 | Marcus Nyeholt 6 | 7 | 8 | 9 | ## Requirements 10 | 11 | * SilverStripe 3.1.x 12 | * QueuedJobs module for scheduled report generation 13 | https://github.com/nyeholt/silverstripe-queuedjobs 14 | * PDFRendition module for PDF generation of reports 15 | https://github.com/nyeholt/silverstripe-pdfrendition 16 | * MultiValueField for selecting fields 17 | https://github.com/nyeholt/silverstripe-multivaluefield 18 | 19 | 20 | ## Documentation 21 | 22 | See https://github.com/nyeholt/silverstripe-advancedreports/wiki 23 | 24 | The Advanced Report module provides a flexible mechanism for defining reports 25 | that are more complex in structure than the standard SilverStripe reports. 26 | Advanced Reports are saved as files in the filesystem, allowing snapshots 27 | of information to be saved for later perusal. 28 | 29 | In addition to manually generating reports, they can be scheduled for automatic 30 | generation on a schedule. 31 | 32 | ## Quick Usage Overview 33 | 34 | 35 | 36 | ## Troubleshooting 37 | 38 | 39 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 2.1.1 2 | -------------------------------------------------------------------------------- /_config.php: -------------------------------------------------------------------------------- 1 | sanitiseClassName($this->modelClass); 27 | $grid = $form->Fields()->dataFieldByName($name); 28 | 29 | $grid->getConfig()->getComponentByType('GridFieldDetailForm')->setItemRequestClass( 30 | 'AdvancedReportsAdminItemRequest' 31 | ); 32 | 33 | if (class_exists('GridFieldCopyButton')) { 34 | $grid->getConfig()->addComponent(new GridFieldCopyButton(), 'GridFieldEditButton'); 35 | } 36 | 37 | return $form; 38 | } 39 | 40 | public function getList() { 41 | return parent::getList()->filter('ReportID', 0); 42 | } 43 | 44 | /** 45 | * If no managed models are explicitly defined, then default to displaying 46 | * all available reports. 47 | * 48 | * @return array 49 | */ 50 | public function getManagedModels() { 51 | if($this->managedModels !== null) { 52 | return $this->managedModels; 53 | } 54 | 55 | if($this->stat('managed_models')) { 56 | $result = parent::getManagedModels(); 57 | } else { 58 | $classes = ClassInfo::subclassesFor('AdvancedReport'); 59 | $result = array(); 60 | 61 | array_shift($classes); 62 | 63 | foreach($classes as $class) { 64 | $result[$class] = array('title' => singleton($class)->singular_name()); 65 | } 66 | } 67 | 68 | return $this->managedModels = $result; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /code/admin/AdvancedReportsAdminItemRequest.php: -------------------------------------------------------------------------------- 1 | record->isInDB() && $this->record->canGenerate()) { 16 | $form->Actions()->merge(array( 17 | FormAction::create('reportpreview', '') 18 | ->setTitle(_t('AdvancedReport.PREVIEW', 'Preview')) 19 | ->setAttribute('target', '_blank') 20 | ->setAttribute('data-icon', 'preview'), 21 | FormAction::create('generate', '') 22 | ->setTitle(_t('AdvancedReport.GENERATE', 'Generate')) 23 | )); 24 | } 25 | 26 | return $form; 27 | } 28 | 29 | /** 30 | * Handler to view a generated report file 31 | * 32 | * @param type $data 33 | * @param type $form 34 | */ 35 | public function viewreport($request) { 36 | $item = 1; 37 | $allowed = array('html', 'pdf', 'csv'); 38 | $ext = $request->getExtension(); 39 | if (!in_array($ext, $allowed)) { 40 | return $this->httpError(404); 41 | } 42 | $reportID = (int) $request->param('ID'); 43 | $fileID = (int) $request->param('OtherID'); 44 | 45 | $report = AdvancedReport::get()->byID($reportID); 46 | if (!$report || !$report->canView()) { 47 | return $this->httpError(404); 48 | } 49 | $file = $report->{strtoupper($ext) . 'File'}(); 50 | if (!$file || !strlen($file->Content)) { 51 | return $this->httpError(404); 52 | } 53 | 54 | $mimeType = HTTP::get_mime_type($file->Name); 55 | header("Content-Type: {$mimeType}; name=\"" . addslashes($file->Name) . "\""); 56 | header("Content-Disposition: attachment; filename=" . addslashes($file->Name)); 57 | header("Content-Length: {$file->getSize()}"); 58 | header("Pragma: "); 59 | 60 | session_write_close(); 61 | ob_flush(); 62 | flush(); 63 | // Push the file while not EOF and connection exists 64 | echo base64_decode($file->Content); 65 | exit(); 66 | } 67 | 68 | public function reportpreview($data, $form) { 69 | $data = $form->getData(); 70 | $format = $data['PreviewFormat']; 71 | 72 | $result = $this->record->createReport($format); 73 | 74 | if($result->content) { 75 | return $result->content; 76 | } else { 77 | return SS_HTTPRequest::send_file( 78 | file_get_contents($result->filename), "$data[GeneratedReportTitle].$format" 79 | ); 80 | } 81 | } 82 | 83 | public function generate($data, $form) { 84 | $report = $this->record; 85 | 86 | if(!empty($data['GeneratedReportTitle'])) { 87 | $title = $data['GeneratedReportTitle']; 88 | } else { 89 | $title = $report->Title; 90 | } 91 | 92 | $report->GeneratedReportTitle = $title; 93 | $report->prepareAndGenerate(); 94 | 95 | return Controller::curr()->redirect($this->Link()); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /code/dataobjects/AdvancedReport.php: -------------------------------------------------------------------------------- 1 | 'html'); 27 | 28 | /** 29 | * Enables or disables PDF generation using the PDF rendition module. 30 | * 31 | * By default PDF generation is enabled if the PDF rendition module is 32 | * installed. 33 | * 34 | * @var bool 35 | * @config 36 | */ 37 | private static $generate_pdf = false; 38 | 39 | /** 40 | * Do we allow groupby and sum settings? 41 | * 42 | * Child classes can set this to true 43 | * 44 | * @var boolean 45 | */ 46 | private static $allow_grouping = false; 47 | 48 | /** 49 | * A list of allowed filter conditions. 50 | * 51 | * @var array 52 | * @config 53 | */ 54 | private static $allowed_conditions = array( 55 | 'ExactMatch' => '=', 56 | 'ExactMatch:not' => '!=', 57 | 'GreaterThanOrEqual' => '>=', 58 | 'GreaterThan' => '>', 59 | 'LessThan' => '<', 60 | 'LessThanOrEqual' => '<=', 61 | 'InList' => 'In List', 62 | 'IsNull' => 'IS NULL', 63 | 'IsNull:not' => 'IS NOT NULL' 64 | ); 65 | 66 | private static $db = array( 67 | 'Title' => 'Varchar(128)', 68 | 'GeneratedReportTitle' => 'Varchar(128)', 69 | 'Description' => 'Text', 70 | 'ReportFields' => 'MultiValueField', 71 | 'ReportHeaders' => 'MultiValueField', 72 | 'ConditionFields' => 'MultiValueField', 73 | 'ConditionOps' => 'MultiValueField', 74 | 'ConditionValues' => 'MultiValueField', 75 | 'PaginateBy' => 'Varchar(64)', // a field used to separate tables (eg financial years) 76 | 'PageHeader' => 'Varchar(64)', // used as a keyworded string for pages 77 | 78 | // optional fields that child classes will need to provide implementation for 79 | 'GroupBy' => 'MultiValueField', 80 | 'SumFields' => 'MultiValueField', 81 | 82 | 'SortBy' => 'MultiValueField', 83 | 'SortDir' => 'MultiValueField', 84 | 'ClearColumns' => 'MultiValueField', 85 | 'AddInRows' => 'MultiValueField', // which fields in each row should be added? 86 | 'AddCols' => 'MultiValueField', // Which columns should be added ? 87 | 'NumericSort' => 'MultiValueField', // columns to be numericly sorted 88 | 'ReportParams' => 'MultiValueField', // provide some defaults for parameterised reports 89 | 'FieldFormattingField' => 'MultiValueField', // list of fields which should be formated somehow 90 | 'FieldFormattingFormatter' => 'MultiValueField', // list of used formatter for this 91 | ); 92 | 93 | private $field_labels = array( 94 | 'ReportFields' => 'Fields', 95 | 'ReportHeaders' => 'Field Headers', 96 | 'ConditionFields' => 'Conditions', 97 | 'PaginateBy' => 'Paginate By', 98 | 'SortBy' => 'Sort Field', 99 | 'SortDir' => 'Sort Order', 100 | ); 101 | 102 | 103 | private static $has_one = array( 104 | 'Report' => 'AdvancedReport', // never set for the 'template' report for a page, but used to 105 | // list all the generated reports. 106 | 'HTMLFile' => 'File', 107 | 'CSVFile' => 'File', 108 | 'PDFFile' => 'File', 109 | ); 110 | 111 | private static $has_many = array( 112 | 'Reports' => 'AdvancedReport', 113 | ); 114 | 115 | private static $default_sort = "Title ASC"; 116 | 117 | private static $searchable_fields = array( 118 | 'Title', 119 | 'Description', 120 | ); 121 | 122 | private static $summary_fields = array( 123 | 'Title', 124 | 'Description' 125 | ); 126 | 127 | /** 128 | * Should generated report contents be stored on the file object? 129 | * 130 | * @var boolean 131 | */ 132 | private static $store_file_content = false; 133 | 134 | /** 135 | * Gets the form fields for display in the CMS. 136 | * 137 | * The actual report configuration fields should be generated by 138 | * {@link getSettingsFields()}, so they can also be used in the front end. 139 | * 140 | * @return FieldList 141 | */ 142 | public function getCMSFields() { 143 | Requirements::css('advancedreports/css/cms.css'); 144 | 145 | $fields = new FieldList(array( 146 | new TabSet('Root', new Tab( 147 | 'Main', 148 | new GridField( 149 | 'GeneratedReports', 150 | _t('AdvancedReport.GENERATED_REPORTS', 'Generated Reports'), 151 | $this->Reports()->sort('Created', 'DESC'), 152 | $config = GridFieldConfig_Base::create() 153 | ->addComponent(new GridFieldDeleteAction()) 154 | ) 155 | )) 156 | )); 157 | 158 | $columns = $config->getComponentByType('GridFieldDataColumns'); 159 | 160 | $columns->setDisplayFields(array( 161 | 'Title' => _t('AdvancedReport.TITLE', 'Title'), 162 | 'Created' => _t('AdvancedReport.GENERATED_AT', 'Generated At'), 163 | 'Links' => _t('AdvancedReport.LINKS', 'Links') 164 | )); 165 | 166 | $columns->setFieldFormatting(array( 167 | 'Links' => function($value, $item) { 168 | $result = ''; 169 | $links = array('html', 'csv'); 170 | 171 | if($item->config()->generate_pdf) { 172 | $links[] = 'pdf'; 173 | } 174 | 175 | foreach($links as $type) { 176 | $result .= sprintf( 177 | '%s', 178 | $item->getFileLink($type), 179 | strtoupper($type) 180 | ); 181 | } 182 | 183 | return $result; 184 | } 185 | )); 186 | 187 | if($this->isInDB() && $this->canGenerate()) { 188 | $options = array( 189 | 'html' => 'HTML', 'csv' => 'CSV', 'pdf' => 'PDF' 190 | ); 191 | if (!class_exists('PDFRenditionService')) { 192 | unset($options['pdf']); 193 | } 194 | 195 | $fields->addFieldsToTab( 196 | 'Root.Main', 197 | array( 198 | DropdownField::create('PreviewFormat') 199 | ->setTitle(_t('AdvancedReport.PREVIEW_FORMAT', 'Preview format')) 200 | ->setSource($options), 201 | TextField::create('GeneratedReportTitle') 202 | ->setTitle(_t('AdvancedReport.GENERATED_TITLE', 'Generated report title')) 203 | ->setValue($this->Title) 204 | ), 205 | 'GeneratedReports' 206 | ); 207 | } 208 | 209 | if($this->canEdit()) { 210 | $fields->addFieldsToTab('Root.Settings', $this->getSettingsFields()); 211 | } 212 | 213 | $this->extend('updateCMSFields', $fields); 214 | 215 | return $fields; 216 | } 217 | 218 | /** 219 | * Gets the form fields for configuring the report settings. 220 | * 221 | * @return FieldList 222 | */ 223 | public function getSettingsFields() { 224 | $reportable = $this->getReportableFields(); 225 | $converted = array(); 226 | 227 | foreach($reportable as $k => $v) { 228 | if(preg_match('/^(.*) +AS +"([^"]*)"/i', $k, $matches)) { 229 | $k = $matches[2]; 230 | } 231 | $converted[$this->dottedFieldToUnique($k)] = $v; 232 | } 233 | 234 | $fieldsGroup = new FieldGroup( 235 | 'Fields', 236 | MultiValueDropdownField::create('ReportFields') 237 | ->setTitle(_t('AdvancedReport.REPORT_FIELDS', 'Report Fields')) 238 | ->setSource($reportable) 239 | ->addExtraClass('advanced-report-field-names'), 240 | MultiValueTextField::create('ReportHeaders') 241 | ->setTitle(_t('AdvancedReport.REPORT_HEADERS', 'Headers')) 242 | ->addExtraClass('advanced-report-field-headers') 243 | ); 244 | $fieldsGroup->setName('FieldsGroup'); 245 | $fieldsGroup->addExtraClass('advanced-report-fields dropdown'); 246 | 247 | $conditionsGroup = new FieldGroup( 248 | 'Conditions', 249 | new MultiValueDropdownField( 250 | 'ConditionFields', 251 | _t('AdvancedReport.CONDITION_FIELDS', 'Condition Fields'), 252 | $reportable 253 | ), 254 | new MultiValueDropdownField( 255 | 'ConditionOps', 256 | _t('AdvancedReport.CONDITION_OPERATIONS', 'Operation'), 257 | $this->config()->allowed_conditions 258 | ), 259 | new MultiValueTextField( 260 | 'ConditionValues', 261 | _t('AdvancedReport.CONDITION_VALUES', 'Value') 262 | ) 263 | ); 264 | $conditionsGroup->setName('ConditionsGroup'); 265 | $conditionsGroup->addExtraClass('dropdown'); 266 | 267 | // define the group for the sort field 268 | $sortGroup = new FieldGroup( 269 | 'Sort', 270 | new MultiValueDropdownField( 271 | 'SortBy', 272 | _t('AdvancedReport.SORTED_BY', 'Sorted By'), 273 | $reportable 274 | ), 275 | new MultiValueDropdownField( 276 | 'SortDir', 277 | _t('AdvancedReport.SORT_DIRECTION', 'Sort Direction'), 278 | array( 279 | 'ASC' => _t('AdvancedReport.ASC', 'Ascending'), 280 | 'DESC' => _t('AdvancedReport.DESC', 'Descending') 281 | ) 282 | ) 283 | ); 284 | $sortGroup->setName('SortGroup'); 285 | $sortGroup->addExtraClass('dropdown'); 286 | 287 | 288 | // build a list of the formatters 289 | $formatters = ClassInfo::implementorsOf('ReportFieldFormatter'); 290 | $fmtrs = array(); 291 | foreach ($formatters as $formatterClass) { 292 | $formatter = new $formatterClass(); 293 | $fmtrs[$formatterClass] = $formatter->label(); 294 | } 295 | 296 | // define the group for the custom field formatters 297 | $fieldFormattingGroup = new FieldGroup( 298 | _t('AdvancedReport.FORMAT_FIELDS', 'Custom field formatting'), 299 | new MultiValueDropdownField( 300 | 'FieldFormattingField', 301 | _t('AdvancedReport.FIELDFORMATTING', 'Field'), 302 | $converted 303 | ), 304 | new MultiValueDropdownField( 305 | 'FieldFormattingFormatter', 306 | _t('AdvancedReport.FIELDFORMATTINGFORMATTER', 'Formatter'), 307 | $fmtrs 308 | ) 309 | ); 310 | $fieldFormattingGroup->setName('FieldFormattingGroup'); 311 | $fieldFormattingGroup->addExtraClass('dropdown'); 312 | 313 | // assemble the fieldlist 314 | $fields = new FieldList( 315 | new TextField('Title', _t('AdvancedReport.TITLE', 'Title')), 316 | new TextareaField( 317 | 'Description', 318 | _t('AdvancedReport.DESCRIPTION', 'Description') 319 | ), 320 | $fieldsGroup, 321 | $conditionsGroup, 322 | new KeyValueField( 323 | 'ReportParams', 324 | _t('AdvancedReport.REPORT_PARAMETERS', 'Default report parameters') 325 | ), 326 | $sortGroup, 327 | new MultiValueDropdownField( 328 | 'NumericSort', 329 | _t('AdvancedReport.SORT_NUMERICALLY', 'Sort these fields numerically'), 330 | $reportable 331 | ), 332 | DropdownField::create('PaginateBy') 333 | ->setTitle(_t('AdvancedReport.PAGINATE_BY', 'Paginate By')) 334 | ->setSource($reportable) 335 | ->setHasEmptyDefault(true), 336 | TextField::create('PageHeader') 337 | ->setTitle(_t('AdvancedReport.HEADER_TEXT', 'Header text')) 338 | ->setDescription(_t('AdvancedReport.USE_NAME_FOR_PAGE_NAME', 'use $name for the page name')) 339 | ->setValue('$name'), 340 | new MultiValueDropdownField( 341 | 'AddInRows', 342 | _t('AdvancedReport.ADD_IN_ROWS', 'Add these columns for each row'), 343 | $converted 344 | ), 345 | new MultiValueDropdownField( 346 | 'AddCols', 347 | _t('AdvancedReport.ADD_IN_ROWS', 'Provide totals for these columns'), 348 | $converted 349 | ), 350 | $fieldFormattingGroup, 351 | new MultiValueDropdownField( 352 | 'ClearColumns', 353 | _t('AdvancedReport.CLEARED_COLS', '"Cleared" columns'), 354 | $converted 355 | ) 356 | ); 357 | 358 | if ($this->config()->allow_grouping) { 359 | // GroupBy 360 | $groupingGroup = new FieldGroup( 361 | 'Grouping', 362 | new MultiValueDropdownField( 363 | 'GroupBy', 364 | _t('AdvancedReport.GROUPBY_FIELDS', 'Group by fields'), 365 | $reportable 366 | ), 367 | new MultiValueDropdownField( 368 | 'SumFields', 369 | _t('AdvancedReport.SUM_FIELDS', 'SUM fields'), 370 | $reportable 371 | ) 372 | ); 373 | $groupingGroup->addExtraClass('dropdown'); 374 | $fields->insertAfter($groupingGroup, 'Conditions'); 375 | } 376 | 377 | 378 | if($this->hasMethod('updateReportFields')) { 379 | Deprecation::notice( 380 | '3.0', 381 | 'The updateReportFields method is deprecated, instead overload getSettingsFields' 382 | ); 383 | 384 | $this->updateReportFields($fields); 385 | } 386 | 387 | $this->extend('updateSettingsFields', $fields); 388 | return $fields; 389 | } 390 | 391 | /** 392 | * Prepare and generate this report into report instances 393 | * 394 | * @return AdvancedReport 395 | */ 396 | public function prepareAndGenerate() { 397 | $report = $this->duplicate(false); 398 | $report->ReportID = $this->ID; 399 | $report->Created = SS_Datetime::now(); 400 | $report->LastEdited = SS_Datetime::now(); 401 | $report->Title = $this->GeneratedReportTitle; 402 | $report->write(); 403 | 404 | $report->generateReport('html'); 405 | $report->generateReport('csv'); 406 | if($this->config()->generate_pdf) $report->generateReport('pdf'); 407 | 408 | return $report; 409 | } 410 | 411 | /** 412 | * Get a link to a specific instance of this report. 413 | * 414 | * @param string $type 415 | * @return string 416 | */ 417 | public function getFileLink($type) { 418 | $file = $this->{strtoupper($type) . 'File'}(); // ->Link(); 419 | if ($this->config()->store_file_content) { 420 | return Controller::join_links( 421 | 'admin/advanced-reports/AdvancedDisruptionReport/EditForm/field/AdvancedDisruptionReport/item', 422 | $this->ID, 423 | 'viewreport', 424 | $file->Name 425 | ); 426 | } else { 427 | return $file->Link(); 428 | } 429 | } 430 | 431 | /** 432 | * Abstract method; actual reports should define this. 433 | */ 434 | public function getReportName() { 435 | throw new Exception("Abstract method called; please implement getReportName()"); 436 | } 437 | 438 | /** 439 | * Gets an array of field names that can be used in this report 440 | * 441 | * Override to specify your own values. 442 | */ 443 | protected function getReportableFields() { 444 | return array('Title' => 'Title'); 445 | } 446 | 447 | /** 448 | * Converts a field in dotted notation (as used in some report selects) to a unique name 449 | * that can be used for, eg "Table.Field AS Table_Field" so that we don't have problems with 450 | * duplicity in queries, and mapping them back and forth 451 | * 452 | * We keep this as a method to ensure that we're explicity as to what/why we're doing 453 | * this so that when someone comes along later, it's not toooo wtfy 454 | * 455 | * @param string $field 456 | * @return string 457 | */ 458 | public function dottedFieldToUnique($field) { 459 | return str_replace('.', '_', $field); 460 | } 461 | 462 | /** 463 | * Determine the class that defines the given field. 464 | * 465 | * This will look through all parent classes and return the class that has a dbtable that defines the 466 | * field. 467 | * 468 | * @param string $type 469 | * The base data object type the field is being referenced in 470 | * @param string $field 471 | * The field being referenced 472 | * @return string 473 | */ 474 | protected function tableSpacedField($type, $field) { 475 | $types = ClassInfo::ancestry($type, true); 476 | $class = ''; 477 | foreach (array_reverse($types) as $class) { 478 | // check its DB and whether it defines the field 479 | $db = Config::inst()->get($class, 'db', Config::UNINHERITED); 480 | if (isset($db[$field])) { 481 | break; 482 | } 483 | } 484 | 485 | if (!$class) { 486 | $class = $type; 487 | } 488 | // if we fall through to here, we assume that we're just going to use the base data table 489 | return '"' . Convert::raw2sql($class). '"."' . Convert::raw2sql($field) . '"'; 490 | } 491 | 492 | /** 493 | * Return the 'included fields' list. 494 | * 495 | * @return 496 | */ 497 | public function getHeaders() { 498 | $headers = array(); 499 | 500 | $reportFields = $this->getReportableFields(); 501 | $sel = $this->ReportFields->getValues(); 502 | $headerTitles = $this->ReportHeaders->getValues(); 503 | $selected = array(); 504 | 505 | for ($i = 0, $c = count($sel); $i < $c; $i++) { 506 | $field = $sel[$i]; 507 | 508 | if(preg_match('/^(.*) +AS +"?([^"]*)"?/i', $field, $matches)) { 509 | $field = $matches[2]; 510 | } 511 | 512 | $fieldName = $this->dottedFieldToUnique($field); 513 | 514 | if (isset($selected[$field])) { 515 | $selected[$field]++; 516 | $fieldName .= '_' . $selected[$field]; 517 | } 518 | 519 | if (isset($headerTitles[$i])) { 520 | $headers[$fieldName] = $headerTitles[$i]; 521 | } else { 522 | $headers[$fieldName] = (isset($reportFields[$field]) ? $reportFields[$field] : $field); 523 | } 524 | 525 | if (!isset($selected[$field])) { 526 | $selected[$field] = 1; 527 | } 528 | } 529 | 530 | return $headers; 531 | } 532 | 533 | /** 534 | * Retrieve the raw data objects set for this report 535 | * 536 | * Note that the "DataObjects" don't necessarily need to implement DataObjectInterface; 537 | * we can return whatever objects (or array maps) that we like. 538 | * 539 | */ 540 | public function getDataObjects() { 541 | throw new Exception("Abstract method called; please implement getDataObjects()"); 542 | } 543 | 544 | /** 545 | * Get the selected report fields in a format suitable to be put in an 546 | * SQL select (an array format) 547 | * 548 | * @return array 549 | */ 550 | protected function getReportFieldsForQuery() { 551 | $fields = $this->ReportFields->getValues(); 552 | $reportFields = $this->getReportableFields(); 553 | $sortVals = $this->SortBy->getValues(); 554 | 555 | if (!$sortVals) { 556 | $sortVals = array(); 557 | } 558 | 559 | $toSelect = array(); 560 | $selected = array(); 561 | 562 | // make sure our sortvals are in the query too 563 | foreach ($sortVals as $sortOpt) { 564 | if (!in_array($sortOpt, $fields)) { 565 | $fields[] = $sortOpt; 566 | } 567 | } 568 | 569 | foreach ($fields as $field) { 570 | if (isset($reportFields[$field])) { 571 | $fieldName = $field; 572 | if (strpos($field, ' AS ')) { 573 | // do nothing to the field?? 574 | } else if (strpos($field, '.')) { 575 | $parts = explode('.', $field); 576 | $sep = ''; 577 | $quotedField = implode('"."', $parts); 578 | 579 | if (isset($selected[$fieldName])) { 580 | $selected[$fieldName]++; 581 | $field = $field . '_' . $selected[$fieldName]; 582 | } 583 | 584 | $field = '"'.$quotedField . '" AS "' . $this->dottedFieldToUnique($field) . '"'; 585 | } else { 586 | if (isset($selected[$fieldName])) { 587 | $selected[$fieldName]++; 588 | $field = '"'.$field.'" AS "'.$field . '_' . $selected[$fieldName].'"'; 589 | } else { 590 | $field = '"'.$field.'"'; 591 | } 592 | } 593 | $toSelect[] = $field; 594 | } 595 | 596 | if (!isset($selected[$fieldName])) { 597 | $selected[$fieldName] = 1; 598 | } 599 | } 600 | 601 | return $toSelect; 602 | } 603 | 604 | /** 605 | * Return an array of FieldValuePrefix => Callable 606 | * filters for changing the values of the condition value 607 | * 608 | * This is so that you can do things like strtotime() in conditions for 609 | * a date field, for example. 610 | * 611 | * Everything AFTER the prefix given here is passed through to the 612 | * callable, so you can handle the passing of parameters manually 613 | * if needed 614 | * 615 | * @return array 616 | */ 617 | protected function getConditionFilters() { 618 | $defaultFilters = new ConditionFilters(); 619 | 620 | return array( 621 | 'strtotime:' => array($defaultFilters, 'strtotimeDateValue'), 622 | 'param:' => array($defaultFilters, 'paramValue'), 623 | ); 624 | } 625 | 626 | /** 627 | * Generate a WHERE clause based on the input the user provided. 628 | * 629 | * Assumes the user has provided some values for the $this->ConditionFields etc. Converts 630 | * everything to an array that is run through the dbQuote() util method that handles all the 631 | * escaping 632 | * 633 | * @param $defaults 634 | * Some hardcoded default conditions applied 635 | * 636 | * @param $withOperands 637 | * Whether the return should be as SQL operands instead of ORM filters 638 | * 639 | * @return array 640 | */ 641 | public function getConditions($defaults = array(), $withOperands = false) { 642 | $reportFields = $this->getReportableFields(); 643 | $fields = $this->ConditionFields->getValues(); 644 | if (!$fields || !count($fields)) { 645 | return array(); 646 | } 647 | 648 | $ops = $this->ConditionOps->getValues(); 649 | $vals = $this->ConditionValues->getValues(); 650 | 651 | $filter = $defaults; 652 | $conditions = $this->config()->allowed_conditions; 653 | $conditionFilters = $this->getConditionFilters(); 654 | 655 | for ($i = 0, $c = count($fields); $i < $c; $i++) { 656 | $field = $fields[$i]; 657 | if (!isset($ops[$i]) || !isset($vals[$i])) { 658 | continue; 659 | } 660 | 661 | $op = $ops[$i]; 662 | if (!isset($conditions[$op])) { 663 | continue; 664 | } 665 | 666 | $originalVal = $val = $vals[$i]; 667 | $val = $this->applyFiltersToValue($originalVal); 668 | 669 | switch ($op) { 670 | case 'InList': { 671 | $op = 'ExactMatch'; 672 | $val = explode(',', $val); 673 | break; 674 | } 675 | case 'IS': 676 | case 'IS NOT': { 677 | if (strtolower($val) == 'null') { 678 | $val = null; 679 | } 680 | break; 681 | } 682 | } 683 | 684 | if ($withOperands) { 685 | $rawOp = $conditions[$op]; 686 | if (is_array($val)) { 687 | $rawOp = 'IN'; 688 | $filter[] = $field . ' ' . $rawOp . ' (' . implode(',', Convert::raw2sql($val)) .')'; 689 | } else { 690 | $filter[] = $field . ' ' . $rawOp . ' \'' . Convert::raw2sql($val) . '\''; 691 | } 692 | 693 | } else { 694 | $filter[$field . ':' . $op] = $val; 695 | } 696 | } 697 | 698 | return $filter; 699 | } 700 | 701 | /** 702 | * Apply some filters to a condition value for use in a query 703 | * 704 | * @param string $originalVal 705 | * @return string 706 | */ 707 | public function applyFiltersToValue($originalVal) { 708 | $filters = $this->getConditionFilters(); 709 | 710 | foreach ($filters as $prefix => $callable) { 711 | if (strpos($originalVal, $prefix) === 0) { 712 | $val = substr($originalVal, strlen($prefix)); 713 | return call_user_func($callable, $val, $this); 714 | } 715 | } 716 | 717 | return $originalVal; 718 | } 719 | 720 | 721 | /** 722 | * Helper method that applies the given filters to a specific DataQuery object 723 | * 724 | * Replicates similar functionality in DataList 725 | * 726 | * @param DataQuery $dataQuery 727 | * @param array $filterArray 728 | */ 729 | protected function getWhereClause($filterArray, $baseType) { 730 | $parts = array(); 731 | $allowed = self::config()->allowed_conditions; 732 | 733 | if (is_array($filterArray) && count($filterArray)) { 734 | foreach($filterArray as $field => $value) { 735 | $fieldArgs = explode(':', $field); 736 | $field = array_shift($fieldArgs); 737 | $filterType = array_shift($fieldArgs); 738 | $modifiers = $fieldArgs; 739 | $originalFilter = $filterType; 740 | if (count($modifiers)) { 741 | $originalFilter = $originalFilter . ':' . implode(':', $modifiers); 742 | } 743 | 744 | if (!isset($allowed[$originalFilter])) { 745 | continue; 746 | } 747 | 748 | // actually escape the field 749 | if (!strpos($field, '.')) { 750 | $field = $this->tableSpacedField($baseType, $field); 751 | } 752 | 753 | $operator = $allowed[$originalFilter]; 754 | 755 | $parts[$field . ' ' . $operator] = $value; 756 | } 757 | } 758 | 759 | 760 | $where = ''; 761 | 762 | if (count($parts)) { 763 | $where = $this->dbQuote($parts); 764 | } 765 | return $where; 766 | } 767 | 768 | /** 769 | * Gets a string that represents the possible 'sort' options. 770 | * 771 | * @return string 772 | */ 773 | protected function getSort() { 774 | $sortBy = ''; 775 | $sortVals = $this->SortBy->getValues(); 776 | $dirs = $this->SortDir->getValues(); 777 | 778 | $dir = 'ASC'; 779 | 780 | $reportFields = $this->getReportableFields(); 781 | $numericSort = $this->getNumericSortFields(); 782 | 783 | if (count($sortVals)) { 784 | $sep = ''; 785 | $index = 0; 786 | foreach ($sortVals as $sortOpt) { 787 | // check we're not injecting an invalid sort 788 | if (isset($reportFields[$sortOpt])) { 789 | // update the dir to match, if available, otherwise just use the last one 790 | if (isset($dirs[$index])) { 791 | if (in_array($dirs[$index], array('ASC', 'DESC'))) { 792 | $dir = $dirs[$index]; 793 | } 794 | } 795 | 796 | $sortOpt = $this->dottedFieldToUnique($sortOpt); 797 | 798 | // see http://blog.feedmarker.com/2006/02/01/how-to-do-natural-alpha-numeric-sort-in-mysql/ 799 | // for why we're + 0 here. Basically, coercing an alphanum sort instead of straight string 800 | if (is_array($numericSort) && in_array($sortOpt, $numericSort)) { 801 | $sortOpt .= '+0'; 802 | } 803 | $sortBy .= $sep . $sortOpt . ' ' . $dir; 804 | $sep = ', '; 805 | } 806 | $index++; 807 | } 808 | } else { 809 | $sortBy = 'ID '.$dir; 810 | } 811 | 812 | return $sortBy; 813 | } 814 | 815 | /** 816 | * Return any fields that need special 'numeric' sorting. This allows sorting of numbers 817 | * in strings, so that 818 | * 819 | * 1-document.txt 820 | * 2-document.txt 821 | * 11-document.txt 822 | * 823 | * are sorted in their correct order, and the '11' document doesn't come immediately 824 | * after the '1' document. 825 | * 826 | */ 827 | protected function getNumericSortFields() { 828 | if ($this->NumericSort) { 829 | return $this->NumericSort->getValue(); 830 | } 831 | return array(); 832 | } 833 | 834 | 835 | /** 836 | * Get a list of columns that should have subsequent duplicated entries 'blanked' out 837 | * 838 | * This is used in cases where there is a table of data that might have 3 different values in 839 | * the left column, and for each of those 3 values, many entries in the right column. What will happen 840 | * (if the array here returns 'LeftColFieldName') is that any immediately following column that 841 | * has the same value as current is blanked out. 842 | */ 843 | public function getDuplicatedBlankingFields() { 844 | if ($this->ClearColumns && $this->ClearColumns->getValues()) { 845 | $fields = $this->ClearColumns->getValues(); 846 | $ret = array(); 847 | foreach ($fields as $field) { 848 | if (strpos($field, '.')) { 849 | $field = $this->dottedFieldToUnique($field); 850 | } 851 | $ret[] = $field; 852 | } 853 | return $ret; 854 | } 855 | return array(); 856 | } 857 | 858 | /** 859 | * Gets field formatting functions used for applying transformations to values. 860 | * 861 | * The formatters should be a map of field name to callable. The callable 862 | * is passed the original value and current record. 863 | * 864 | * @return array 865 | */ 866 | public function getFieldFormatting() { 867 | $combined_array = array(); 868 | 869 | // make sure we dont try to combine are arrays and have at least 1 element 870 | $keys = $this->FieldFormattingField->getValues(); 871 | $values = $this->FieldFormattingFormatter->getValues(); 872 | 873 | if (is_array($keys) && is_array($values) && count($keys) == count($values) && count($keys) > 0) { 874 | $combined_array = array_combine($keys, $values); 875 | } 876 | 877 | return $combined_array; 878 | } 879 | 880 | /** 881 | * Creates a report in a specified format, returning a string which contains either 882 | * the raw content of the report, or an object that encapsulates the report (eg a PDF). 883 | * 884 | * @param string $format 885 | * @param boolean $store 886 | * Whether to store the created report. 887 | * @param array $parameters 888 | * An array of parameters that will be used as dynamic replacements 889 | */ 890 | public function createReport($format = 'html', $store = false) { 891 | Requirements::clear(); 892 | $convertTo = null; 893 | $renderFormat = $format; 894 | $conversions = $this->config()->conversion_formats; 895 | 896 | if (isset($conversions[$format])) { 897 | $convertTo = 'pdf'; 898 | $renderFormat = $conversions[$format]; 899 | } 900 | 901 | $formatter = $this->getReportFormatter($renderFormat); 902 | 903 | if($formatter) { 904 | $content = $formatter->format(); 905 | } else { 906 | $content = "Formatter for '$renderFormat' not found."; 907 | } 908 | 909 | $classes = array_reverse(ClassInfo::ancestry(get_class($this))); 910 | $templates = array(); 911 | foreach ($classes as $cls) { 912 | if ($cls == 'AdvancedReport') { 913 | // catchall 914 | $templates[] = 'AdvancedReport_' . $renderFormat; 915 | break; 916 | } 917 | $templates[] = $cls . '_' . $renderFormat; 918 | } 919 | 920 | $date = DBField::create_field('SS_Datetime', time()); 921 | $this->Text = nl2br($this->Text); 922 | 923 | $reportData = array('ReportContent' => $content, 'Format' => $format, 'Now' => $date); 924 | $additionalData = $this->additionalReportData(); 925 | $reportData = array_merge($reportData, $additionalData); 926 | 927 | $output = $this->customise($reportData)->renderWith($templates); 928 | if ($output instanceof HTMLText) { 929 | $output = $output->getValue(); 930 | } 931 | 932 | if (!$output) { 933 | // put_contents fails if it's an empty string... 934 | $output = " "; 935 | } 936 | 937 | if (!$convertTo) { 938 | if ($store) { 939 | // stick it in a temp file? 940 | $outputFile = tempnam(TEMP_FOLDER, $format); 941 | if (file_put_contents($outputFile, $output)) { 942 | return new AdvancedReportOutput(null, $outputFile); 943 | } else { 944 | throw new Exception("Failed creating report in $outputFile"); 945 | } 946 | 947 | } else { 948 | return new AdvancedReportOutput($output); 949 | } 950 | } 951 | 952 | // hard coded for now, need proper content transformations.... 953 | switch ($convertTo) { 954 | case 'pdf': { 955 | if ($store) { 956 | $filename = singleton('PdfRenditionService')->render($output); 957 | return new AdvancedReportOutput(null, $filename); 958 | } else { 959 | singleton('PdfRenditionService')->render($output, 'browser'); 960 | return new AdvancedReportOutput(); 961 | } 962 | break; 963 | } 964 | default: { 965 | break; 966 | } 967 | } 968 | } 969 | 970 | /** 971 | * Get an array of additional data to add to a report. 972 | * 973 | * @return array 974 | */ 975 | protected function additionalReportData() { 976 | return array(); 977 | } 978 | 979 | /** 980 | * Generates an actual report file. 981 | * 982 | * @param string $format 983 | */ 984 | public function generateReport($format = 'html') { 985 | $field = strtoupper($format) . 'FileID'; 986 | $storeIn = $this->getReportFolder(); 987 | 988 | // SS hates spaces in here :( 989 | $name = preg_replace('/ +/', '-', trim($this->Title)); 990 | $name = $name . '.' . $format; 991 | $name = FileNameFilter::create()->filter($name); 992 | 993 | $childId = $storeIn->constructChild($name); 994 | $file = DataObject::get_by_id('File', $childId); 995 | 996 | // it's a new file, so trigger the onAfterUpload method for extensions that expect it 997 | if (method_exists($file, 'onAfterUpload')) { 998 | $file->onAfterUpload(); 999 | } 1000 | 1001 | // okay, now we should copy across... right? 1002 | $file->setName($name); 1003 | $file->write(); 1004 | 1005 | // create the raw report file 1006 | $output = $this->createReport($format, true); 1007 | 1008 | if (is_object($output)) { 1009 | if (file_exists($output->filename)) { 1010 | if ($this->config()->store_file_content) { 1011 | $file->Content = base64_encode(file_get_contents($output->filename)); 1012 | $file->write(); 1013 | } else { 1014 | copy($output->filename, $file->getFullPath()); 1015 | } 1016 | } 1017 | } 1018 | 1019 | // make sure to set the appropriate ID 1020 | $this->$field = $file->ID; 1021 | $this->write(); 1022 | } 1023 | 1024 | /** 1025 | * Returns a report formatter instance for an output format. 1026 | * 1027 | * @param string $format 1028 | * @return ReportFormatter 1029 | */ 1030 | public function getReportFormatter($format) { 1031 | $class = ucfirst($format) . 'ReportFormatter'; 1032 | 1033 | if(class_exists($class)) { 1034 | return new $class($this); 1035 | } 1036 | } 1037 | 1038 | /** 1039 | * Gets the report folder used for storing generated reports. 1040 | * 1041 | * @return string 1042 | */ 1043 | protected function getReportFolder() { 1044 | return Folder::find_or_make("advanced-reports/$this->ReportID/$this->ID"); 1045 | } 1046 | 1047 | public function canView($member = null) { 1048 | return Permission::check('CMS_ACCESS_AdvancedReportsAdmin', 'any', $member); 1049 | } 1050 | 1051 | public function canEdit($member = null) { 1052 | return Permission::check('EDIT_ADVANCED_REPORT', 'any', $member); 1053 | } 1054 | 1055 | public function canDelete($member = null) { 1056 | return Permission::check('CMS_ACCESS_AdvancedReportsAdmin', 'any', $member); 1057 | } 1058 | 1059 | public function canCreate($member = null) { 1060 | return Permission::check('CMS_ACCESS_AdvancedReportsAdmin', 'any', $member); 1061 | } 1062 | 1063 | public function canGenerate($member = null) { 1064 | return Permission::check('GENERATE_ADVANCED_REPORT', 'any', $member); 1065 | } 1066 | 1067 | /** 1068 | * @return array 1069 | */ 1070 | public function providePermissions() { 1071 | return array( 1072 | 'EDIT_ADVANCED_REPORT' => array( 1073 | 'name' => _t('AdvancedReport.EDIT', 'Create and edit Advanced Report pages'), 1074 | 'category' => _t('AdvancedReport.ADVANCED_REPORTS_CATEGORY', 'Advanced Reports permissions'), 1075 | 'help' => _t( 1076 | 'AdvancedReport.ADVANCED_REPORTS_EDIT_HELP', 1077 | 'Users with this permission can create new Report Pages from a Report Holder page' 1078 | ), 1079 | 'sort' => 400 1080 | ), 1081 | 'GENERATE_ADVANCED_REPORT' => array( 1082 | 'name' => _t('AdvancedReport.GENERATE', 'Generate an Advanced Report'), 1083 | 'category' => _t('AdvancedReport.ADVANCED_REPORTS_CATEGORY', 'Advanced Reports permissions'), 1084 | 'help' => _t( 1085 | 'AdvancedReport.ADVANCED_REPORTS_GENERATE_HELP', 1086 | 'Users with this permission can generate reports based on ' . 1087 | 'existing report templates via a frontend Report Page' 1088 | ), 1089 | 'sort' => 400 1090 | ), 1091 | ); 1092 | } 1093 | 1094 | public function dbQuote($filter = array(), $join = " AND ") { 1095 | $QUOTE_CHAR = defined('DB::USE_ANSI_SQL') ? '"' : ''; 1096 | 1097 | $string = ''; 1098 | $sep = ''; 1099 | 1100 | foreach ($filter as $field => $value) { 1101 | // first break the field up into its two components 1102 | $operator = ''; 1103 | if (is_string($field)) { 1104 | list($field, $operator) = explode(' ', trim($field)); 1105 | } 1106 | 1107 | if (is_array($value) && $operator == '=') { 1108 | // convert to "IN" 1109 | $operator = 'IN'; 1110 | } 1111 | 1112 | $value = $this->recursiveQuote($value); 1113 | 1114 | // not using quote char if it's already escaped 1115 | if ($field[0] == '"') { 1116 | $QUOTE_CHAR = ''; 1117 | } else { 1118 | $QUOTE_CHAR = defined('DB::USE_ANSI_SQL') ? '"' : ''; 1119 | } 1120 | 1121 | if (strpos($field, '.')) { 1122 | list($tb, $fl) = explode('.', $field); 1123 | $string .= $sep . $QUOTE_CHAR . $tb . $QUOTE_CHAR . '.' . $QUOTE_CHAR . $fl . $QUOTE_CHAR 1124 | . " $operator " . $value; 1125 | } else { 1126 | if (is_numeric($field)) { 1127 | $string .= $sep . $value; 1128 | } else { 1129 | $string .= $sep . $QUOTE_CHAR . $field . $QUOTE_CHAR . " $operator " . $value; 1130 | } 1131 | } 1132 | 1133 | $sep = $join; 1134 | } 1135 | 1136 | return $string; 1137 | } 1138 | 1139 | protected function recursiveQuote($val) { 1140 | if (is_array($val)) { 1141 | $return = array(); 1142 | foreach ($val as $v) { 1143 | $return[] = $this->recursiveQuote($v); 1144 | } 1145 | 1146 | return '('.implode(',', $return).')'; 1147 | } else if (is_null($val)) { 1148 | $val = 'NULL'; 1149 | } else if (is_int($val)) { 1150 | $val = (int) $val; 1151 | } else if (is_double($val)) { 1152 | $val = (double) $val; 1153 | } else if (is_float($val)) { 1154 | $val = (float) $val; 1155 | } else { 1156 | $val = "'" . Convert::raw2sql($val) . "'"; 1157 | } 1158 | 1159 | return $val; 1160 | } 1161 | } 1162 | 1163 | class ConditionFilters { 1164 | 1165 | const ARGUMENT_SEPARATOR = '|'; 1166 | 1167 | protected $possibleParamValues = array(); 1168 | 1169 | public function __construct($possibleValues = array()) { 1170 | $this->possibleParamValues = $possibleValues; 1171 | } 1172 | 1173 | public function strtotimeDateValue($value) { 1174 | $args = $this->getArgs($value); 1175 | if (!isset($args[1])) { 1176 | $args[1] = 'Y-m-d H:i:s'; 1177 | } 1178 | if ($args[1] == 'stamp') { 1179 | return strtotime($args[0]); 1180 | } 1181 | return date($args[1], strtotime($args[0])); 1182 | } 1183 | 1184 | public function paramValue($value, $report) { 1185 | $args = $this->getArgs($value); 1186 | $params = $report->ReportParams; 1187 | if ($params) { 1188 | $params = $params->getValues(); 1189 | } 1190 | 1191 | if (isset($_GET[$args[0]])) { 1192 | return $_GET[$args[0]]; 1193 | } 1194 | 1195 | if ($params && isset($args[0]) && isset($params[$args[0]])) { 1196 | return $report->applyFiltersToValue($params[$args[0]]); 1197 | } 1198 | 1199 | 1200 | return ''; 1201 | } 1202 | 1203 | protected function getArgs($str) { 1204 | return explode(self::ARGUMENT_SEPARATOR, $str); 1205 | } 1206 | } 1207 | 1208 | /** 1209 | * Wrapper around a report output that might be raw content or a filename to the 1210 | * report 1211 | * 1212 | */ 1213 | class AdvancedReportOutput { 1214 | public $filename; 1215 | public $content; 1216 | 1217 | public function __construct($content = null, $filename = null) { 1218 | $this->filename = $filename; 1219 | $this->content = $content; 1220 | } 1221 | } 1222 | -------------------------------------------------------------------------------- /code/dataobjects/CombinedReport.php: -------------------------------------------------------------------------------- 1 | 7 | * @license BSD License http://www.silverstripe.org/bsd-license 8 | */ 9 | class CombinedReport extends AdvancedReport { 10 | private static $db = array( 11 | ); 12 | 13 | private static $has_many = array( 14 | 'ChildReports' => 'RelatedReport', 15 | ); 16 | 17 | public function getCMSFields($params = null) { 18 | $fields = parent::getCMSFields($params); // new FieldSet(); 19 | 20 | // tabbed or untabbed 21 | // $fields->push(new TabSet("Root", $mainTab = new Tab("Main"))); 22 | // $mainTab->setTitle(_t('SiteTree.TABMAIN', "Main")); 23 | 24 | $fields->removeByName('Settings'); 25 | 26 | $fields->addFieldToTab('Root.Main', new TextField('Title')); 27 | 28 | $fields->addFieldToTab('Root.Main', new TextareaField('Description')); 29 | 30 | if ($this->ID) { 31 | $config = new GridFieldConfig_RecordEditor(); 32 | $grid = new GridField('ChildReports', 'Child reports', $this->ChildReports(), $config); 33 | 34 | $fields->addFieldToTab('Root.Main', $grid); 35 | 36 | if (class_exists('GridFieldOrderableRows')) { 37 | $config->addComponent(new GridFieldOrderableRows()); 38 | } 39 | } else { 40 | $fields->addFieldToTab( 41 | 'Root.Main', 42 | new LiteralField('Notice', 'Please save before adding related reports') 43 | ); 44 | } 45 | 46 | return $fields; 47 | } 48 | 49 | /** 50 | * Prepares this combined report 51 | * 52 | * Functions the same as the parent class, but we need to 53 | * clone the Related reports too 54 | * 55 | * @return CombinedReport 56 | */ 57 | public function prepareAndGenerate() { 58 | $report = $this->duplicate(false); 59 | $report->ReportID = $this->ID; 60 | $report->Created = SS_Datetime::now(); 61 | $report->LastEdited = SS_Datetime::now(); 62 | $report->Title = $this->GeneratedReportTitle; 63 | $report->write(); 64 | 65 | $toClone = $this->ChildReports(); 66 | if ($toClone) { 67 | foreach ($toClone as $child) { 68 | $clonedChild = $child->duplicate(false); 69 | $clonedChild->Created = SS_Datetime::now(); 70 | $clonedChild->LastEdited = SS_Datetime::now(); 71 | $clonedChild->CombinedReportID = $report->ID; 72 | $clonedChild->write(); 73 | } 74 | } 75 | 76 | $report->generateReport('html'); 77 | $report->generateReport('csv'); 78 | 79 | if (class_exists('PdfRenditionService')) { 80 | $report->generateReport('pdf'); 81 | } 82 | 83 | return $report; 84 | } 85 | 86 | /** 87 | * Creates a report in a specified format, returning a string which contains either 88 | * the raw content of the report, or an object that encapsulates the report (eg a PDF). 89 | * 90 | * @param string $format 91 | * @param boolean $store 92 | * Whether to store the created report. 93 | * @param array $parameters 94 | * An array of parameters that will be used as dynamic replacements 95 | */ 96 | public function createReport($format = 'html', $store = false) { 97 | Requirements::clear(); 98 | $convertTo = null; 99 | $renderFormat = $format; 100 | if (isset(AdvancedReport::config()->conversion_formats[$format])) { 101 | $convertTo = 'pdf'; 102 | $renderFormat = AdvancedReport::config()->conversion_formats[$format]; 103 | } 104 | 105 | $reports = $this->ChildReports(); 106 | if (!$reports->count()) { 107 | return _t('AdvancedReport.NO_REPORTS_SELECTED', 'No reports selected'); 108 | } 109 | 110 | $contents = array(); 111 | foreach ($reports as $linkedReport) { 112 | if (!$linkedReport->ReportID) { 113 | continue; 114 | } 115 | 116 | $params = $linkedReport->Parameters; 117 | 118 | $report = $linkedReport->Report(); 119 | 120 | if ($params) { 121 | $params = $params->getValues(); 122 | $baseParams = $report->ReportParams->getValues(); 123 | $params = array_merge($baseParams, $params); 124 | $report->ReportParams = $params; 125 | } 126 | 127 | $formatter = $report->getReportFormatter($renderFormat); 128 | 129 | if($formatter) { 130 | $contents[] = $report->customise(array('ReportContent' => $formatter->format(), 'Title' => $linkedReport->Title)); 131 | } else { 132 | $contents[] = new ArrayData(array('ReportContent' => "Formatter for '$renderFormat' not found.")); 133 | } 134 | } 135 | 136 | $templates = array(get_class($this) . '_' . $renderFormat); 137 | 138 | $date = DBField::create_field('SS_Datetime', time()); 139 | $this->Description = nl2br($this->Description); 140 | 141 | $reportData = array('Reports' => new ArrayList($contents), 'Format' => $format, 'Now' => $date); 142 | 143 | $output = $this->customise($reportData)->renderWith($templates); 144 | 145 | if (!$output) { 146 | // put_contents fails if it's an empty string... 147 | $output = " "; 148 | } 149 | 150 | if (!$convertTo) { 151 | if ($store) { 152 | // stick it in a temp file? 153 | $outputFile = tempnam(TEMP_FOLDER, $format); 154 | if (file_put_contents($outputFile, $output)) { 155 | return new AdvancedReportOutput(null, $outputFile); 156 | } else { 157 | throw new Exception("Failed creating report in $outputFile"); 158 | } 159 | 160 | } else { 161 | return new AdvancedReportOutput($output); 162 | } 163 | } 164 | 165 | // hard coded for now, need proper content transformations.... 166 | switch ($convertTo) { 167 | case 'pdf': { 168 | if ($store) { 169 | $filename = singleton('PdfRenditionService')->render($output); 170 | return new AdvancedReportOutput(null, $filename); 171 | } else { 172 | singleton('PdfRenditionService')->render($output, 'browser'); 173 | return new AdvancedReportOutput(); 174 | } 175 | break; 176 | } 177 | default: { 178 | break; 179 | } 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /code/dataobjects/DataObjectReport.php: -------------------------------------------------------------------------------- 1 | 'Varchar(64)', 14 | ); 15 | 16 | public function getReportName() { 17 | return "Generic Report"; 18 | } 19 | 20 | protected function getReportableFields() { 21 | $fields = array( 22 | 'ID' => 'ID', 23 | 'Created' => 'Created', 24 | 'LastEdited' => 'LastEdited', 25 | ); 26 | 27 | if($this->ReportOn) { 28 | $config = Config::inst()->forClass($this->ReportOn); 29 | 30 | $db = $config->get('db'); 31 | $hasOne = $config->get('has_one'); 32 | 33 | if($db) { 34 | $fields = array_merge($fields, $db); 35 | } 36 | 37 | if($hasOne) foreach(array_keys($hasOne) as $name) { 38 | $fields[$name . 'ID'] = true; 39 | } 40 | 41 | $fields = array_combine(array_keys($fields), array_keys($fields)); 42 | } 43 | 44 | ksort($fields); 45 | 46 | return $fields; 47 | } 48 | 49 | public function getSettingsFields() { 50 | $fields = parent::getSettingsFields(); 51 | $types = ClassInfo::subclassesFor('DataObject'); 52 | 53 | array_shift($types); 54 | ksort($types); 55 | 56 | $fields->insertAfter( 57 | new DropdownField('ReportOn', _t('AdvancedReport.REPORT_ON', 'Report on'), $types), 58 | 'Title' 59 | ); 60 | 61 | return $fields; 62 | } 63 | 64 | /** 65 | * @return DataList 66 | */ 67 | public function getDataObjects() { 68 | return DataList::create($this->ReportOn) 69 | ->filter($this->getFilter()) 70 | ->sort($this->getSort()); 71 | } 72 | 73 | /** 74 | * Gets the filter we need for the report 75 | * 76 | * @param $agreementFilter 77 | * @return string 78 | */ 79 | protected function getFilter() { 80 | $conditions = $this->getConditions(); 81 | return $conditions; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /code/dataobjects/FreeformReport.php: -------------------------------------------------------------------------------- 1 | 7 | * @license BSD License http://www.silverstripe.org/bsd-license 8 | */ 9 | class FreeformReport extends AdvancedReport { 10 | 11 | private static $allow_grouping = true; 12 | 13 | private static $db = array( 14 | 'DataTypes' => 'MultiValueField', 15 | ); 16 | 17 | private static $allowed_types = array('Page' => 'Page', 'Member' => 'Member'); 18 | 19 | /** 20 | * Which types have been currently selected? 21 | * @var type 22 | */ 23 | protected $selectedTypes = array(); 24 | 25 | /** 26 | * Keeps track of which tables are mapped via what fields 27 | * eg Lga.Vips => has_one 28 | * @var type 29 | */ 30 | protected $componentTypeMap = array(); 31 | 32 | 33 | public function getSettingsFields() { 34 | 35 | $dataTypes = $this->getAvailableTypes(); 36 | $reportable = $this->getReportableFields(); 37 | 38 | $converted = array(); 39 | 40 | foreach($reportable as $k => $v) { 41 | $converted[$this->dottedFieldToUnique($k)] = $v; 42 | } 43 | 44 | $dataTypes = array_merge(array('' => ''), $dataTypes); 45 | $types = new MultiValueDropdownField('DataTypes', _t('AdvancedReport.DATA_TYPES', 'Data types'), $dataTypes); 46 | 47 | $fieldsGroup = new FieldGroup( 48 | 'Fields', 49 | $reportFieldsSelect = new MultiValueDropdownField( 50 | 'ReportFields', 51 | _t('AdvancedReport.REPORT_FIELDS', 'Report Fields'), 52 | $reportable 53 | ) 54 | ); 55 | 56 | $fieldsGroup->push(new MultiValueTextField('ReportHeaders', _t('AdvancedReport.HEADERS', 'Header labels'))); 57 | 58 | $fieldsGroup->addExtraClass('reportMultiField'); 59 | $reportFieldsSelect->addExtraClass('reportFieldsSelection'); 60 | 61 | $fieldsGroup->setName('FieldsGroup'); 62 | $fieldsGroup->addExtraClass('advanced-report-fields dropdown'); 63 | 64 | $conditions = new FieldGroup('Conditions', 65 | new MultiValueDropdownField( 66 | 'ConditionFields', 67 | _t('AdvancedReport.CONDITION_FIELDS', 'Condition Fields'), 68 | $reportable 69 | ), 70 | new MultiValueDropdownField( 71 | 'ConditionOps', 72 | _t('AdvancedReport.CONDITION_OPERATIONS', 'Op'), 73 | $this->config()->allowed_conditions 74 | ), 75 | new MultiValueTextField('ConditionValues', _t('AdvancedReport.CONDITION_VALUES', 'Value')) 76 | ); 77 | 78 | $conditions->setName('ConditionsGroup'); 79 | $conditions->addExtraClass('dropdown'); 80 | 81 | // define the group for the sort field 82 | $sortGroup = new FieldGroup( 83 | 'Sort', 84 | new MultiValueDropdownField( 85 | 'SortBy', 86 | _t('AdvancedReport.SORTED_BY', 'Sorted By'), 87 | $reportable 88 | ), 89 | new MultiValueDropdownField( 90 | 'SortDir', 91 | _t('AdvancedReport.SORT_DIRECTION', 'Sort Direction'), 92 | array( 93 | 'ASC' => _t('AdvancedReport.ASC', 'Ascending'), 94 | 'DESC' => _t('AdvancedReport.DESC', 'Descending') 95 | ) 96 | ) 97 | ); 98 | $sortGroup->setName('SortGroup'); 99 | $sortGroup->addExtraClass('dropdown'); 100 | 101 | 102 | // build a list of the formatters 103 | $formatters = ClassInfo::implementorsOf('ReportFieldFormatter'); 104 | $fmtrs = array(); 105 | foreach ($formatters as $formatterClass) { 106 | $formatter = new $formatterClass(); 107 | $fmtrs[$formatterClass] = $formatter->label(); 108 | } 109 | 110 | // define the group for the custom field formatters 111 | $fieldFormattingGroup = new FieldGroup( 112 | _t('AdvancedReport.FORMAT_FIELDS', 'Custom field formatting'), 113 | new MultiValueDropdownField( 114 | 'FieldFormattingField', 115 | _t('AdvancedReport.FIELDFORMATTING', 'Field'), 116 | $converted 117 | ), 118 | new MultiValueDropdownField( 119 | 'FieldFormattingFormatter', 120 | _t('AdvancedReport.FIELDFORMATTINGFORMATTER', 'Formatter'), 121 | $fmtrs 122 | ) 123 | ); 124 | $fieldFormattingGroup->setName('FieldFormattingGroup'); 125 | $fieldFormattingGroup->addExtraClass('dropdown'); 126 | 127 | // assemble the fieldlist 128 | $fields = new FieldList( 129 | new TextField('Title', _t('AdvancedReport.TITLE', 'Title')), 130 | new TextareaField( 131 | 'Description', 132 | _t('AdvancedReport.DESCRIPTION', 'Description') 133 | ), 134 | $types, 135 | $fieldsGroup, 136 | $conditions, 137 | new KeyValueField( 138 | 'ReportParams', 139 | _t('AdvancedReport.REPORT_PARAMETERS', 'Default report parameters') 140 | ), 141 | $sortGroup, 142 | new MultiValueDropdownField( 143 | 'NumericSort', 144 | _t('AdvancedReport.SORT_NUMERICALLY', 'Sort these fields numerically'), 145 | $reportable 146 | ), 147 | DropdownField::create('PaginateBy') 148 | ->setTitle(_t('AdvancedReport.PAGINATE_BY', 'Paginate By')) 149 | ->setSource($reportable) 150 | ->setHasEmptyDefault(true), 151 | TextField::create('PageHeader') 152 | ->setTitle(_t('AdvancedReport.HEADER_TEXT', 'Header text')) 153 | ->setDescription(_t('AdvancedReport.USE_NAME_FOR_PAGE_NAME', 'use $name for the page name')) 154 | ->setValue('$name'), 155 | new MultiValueDropdownField( 156 | 'AddInRows', 157 | _t('AdvancedReport.ADD_IN_ROWS', 'Add these columns for each row'), 158 | $converted 159 | ), 160 | new MultiValueDropdownField( 161 | 'AddCols', 162 | _t('AdvancedReport.ADD_IN_ROWS', 'Provide totals for these columns'), 163 | $converted 164 | ), 165 | $fieldFormattingGroup, 166 | new MultiValueDropdownField( 167 | 'ClearColumns', 168 | _t('AdvancedReport.CLEARED_COLS', '"Cleared" columns'), 169 | $converted 170 | ) 171 | ); 172 | 173 | if ($this->config()->allow_grouping) { 174 | // GroupBy 175 | $groupingGroup = new FieldGroup( 176 | 'Grouping', 177 | new MultiValueDropdownField( 178 | 'GroupBy', 179 | _t('AdvancedReport.GROUPBY_FIELDS', 'Group by fields'), 180 | $reportable 181 | ), 182 | new MultiValueDropdownField( 183 | 'SumFields', 184 | _t('AdvancedReport.SUM_FIELDS', 'SUM fields'), 185 | $reportable 186 | ) 187 | ); 188 | $groupingGroup->addExtraClass('dropdown'); 189 | $fields->insertAfter($groupingGroup, 'ConditionsGroup'); 190 | } 191 | 192 | if($this->hasMethod('updateReportFields')) { 193 | Deprecation::notice( 194 | '3.0', 195 | 'The updateReportFields method is deprecated, instead overload getSettingsFields' 196 | ); 197 | 198 | $this->updateReportFields($fields); 199 | } 200 | 201 | $this->extend('updateSettingsFields', $fields); 202 | return $fields; 203 | } 204 | 205 | protected function allowedTypes() { 206 | return $this->config()->allowed_types; 207 | } 208 | 209 | protected function getAvailableTypes() { 210 | $types = array(); // self::$allowed_types; 211 | 212 | $hasRoot = false; 213 | $dataTypes = $this->DataTypes->getValues(); 214 | $allowedTypes = $this->allowedTypes(); 215 | 216 | if ($dataTypes) { 217 | if (is_array($dataTypes)) { 218 | foreach ($dataTypes as $selected) { 219 | if (!strlen(trim($selected))) { 220 | continue; 221 | } 222 | 223 | if (!class_exists($selected)) { 224 | continue; 225 | } 226 | 227 | // make sure we're only processing top level types 228 | if (!isset($allowedTypes[$selected])) { 229 | continue; 230 | } 231 | 232 | if (isset($allowedTypes[$selected])) { 233 | if ($hasRoot) { 234 | continue; 235 | } 236 | $hasRoot = true; 237 | $types[$selected] = $allowedTypes[$selected]; 238 | } 239 | 240 | // get all has_many, has_one, many_many field options 241 | $has_ones = Config::inst()->get($selected, 'has_one'); 242 | if ($has_ones && count($has_ones)) { 243 | foreach ($has_ones as $name => $type) { 244 | $types["$selected.$name"] = "$selected.$name"; 245 | $this->componentTypeMap["$selected.$name"] = 'has_one'; 246 | } 247 | } 248 | 249 | $has_manies = Config::inst()->get($selected, 'has_many'); 250 | if ($has_manies && count($has_manies)) { 251 | foreach ($has_manies as $name => $type) { 252 | $types["$selected.$name"] = "$selected.$name"; 253 | $this->componentTypeMap["$selected.$name"] = 'has_many'; 254 | } 255 | } 256 | 257 | $many_many = Config::inst()->get($selected, 'many_many'); 258 | if ($many_many && count($many_many)) { 259 | foreach ($many_many as $name => $type) { 260 | $types["$selected.$name"] = "$selected.$name"; 261 | $this->componentTypeMap["$selected.$name"] = 'many_many'; 262 | } 263 | } 264 | 265 | $many_many = Config::inst()->get($selected, 'belongs_many_many'); 266 | if ($many_many && count($many_many)) { 267 | foreach ($many_many as $name => $type) { 268 | $types["$selected.$name"] = "$selected.$name"; 269 | $this->componentTypeMap["$selected.$name"] = 'belongs_many_many'; 270 | } 271 | } 272 | } 273 | } 274 | } 275 | 276 | if (count($types) == 0) { 277 | $types = $allowedTypes; 278 | } 279 | 280 | return $types; 281 | } 282 | 283 | 284 | /** 285 | * Gets an array of field names that can be used in this report 286 | * 287 | * Override to specify your own values. 288 | */ 289 | protected function getReportableFields() { 290 | $tables = $this->getQueryTables(); 291 | 292 | $fields = array(); 293 | 294 | // now figure out which fields we can now select based on the tables being included; 295 | foreach ($tables as $type => $table) { 296 | // do we look up ALL fields from parent types too? Only works for 297 | // the _base_ class, NOT joined class inherited fields 298 | $includeInheritedDbFields = Config::INHERITED; 299 | $alias = ''; 300 | $fieldPrefix = $type . '.'; 301 | if (strpos($type, '.')) { 302 | // @TODO - this should be changed so that the fields added further below 303 | // use the full inherited list of fields available on this remote type 304 | $includeInheritedDbFields = Config::UNINHERITED; 305 | list($type, $rel) = explode('.', $type); 306 | $alias = 'tbl_' . $type . '_' . $rel .'.'; 307 | $type = $this->getTypeRelationshipClass($type, $rel); 308 | $fieldPrefix = $rel . '.'; 309 | } 310 | 311 | $fields["{$alias}ID"] = $fieldPrefix . 'ID'; 312 | $fields["{$alias}Created"] = $fieldPrefix . 'Created'; 313 | $fields["{$alias}LastEdited"] = $fieldPrefix . 'LastEdited'; 314 | 315 | $dbDefined = Config::inst()->get($type, 'db', $includeInheritedDbFields); 316 | if (is_array($dbDefined)) { 317 | foreach ($dbDefined as $field => $fieldtype) { 318 | if($fieldtype == 'MultiValueField') { 319 | $fields["$alias{$field}Value"] = $fieldPrefix . $field; 320 | } else { 321 | $fields["$alias$field"] = $fieldPrefix . $field; 322 | } 323 | } 324 | } 325 | 326 | $dbHasOnes = Config::inst()->get($type, 'has_one', $includeInheritedDbFields); 327 | if (is_array($dbHasOnes)) { 328 | foreach ($dbHasOnes as $name => $relatedType) { 329 | $field = $name . "ID"; 330 | $fields["$alias$field"] = $fieldPrefix . $field; 331 | } 332 | } 333 | 334 | if (class_exists($type)) { 335 | $sng = singleton($type); 336 | if (method_exists($sng, 'getAdvancedReportableFields')) { 337 | $defined = $sng->getAdvancedReportableFields($alias); 338 | foreach ($defined as $name => $source) { 339 | $fields["$alias$name"] = $fieldPrefix . $name; 340 | } 341 | } 342 | } 343 | } 344 | 345 | return $fields; 346 | } 347 | 348 | /** 349 | * Based on the user's selection, get all the query tables that will be included 350 | */ 351 | protected function getQueryTables() { 352 | $allowedTypes = $this->getAvailableTypes(); 353 | 354 | $tables = array(); 355 | 356 | $dataTypes = $this->DataTypes->getValues(); 357 | if (is_array($dataTypes)) { 358 | foreach ($dataTypes as $type) { 359 | if (!strlen($type)) { 360 | continue; 361 | } 362 | if (!isset($allowedTypes[$type])) { 363 | continue; 364 | } 365 | if (strpos($type, '.')) { 366 | list($type, $rel) = explode('.', $type); 367 | $actualType = $this->getTypeRelationshipClass($type, $rel); 368 | $tables["$type.$rel"] = "$actualType tbl_{$type}_$rel"; 369 | } else { 370 | $tables[$type] = $type; 371 | } 372 | } 373 | } 374 | 375 | return $tables; 376 | } 377 | 378 | /** 379 | * Get the type of a relationship for a given type and relationship name 380 | * 381 | * @param string $type 382 | * @param string $relName 383 | */ 384 | protected function getTypeRelationshipClass($type, $relName) { 385 | foreach (array('has_one', 'has_many', 'many_many', 'belongs_many_many') as $rel) { 386 | $options = Config::inst()->get($type, $rel); 387 | if ($options && isset($options[$relName])) { 388 | return $options[$relName]; 389 | } 390 | } 391 | } 392 | 393 | public function getDataObjects() { 394 | Versioned::reading_stage('Stage'); 395 | $rows = array(); 396 | $tables = $this->getQueryTables(); 397 | 398 | $allFields = $this->getReportableFields(); 399 | 400 | $selectedFields = $this->ReportFields->getValues(); 401 | 402 | $fields = array(); 403 | 404 | $sum = $this->SumFields->getValues(); 405 | $sum = $sum ? $sum : array(); 406 | 407 | if ($selectedFields) { 408 | foreach ($selectedFields as $field) { 409 | if (!isset($allFields[$field])) { 410 | continue; 411 | } 412 | $as = $this->dottedFieldToUnique($allFields[$field]); 413 | $fields[$as] = $field; 414 | } 415 | } 416 | 417 | $baseTable = null; 418 | 419 | // stores the relationship name-to-aliasname mapping 420 | $relatedTables = array(); 421 | 422 | // stores the tbl_{one}_{two} alias name-to-class-type mapping 423 | $aliasToDataType = array(); 424 | 425 | foreach ($tables as $typeName => $alias) { 426 | if (strpos($typeName, '.')) { 427 | $relatedTables[$typeName] = $alias; 428 | } else { 429 | if (!$baseTable) { 430 | $baseTable = $typeName; 431 | } 432 | } 433 | } 434 | 435 | if (!$baseTable) { 436 | throw new Exception("All freeform reports must have a base data type selected"); 437 | } 438 | 439 | $multiValue = array(); 440 | 441 | // go through and capture all the multivalue fields 442 | // at the same time, remap Type.Field structures to 443 | // TableName.Field 444 | $remappedFields = array(); 445 | $simpleFields = array(); 446 | foreach ($fields as $alias => $name) { 447 | $class = ''; 448 | if (strpos($name, '.')) { 449 | list($class, $field) = explode('.', $name); 450 | } else { 451 | $class = $baseTable; 452 | $field = $name; 453 | } 454 | $typeFields = array(); 455 | 456 | if (class_exists($class)) { 457 | $instance = singleton($class); 458 | $typeFields = method_exists($instance, 'getAdvancedReportableFields') ? $instance->getAdvancedReportableFields() : array(); 459 | } else if ($dataType = $this->fieldAliasToDataType($class)) { 460 | $instance = singleton($dataType); 461 | $typeFields = method_exists($instance, 'getAdvancedReportableFields') ? $instance->getAdvancedReportableFields($class.'.') : array(); 462 | } 463 | 464 | $fieldAlias = ''; 465 | 466 | // if the name is prefixed, we need to figure out what the actual $class is from the 467 | // remote join 468 | if (strpos($name, 'tbl_') === 0) { 469 | $fieldAlias = $this->dottedFieldToUnique($name); 470 | $selectField = '"' . Convert::raw2sql($class) . '"."' . Convert::raw2sql($field) . '"'; 471 | if (isset($typeFields[$field])) { 472 | $selectField = $typeFields[$field]; 473 | } 474 | $remappedFields[$fieldAlias] = $selectField; 475 | 476 | if (in_array($name, $sum)) { 477 | $remappedFields[$fieldAlias] = 'SUM(' . $remappedFields[$fieldAlias] .')'; 478 | } 479 | 480 | } else { 481 | if (isset($typeFields[$name])) { 482 | $remappedFields[$name] = $typeFields[$name]; 483 | } else { 484 | 485 | if (in_array($name, $sum)) { 486 | $remappedFields[$field] = 'SUM(' . $field .')'; 487 | } else { 488 | // just store it as is 489 | $simpleFields[$alias] = $field; 490 | } 491 | } 492 | } 493 | 494 | $field = preg_replace('/Value$/', '', $field); 495 | $db = Config::inst()->get($class, 'db'); 496 | 497 | if (isset($db[$field]) && $db[$field] == 'MultiValueField') { 498 | $multiValue[] = $alias; 499 | } 500 | } 501 | 502 | $dataQuery = new DataQuery($baseTable); 503 | // converts all the fields being queried into the appropriate 504 | // tables for querying. 505 | $query = $dataQuery->getFinalisedQuery(); 506 | 507 | 508 | $baseFields = $query->getSelect(); 509 | // we _only_ want the fields the user selected, so clear out and add those that are fine 510 | $query->setSelect(array()); 511 | 512 | foreach ($simpleFields as $alias => $name) { 513 | if (isset($baseFields[$name])) { 514 | $query->selectField($baseFields[$name]); 515 | } 516 | } 517 | 518 | // explicit fields that we want to query against, that come from joins. 519 | // we need to do it this way to ensure that a) the field names in the results match up to 520 | // the header labels specified and b) because dataQuery by default doesn't return fields from 521 | // joined tables, it merely allows for fields from the base dataClass 522 | foreach ($remappedFields as $alias => $name) { 523 | $query->selectField($name, $alias); 524 | } 525 | 526 | // and here's where those joins are added. This is somewhat copied from dataQuery, 527 | // but modified so that our table aliases are used properly to avoid the bug described at 528 | // https://github.com/silverstripe/silverstripe-framework/issues/3518 529 | // it also ensures the alias names are in a format that our header assignment and field retrieval works as 530 | // expected 531 | foreach (array_keys($relatedTables) as $relation) { 532 | $this->applyRelation($baseTable, $query, $relation); 533 | } 534 | 535 | $sort = $this->getSort(); 536 | if ($sort !== 'ID ASC' && strpos($sort, ' ')) { 537 | $sortOpts = explode(",", $sort); 538 | $sortClauses = array(); 539 | foreach ($sortOpts as $sort) { 540 | $sort = trim($sort); 541 | list($field, $dir) = explode(" ", $sort); 542 | if (isset($baseFields[$field])) { 543 | $field = $baseFields[$field]; 544 | } 545 | $sortClauses[] = "$field $dir"; 546 | } 547 | $query->setOrderBy($sortClauses); 548 | } 549 | 550 | $filter = $this->getConditions(); 551 | $where = $this->getWhereClause($filter, $baseTable); 552 | $query->setWhere($where); 553 | 554 | $groupBy = $this->GroupBy->getValues(); 555 | if (count($groupBy)) { 556 | 557 | // add in all the selected fields also, as mysql 5.7.5 is strict about this 558 | // because of ONLY_FULL_GROUP_BY 559 | $allSelected = $query->getSelect(); 560 | foreach ($allSelected as $alias => $fieldExpr) { 561 | if (!preg_match('{\w\(.+?\)}', $fieldExpr)) { 562 | $groupBy[] = $alias; 563 | } 564 | } 565 | 566 | $groupBy = array_unique($groupBy); 567 | 568 | $query->setGroupBy($groupBy); 569 | 570 | } 571 | 572 | $sql = $query->sql(); 573 | 574 | $out = $query->execute(); 575 | 576 | $rows = array(); 577 | 578 | $headers = $this->getHeaders(); 579 | foreach ($out as $row) { 580 | foreach ($multiValue as $field) { 581 | $row[$field] = implode("\n", (array) unserialize($row[$field])); 582 | } 583 | 584 | $rows[] = $row; 585 | } 586 | 587 | return ArrayList::create($rows); 588 | } 589 | 590 | /** 591 | * Traverse the relationship fields, and add the table 592 | * mappings to the query object state. This has to be called 593 | * in any overloaded {@link SearchFilter->apply()} methods manually. 594 | * 595 | * @param string|array $relation The array/dot-syntax relation to follow 596 | * @return The model class of the related item 597 | */ 598 | protected function applyRelation($modelClass, SQLQuery $query, $relation) { 599 | // NO-OP 600 | if(!$relation) return; 601 | 602 | $alias = 'tbl_' . $this->dottedFieldToUnique($relation); 603 | 604 | if(is_string($relation)) { 605 | $relation = explode(".", $relation); 606 | } 607 | 608 | foreach($relation as $rel) { 609 | $model = singleton($modelClass); 610 | if ($component = $model->has_one($rel)) { 611 | if(!$query->isJoinedTo($alias)) { 612 | $has_one = array_flip($model->has_one()); 613 | $foreignKey = $has_one[$component]; 614 | $realModelClass = ClassInfo::table_for_object_field($modelClass, "{$foreignKey}ID"); 615 | $query->addLeftJoin($component, 616 | "\"$alias\".\"ID\" = \"{$realModelClass}\".\"{$foreignKey}ID\"", $alias); 617 | 618 | /** 619 | * add join clause to the component's ancestry classes so that the search filter could search on 620 | * its ancestor fields. 621 | */ 622 | $ancestry = ClassInfo::ancestry($component, true); 623 | if(!empty($ancestry)) { 624 | $ancestry = array_reverse($ancestry); 625 | foreach($ancestry as $ancestor) { 626 | if($ancestor != $component) { 627 | $query->addInnerJoin($ancestor, "\"$alias\".\"ID\" = \"$ancestor\".\"ID\""); 628 | } 629 | } 630 | } 631 | } 632 | $modelClass = $component; 633 | 634 | } elseif ($component = $model->has_many($rel)) { 635 | if(!$query->isJoinedTo($alias)) { 636 | $ancestry = $model->getClassAncestry(); 637 | $foreignKey = $model->getRemoteJoinField($rel); 638 | $query->addLeftJoin($component, 639 | "\"$alias\".\"{$foreignKey}\" = \"{$ancestry[0]}\".\"ID\"", $alias); 640 | /** 641 | * add join clause to the component's ancestry classes so that the search filter could search on 642 | * its ancestor fields. 643 | */ 644 | $ancestry = ClassInfo::ancestry($component, true); 645 | if(!empty($ancestry)) { 646 | $ancestry = array_reverse($ancestry); 647 | foreach($ancestry as $ancestor) { 648 | if($ancestor != $component) { 649 | $query->addInnerJoin($ancestor, "\"$alias\".\"ID\" = \"$ancestor\".\"ID\""); 650 | } 651 | } 652 | } 653 | } 654 | $modelClass = $component; 655 | 656 | } elseif ($component = $model->many_many($rel)) { 657 | list($parentClass, $componentClass, $parentField, $componentField, $relationTable) = $component; 658 | $parentBaseClass = ClassInfo::baseDataClass($parentClass); 659 | $componentBaseClass = ClassInfo::baseDataClass($componentClass); 660 | $query->addInnerJoin($relationTable, 661 | "\"$relationTable\".\"$parentField\" = \"$parentBaseClass\".\"ID\""); 662 | $query->addLeftJoin($componentBaseClass, 663 | "\"$relationTable\".\"$componentField\" = \"$alias\".\"ID\"", $alias); 664 | if(ClassInfo::hasTable($componentClass)) { 665 | $query->addLeftJoin($componentClass, 666 | "\"$relationTable\".\"$componentField\" = \"$alias\".\"ID\"", $alias); 667 | } 668 | $modelClass = $componentClass; 669 | 670 | } 671 | } 672 | 673 | return $modelClass; 674 | } 675 | 676 | protected function fieldAliasToDataType($alias) { 677 | $tables = $this->getQueryTables(); 678 | 679 | foreach ($tables as $table => $aliasing) { 680 | $bits = explode(' ', $aliasing); 681 | if (count($bits) == 2) { 682 | if ($bits[1] == $alias) { 683 | return $bits[0]; 684 | } 685 | } 686 | } 687 | } 688 | } 689 | -------------------------------------------------------------------------------- /code/dataobjects/RelatedReport.php: -------------------------------------------------------------------------------- 1 | 7 | * @license BSD License http://www.silverstripe.org/bsd-license 8 | */ 9 | class RelatedReport extends DataObject { 10 | private static $db = array( 11 | 'Title' => 'Varchar', 12 | 'Parameters' => 'MultiValueField', 13 | 'Sort' => 'Int', 14 | ); 15 | 16 | private static $has_one = array( 17 | 'CombinedReport' => 'CombinedReport', 18 | 'Report' => 'AdvancedReport', 19 | ); 20 | 21 | private static $summary_fields = array( 22 | 'Report.Title', 23 | 'Title', 24 | ); 25 | 26 | private static $default_sort = 'Sort ASC'; 27 | 28 | public function getCMSFields($params = null) { 29 | $fields = new FieldList(); 30 | 31 | // tabbed or untabbed 32 | $fields->push(new TabSet("Root", $mainTab = new Tab("Main"))); 33 | $mainTab->setTitle(_t('SiteTree.TABMAIN', "Main")); 34 | 35 | $reports = array(); 36 | $reportObjs = AdvancedReport::get()->filter(array('ReportID' => 0)); 37 | if ($reportObjs && $reportObjs->count()) { 38 | foreach ($reportObjs as $obj) { 39 | if ($obj instanceof CombinedReport) { 40 | continue; 41 | } 42 | $reports[$obj->ID] = $obj->Title . '(' . $obj->ClassName .')'; 43 | } 44 | } 45 | 46 | $fields->addFieldsToTab('Root.Main', array( 47 | new DropdownField('ReportID', 'Related report', $reports), 48 | new TextField('Title'), 49 | new KeyValueField('Parameters', 'Parameters to pass to the report'), 50 | new NumericField('Sort'), 51 | )); 52 | 53 | return $fields; 54 | } 55 | 56 | /** 57 | * @param Member $member 58 | * @return boolean 59 | */ 60 | public function canCreate($member = null) { 61 | if (!$member) $member = Member::currentUser(); 62 | 63 | return false 64 | || Permission::check('ADMIN', 'any', $member) 65 | || Permission::check('CMS_ACCESS_AdvancedReportsAdmin', 'any', $member); 66 | } 67 | 68 | /** 69 | * @param Member $member 70 | * @return boolean 71 | */ 72 | public function canView($member = null) { 73 | return $this->CombinedReport()->canView($member); 74 | } 75 | 76 | /** 77 | * @param Member $member 78 | * @return boolean 79 | */ 80 | public function canEdit($member = null) { 81 | return $this->CombinedReport()->canEdit($member); 82 | } 83 | 84 | /** 85 | * @param Member $member 86 | * @return boolean 87 | */ 88 | public function canDelete($member = null) { 89 | return $this->CombinedReport()->canDelete($member); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /code/extensions/ScheduledAdvancedReportExtension.php: -------------------------------------------------------------------------------- 1 | 'Boolean', 11 | 'ScheduledTitle' => 'Varchar(255)', 12 | 'FirstScheduled' => 'SS_Datetime', 13 | 'ScheduleEvery' => 'Enum(array("", "Hour", "Day", "Week", "Fortnight", "Month", "Year", "Custom"))', 14 | 'ScheduleEveryCustom' => 'Varchar(50)', 15 | 'EmailScheduledTo' => 'Varchar(255)' 16 | ); 17 | 18 | /** 19 | * @var array 20 | */ 21 | private static $has_one = array( 22 | 'QueuedJob' => 'QueuedJobDescriptor' 23 | ); 24 | 25 | /** 26 | * @param FieldList $fields 27 | */ 28 | public function updateCMSFields(FieldList $fields) { 29 | Requirements::javascript('advancedreports/javascript/scheduled-report-settings.js'); 30 | 31 | $first = new DatetimeField( 32 | 'FirstScheduled', 33 | _t('AdvancedReport.FIRST_SCHEDULED_GENERATION', 'First scheduled generation') 34 | ); 35 | $first->getDateField()->setConfig('showcalendar', true); 36 | 37 | if($this->owner->QueuedJobID) { 38 | $next = $this->owner->QueuedJob()->obj('StartAfter')->Nice(); 39 | } else { 40 | $next = _t('AdvancedReport.NOT_CURRENTLY_SCHEDULED', 'not currently scheduled'); 41 | } 42 | 43 | $fields->addFieldsToTab('Root.Scheduling', array( 44 | new CheckboxField( 45 | 'Scheduled', 46 | _t('AdvancedReport.SCHEDULE_REPORT_GENERATION', 'Schedule report generation?') 47 | ), 48 | new TextField( 49 | 'ScheduledTitle', 50 | _t('AdvancedReport.SCHEDULED_REPORT_TITLE', 'Scheduled report title') 51 | ), 52 | $first, 53 | new DropdownField( 54 | 'ScheduleEvery', 55 | _t('AdvancedReport.SCHEDULE_EVERY', 'Schedule every'), 56 | $this->owner->dbObject('ScheduleEvery')->enumValues() 57 | ), 58 | TextField::create('ScheduleEveryCustom') 59 | ->setTitle(_t('AdvancedReport.CUSTOM_INTERVAL', 'Custom interval')) 60 | ->setDescription(_t( 61 | 'AdvancedReport.USING_STRTOTIME_FORMAT', 62 | 'Using relative strtotime format', 63 | null, 64 | array('link' => 'http://php.net/strtotime') 65 | ) 66 | ), 67 | new TextField( 68 | 'EmailScheduledTo', 69 | _t('AdvancedReport.EMAIL_SCHEDULED_TO', 'Email scheduled reports to') 70 | ), 71 | new ReadonlyField( 72 | 'NextScheduledGeneration', 73 | _t('AdvancedReport.NEXT_SCHEDULED_GENERATION', 'Next scheduled generation'), 74 | $next 75 | ) 76 | )); 77 | } 78 | 79 | public function onBeforeWrite() { 80 | if(!$this->owner->ScheduledTitle) { 81 | $this->owner->ScheduledTitle = $this->owner->Title; 82 | } 83 | 84 | $jobExists = $this->owner->QueuedJob()->exists(); 85 | if($this->owner->Scheduled) { 86 | $changed = $this->owner->getChangedFields(); 87 | $changed = isset($changed['FirstScheduled']) 88 | || isset($changed['ScheduleEvery']) 89 | || isset($changed['ScheduleEveryCustom']); 90 | 91 | if($jobExists && $changed) { 92 | $this->owner->QueuedJob()->delete(); 93 | $this->owner->QueuedJobID = 0; 94 | } 95 | 96 | if(!$jobExists) { 97 | if($this->owner->FirstScheduled) { 98 | $time = date('Y-m-d H:i:s', strtotime($this->owner->FirstScheduled)); 99 | } else { 100 | $time = date('Y-m-d H:i:s'); 101 | } 102 | 103 | $this->owner->QueuedJobID = singleton('QueuedJobService')->queueJob( 104 | new ScheduledReportJob($this->owner), $time 105 | ); 106 | } 107 | } else { 108 | if($jobExists) { 109 | $this->owner->QueuedJob()->delete(); 110 | $this->owner->QueuedJobID = 0; 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /code/extensions/ScheduledReportExtension.php: -------------------------------------------------------------------------------- 1 | 'Varchar(255)', 15 | 'FirstGeneration' => 'SS_Datetime', 16 | 'RegenerateEvery' => "Enum(',Hour,Day,Week,Fortnight,Month,Year')", 17 | 'RegenerateFree' => 'Varchar', 18 | 'SendReportTo' => 'Varchar(255)', 19 | ); 20 | 21 | /** 22 | * @var array 23 | */ 24 | private static $has_one = array( 25 | 'ScheduledJob' => 'QueuedJobDescriptor', 26 | ); 27 | 28 | /** 29 | * 30 | * @param FieldSet $fields 31 | */ 32 | public function updateCMSFields(FieldList $fields) { 33 | if (class_exists('AbstractQueuedJob')) { 34 | $fields->addFieldsToTab('Root.Schedule', array( 35 | new TextField('ScheduledTitle', _t('AdvancedReport.SCHEDULED_TITLE', 'Title for scheduled report')), 36 | $dt = new Datetimefield('FirstGeneration', _t('AdvancedReport.FIRST_GENERATION', 'First generation')), 37 | new DropdownField( 38 | 'RegenerateEvery', 39 | _t('AdvancedReport.REGENERATE_EVERY', 'Regenerate every'), 40 | $this->owner->dbObject('RegenerateEvery')->enumValues() 41 | ), 42 | new TextField( 43 | 'RegenerateFree', 44 | _t('AdvancedReport.REGENERATE_FREE', 'Scheduled (in strtotime format from first generation)') 45 | ), 46 | new TextField('SendReportTo', _t('AdvancedReport.SEND_TO', 'Send to email addresses')), 47 | )); 48 | 49 | if ($this->owner->ScheduledJobID) { 50 | $jobTime = $this->owner->ScheduledJob()->StartAfter; 51 | $fields->addFieldsToTab('Root.Schedule', array( 52 | new ReadonlyField('NextRunDate', _t('AdvancedReport.NEXT_RUN_DATE', 'Next run date'), $jobTime) 53 | )); 54 | } 55 | 56 | $dt->getDateField()->setConfig('showcalendar', true); 57 | $dt->getTimeField()->setConfig('showdropdown', true); 58 | 59 | } else { 60 | $fields->addFieldToTab( 61 | 'Root.Schedule', 62 | new LiteralField('WARNING', 'You must install the Queued Jobs module to schedule reports') 63 | ); 64 | } 65 | } 66 | 67 | public function onBeforeWrite() { 68 | parent::onBeforeWrite(); 69 | 70 | if (!$this->owner->ScheduledTitle) { 71 | $this->owner->ScheduledTitle = $this->owner->Title; 72 | } 73 | 74 | if ($this->owner->FirstGeneration) { 75 | $changed = $this->owner->getChangedFields(); 76 | $changed = false 77 | || isset($changed['FirstGeneration']) 78 | || isset($changed['RegenerateEvery']) 79 | || isset($changed['RegenerateFree']); 80 | 81 | if ($changed && $this->owner->ScheduledJobID) { 82 | if ($this->owner->ScheduledJob()->ID) { 83 | $this->owner->ScheduledJob()->delete(); 84 | } 85 | 86 | $this->owner->ScheduledJobID = 0; 87 | } 88 | 89 | if (!$this->owner->ScheduledJobID) { 90 | $job = new ScheduledReportJob($this->owner); 91 | $time = date('Y-m-d H:i:s'); 92 | if ($this->owner->FirstGeneration) { 93 | $time = date('Y-m-d H:i:s', strtotime($this->owner->FirstGeneration)); 94 | } 95 | 96 | $this->owner->ScheduledJobID = singleton('QueuedJobService')->queueJob($job, $time); 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /code/filters/IsNullFilter.php: -------------------------------------------------------------------------------- 1 | model = $query->applyRelation($this->relation); 23 | return $query->where(sprintf( 24 | "%s IS NULL", 25 | $this->getDbName() 26 | )); 27 | } 28 | 29 | protected function excludeOne(\DataQuery $query) { 30 | $this->model = $query->applyRelation($this->relation); 31 | return $query->where(sprintf( 32 | "%s IS NOT NULL", 33 | $this->getDbName() 34 | )); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /code/formatters/CsvReportFormatter.php: -------------------------------------------------------------------------------- 1 | headers as $field => $display) { 21 | $header[] = $display; 22 | } 23 | 24 | return '"'.implode('","', $header).'"'; 25 | } 26 | 27 | /** 28 | * Create a body for the report 29 | */ 30 | protected function createBody($tableName, $tableData) { 31 | $body = array(); 32 | 33 | $formatting = $this->getFieldFormatters(); 34 | 35 | foreach ($tableData as $row) { 36 | $csvRow = array(); 37 | foreach ($row as $field => $value) { 38 | if (isset($formatting[$field])) { 39 | $value = $formatting[$field]->format($value); 40 | } 41 | $csvRow[] = str_replace('"', '""', $value); 42 | } 43 | $body[] = '"'.implode('","', $csvRow).'"'; 44 | } 45 | 46 | return implode("\n", $body); 47 | } 48 | 49 | /** 50 | * Format the header and body into a complete report output. 51 | */ 52 | protected function formatReport($reportPieces) { 53 | $bits = ''; 54 | 55 | foreach ($reportPieces as $tableName => $table) { 56 | if ($tableName != ReportFormatter::DEFAULT_TABLE_NAME) { 57 | $bits .= '"'.$tableName.'",'; 58 | } 59 | $bits .= $table['Header']."\n".$table['Body']."\n,\n,\n"; 60 | } 61 | return $bits; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /code/formatters/DateFromTimestampFormatter.php: -------------------------------------------------------------------------------- 1 | 7 | * @license BSD License http://www.silverstripe.org/bsd-license 8 | */ 9 | class FullDateFromTimestampFormatter implements ReportFieldFormatter { 10 | public function format($value) { 11 | $value = (double) $value; 12 | if ($value) { 13 | return date('Y-m-d H:i:s', $value); 14 | } 15 | } 16 | 17 | public function label() { 18 | return "Timestamp to Y-m-d H:i:s"; 19 | } 20 | } 21 | 22 | class DateFromTimestampFormatter implements ReportFieldFormatter { 23 | public function format($value) { 24 | $value = (double) $value; 25 | if ($value) { 26 | return date('Y-m-d', $value); 27 | } 28 | } 29 | 30 | public function label() { 31 | return "Timestamp to Y-m-d"; 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /code/formatters/DecimalHoursFormatter.php: -------------------------------------------------------------------------------- 1 | 7 | * @license BSD License http://www.silverstripe.org/bsd-license 8 | */ 9 | class DecimalHoursFormatter implements ReportFieldFormatter { 10 | public function format($value) { 11 | $hours = floor($value); 12 | $mins = round(($value - $hours) * 60); 13 | return $hours . ':' . str_pad($mins, 2, '0', STR_PAD_LEFT); 14 | } 15 | 16 | public function label() { 17 | return 'Decimal to Time'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /code/formatters/HtmlReportFormatter.php: -------------------------------------------------------------------------------- 1 | '; 26 | foreach ($this->headers as $field => $display) { 27 | $header[] = ''.$display.''; 28 | } 29 | $header[] = ''; 30 | 31 | return implode("\n", $header); 32 | } 33 | 34 | /** 35 | * Create a body for the report 36 | * 37 | * @return string 38 | */ 39 | protected function createBody($tableName, $tableData) { 40 | $body = array(); 41 | $body[] = ''; 42 | 43 | $formatting = $this->getFieldFormatters(); 44 | 45 | $rowNum = 1; 46 | foreach ($tableData as $row) { 47 | $oddEven = $rowNum % 2 == 0 ? 'even' : 'odd'; 48 | $body[] = ''; 49 | foreach ($row as $field => $value) { 50 | $extraclass = ''; 51 | if ($value == '') { 52 | $extraclass = 'noReportData'; 53 | } 54 | 55 | if (isset($formatting[$field])) { 56 | $value = $formatting[$field]->format($value); 57 | } 58 | 59 | $body[] = ''.$value.''; 60 | } 61 | $body[] = ''; 62 | $rowNum++; 63 | } 64 | 65 | $body[] = ''; 66 | 67 | return implode("\n", $body); 68 | } 69 | 70 | /** 71 | * Format the header and body into a complete report output. 72 | * 73 | * @return string 74 | */ 75 | protected function formatReport($reportPieces) { 76 | $bits = ''; 77 | 78 | foreach ($reportPieces as $tableName => $table) { 79 | if ($tableName != ReportFormatter::DEFAULT_TABLE_NAME) { 80 | $bits .= '

'.$tableName."

\n"; 81 | } 82 | 83 | $bits .= ''. 84 | $table['Header'].$table['Body'].'
'."\n\n"; 85 | } 86 | 87 | return $bits; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /code/formatters/ReportFieldFormatter.php: -------------------------------------------------------------------------------- 1 | 7 | * @license BSD License http://www.silverstripe.org/bsd-license 8 | */ 9 | interface ReportFieldFormatter { 10 | 11 | /** 12 | * Return a 'nice' label for this formatter 13 | */ 14 | public function label(); 15 | 16 | /** 17 | * Format a report field in a particular way 18 | * 19 | * @param mixed $value 20 | */ 21 | public function format($value); 22 | } 23 | -------------------------------------------------------------------------------- /code/formatters/ReportFormatter.php: -------------------------------------------------------------------------------- 1 | false, // rolls up columns that have duplicate values so that only the 19 | // first instance is displayed. 20 | 'ShowHeader' => true 21 | ); 22 | 23 | /** 24 | * The report we're formatting 25 | * 26 | * @var AdvancedReport 27 | */ 28 | protected $report; 29 | 30 | /** 31 | * The headers. Should be in a format of 32 | * array( 33 | * 'Field' => 'Display' 34 | * ); 35 | * 36 | * @var array 37 | */ 38 | protected $headers; 39 | 40 | /** 41 | * The raw dataobjects to display 42 | * 43 | * @var DataObjectSet 44 | */ 45 | protected $dataObjects; 46 | 47 | /** 48 | * The formatted data 49 | * 50 | * @var array 51 | */ 52 | protected $data; 53 | 54 | /** 55 | * Create a new report formatter for the given report 56 | * 57 | * @param AdvancedReport $report 58 | */ 59 | public function __construct(AdvancedReport $report) { 60 | $this->report = $report; 61 | $this->headers = $report->getHeaders(); 62 | 63 | $vals = $this->report->AddInRows; 64 | // if there's something in "AddInRows", we need a "Total" column 65 | if ($vals && count($vals->getValues())) { 66 | $this->headers[self::ADD_IN_ROWS_TOTAL] = _t('ReportFormatter.HEADER_TOTAL', 'Total'); 67 | } 68 | } 69 | 70 | /** 71 | * Set or retrieve a configuration variable 72 | * 73 | * @param string $setting 74 | * @param string $value 75 | * @param mixed $val 76 | */ 77 | public function config($setting, $val = null) { 78 | if (!$val) { 79 | return isset($this->settings[$setting]) ? $this->settings[$setting] : null; 80 | } 81 | $this->settings[$setting] = $val; 82 | } 83 | 84 | /** 85 | * Do whatever processing necessary to output the report. 86 | */ 87 | public function format() { 88 | $this->reformatDataObjects(); 89 | $report = array(); 90 | 91 | foreach ($this->data as $tableName => $data) { 92 | $thisReport = array(); 93 | 94 | $thisReport['Header'] = $this->settings['ShowHeader'] ? $this->createHeader($tableName) : ''; 95 | $thisReport['Body'] = $this->createBody($tableName, $data); 96 | 97 | $report[$tableName] = $thisReport; 98 | } 99 | 100 | $output = $this->formatReport($report); 101 | 102 | return $output; 103 | } 104 | 105 | /** 106 | * returns a associated array of fields and formatter instances 107 | * 108 | * @return array 109 | */ 110 | protected function getFieldFormatters() { 111 | $formatting = array(); 112 | 113 | $formatters = $this->report->getFieldFormatting(); 114 | if ($formatters && count($formatters)) { 115 | foreach ($formatters as $field => $class) { 116 | $formatting[$field] = new $class; 117 | } 118 | } 119 | 120 | return $formatting; 121 | } 122 | 123 | /** 124 | * Restructures the data objects according to the settings of the report. 125 | * 126 | * @todo this method is too complex and should be refactored into smaller methods 127 | */ 128 | protected function reformatDataObjects() { 129 | $this->data = array(); 130 | 131 | $dataObjects = $this->report->getDataObjects(); 132 | $colsToBlank = $this->report->getDuplicatedBlankingFields(); 133 | 134 | $i = 0; 135 | $previousVals = array(); 136 | 137 | $tableName = self::DEFAULT_TABLE_NAME; 138 | 139 | $paginateBy = $this->report->dottedFieldToUnique($this->report->PaginateBy); 140 | $headerTemplate = $this->report->PageHeader ? $this->report->PageHeader : '$name'; 141 | 142 | $addCols = null; 143 | if ($this->report->AddInRows && count($this->report->AddInRows->getValues())) { 144 | $addCols = $this->report->AddInRows->getValues(); 145 | } 146 | if (!$dataObjects) { 147 | $this->data[$tableName] = array(); 148 | return; 149 | } 150 | 151 | foreach ($dataObjects as $item) { 152 | 153 | // lets check to see whether this item has the paginate variable, if so we want to be 154 | // adding this result to that table entry 155 | if ($paginateBy) { 156 | $pageVar = is_object($item) ? $item->$paginateBy : $item[$paginateBy]; 157 | if ($pageVar) { 158 | $tableName = str_replace('$name', $pageVar, $headerTemplate); 159 | } else { 160 | $tableName = sprintf(_t('ReportFormatter.NO_PAGINATE_VALUE', 'No %s'), $paginateBy); 161 | } 162 | } 163 | 164 | $row = array(); 165 | 166 | $addToTable = isset($this->data[$tableName]) ? $this->data[$tableName] : array(); 167 | 168 | $rowSum = 0; 169 | 170 | foreach ($this->headers as $field => $display) { 171 | // Account for our total summation of things. 172 | if ($field == self::ADD_IN_ROWS_TOTAL) { 173 | if (is_object($item)) { 174 | $item->$field = $rowSum; 175 | } else { 176 | $item[$field] = $rowSum; 177 | } 178 | } 179 | 180 | // check if the value is coming from an object or is an element of an array 181 | $value = 0; 182 | if (is_object($item)) { 183 | // if this is an object we need to check if the field we are looking for is a method or a value 184 | if (method_exists($item, $field)) { 185 | $value = $item->$field(); 186 | } else { 187 | $value = $item->$field; 188 | } 189 | } else { 190 | $value = isset($item[$field]) ? $item[$field] : ''; 191 | } 192 | 193 | if (in_array($field, $colsToBlank)) { 194 | if (!isset($previousVals[$field]) || $previousVals[$field] != $value) { 195 | $row[$field] = $value; 196 | 197 | // if this value that has changed is the 'first' value, then we need to reset all the other 198 | // 'previous' values from left to right from this position 199 | $previousVals = $this->resetPreviousVals($previousVals, $field); 200 | 201 | $previousVals[$field] = $value; 202 | } else { 203 | $row[$field] = ''; 204 | } 205 | } else { 206 | $row[$field] = $value; 207 | } 208 | 209 | if ($addCols && in_array($field, $addCols)) { 210 | $rowSum += $value; 211 | } 212 | } 213 | 214 | $addToTable[] = $row; 215 | $this->data[$tableName] = $addToTable; 216 | $i++; 217 | } 218 | 219 | // now that the tables have been created, need to do column summation on each. We could do this during 220 | // the above looping, but because we don't FORCE the items to be properly sorted first, we can't guarantee 221 | // that the columns that need summation are in the right places 222 | $addCols = $this->report->AddCols ? $this->report->AddCols->getValues() : array(); 223 | if (count($addCols)) { 224 | // if we had a row total, we'll add it in by default; it only makes sense! 225 | if (isset($this->headers[self::ADD_IN_ROWS_TOTAL])) { 226 | $addCols[] = self::ADD_IN_ROWS_TOTAL; 227 | } 228 | 229 | foreach ($this->data as $tableName => $data) { 230 | $sums = array(); 231 | 232 | $titleColumn = null; 233 | foreach ($data as $row) { 234 | $prevField = null; 235 | foreach ($row as $field => $value) { 236 | if (in_array($field, $addCols)) { 237 | // if we haven't already figured it out, we now know that we want the field BEFORE 238 | // this as the column we put the Total text into 239 | if (!$titleColumn) { 240 | $titleColumn = $prevField; 241 | } 242 | $cur = isset($sums[$field]) ? $sums[$field] : 0; 243 | 244 | // use a report custom method for adding up or count/sum it up 245 | // based on the best possible assumptions we can make 246 | if (method_exists($this->report, 'columnAdder')) { 247 | $sums[$field] = $this->report->columnAdder($field, $cur, $value); 248 | } else { 249 | // summing up totals makes only sense if it is a number 250 | // otherwise we count the number of items 251 | if (is_numeric($value)) { 252 | $sums[$field] = $cur + $value; 253 | } else { 254 | $sums[$field] = $cur + 1; 255 | } 256 | } 257 | } else { 258 | $sums[$field] = ''; 259 | } 260 | $prevField = $field; 261 | } 262 | } 263 | 264 | // figure out the name of the field we want to stick the Total text 265 | $sums[$titleColumn] = _t('ReportFormatter.ROW_TOTAL', 'Total'); 266 | $data[] = $sums; 267 | $this->data[$tableName] = $data; 268 | } 269 | } 270 | } 271 | 272 | /** 273 | * Finds the 'row position' of a given field name in the current report structure 274 | */ 275 | protected function resetPreviousVals($vals, $fieldName) { 276 | $newVals = array(); 277 | foreach ($vals as $field => $val) { 278 | if ($field == $fieldName) { 279 | break; 280 | } 281 | $newVals[$field] = $val; 282 | } 283 | return $newVals; 284 | } 285 | 286 | /** 287 | * Indicate what output format we're going to 288 | */ 289 | abstract protected function getOutputFormat(); 290 | 291 | /** 292 | * Create a header for the report 293 | */ 294 | abstract protected function createHeader($tableName); 295 | 296 | /** 297 | * Create a body for the report 298 | */ 299 | abstract protected function createBody($tableName, $tableData); 300 | 301 | /** 302 | * Format the header and body into a complete report output. 303 | * 304 | * @param array $reportPieces 305 | * The pieces of the report in an array indexed with 'Header' and 'Body' 306 | */ 307 | abstract protected function formatReport($reportPieces); 308 | } 309 | -------------------------------------------------------------------------------- /code/formatters/SecondsToHoursFormatter.php: -------------------------------------------------------------------------------- 1 | 7 | * @license BSD License http://www.silverstripe.org/bsd-license 8 | */ 9 | class SecondsToHoursFormatter extends DecimalHoursFormatter implements ReportFieldFormatter { 10 | public function format($value) { 11 | if ($value) { 12 | $value = $value / 3600; 13 | } 14 | return parent::format($value); 15 | } 16 | 17 | public function label() { 18 | return 'Seconds to hours'; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /code/jobs/ScheduledReportJob.php: -------------------------------------------------------------------------------- 1 | reportID = $report->ID; 13 | $this->timesGenerated = $timesGenerated; 14 | $this->totalSteps = 1; 15 | } 16 | } 17 | 18 | /** 19 | * @return AdvancedReport 20 | */ 21 | public function getReport() { 22 | return AdvancedReport::get()->byID($this->reportID); 23 | } 24 | 25 | /** 26 | * @return string 27 | */ 28 | public function getTitle() { 29 | return 'Generate Report ' . $this->getReport()->ScheduledTitle; 30 | } 31 | 32 | public function process() { 33 | $service = singleton('QueuedJobService'); 34 | $report = $this->getReport(); 35 | 36 | if(!$report) { 37 | $this->currentStep++; 38 | $this->isComplete = true; 39 | 40 | return; 41 | } 42 | 43 | $clone = clone $report; 44 | $clone->GeneratedReportTitle = $report->ScheduledTitle; 45 | 46 | $result = $clone->prepareAndGenerate(); 47 | 48 | if($report->ScheduleEvery) { 49 | if($report->ScheduleEvery == 'Custom') { 50 | $interval = $report->ScheduleEveryCustom; 51 | } else { 52 | $interval = $report->ScheduleEvery; 53 | } 54 | 55 | $next = $service->queueJob( 56 | new ScheduledReportJob($report, $this->timesGenerated + 1), 57 | date('Y-m-d H:i:s', strtotime("+1 $interval")) 58 | ); 59 | 60 | $report->QueuedJobID = $next; 61 | } else { 62 | $report->Scheduled = false; 63 | $report->QueuedJobID = 0; 64 | } 65 | 66 | $report->write(); 67 | 68 | if($report->EmailScheduledTo) { 69 | $email = new Email(); 70 | $email->setTo($report->EmailScheduledTo); 71 | $email->setSubject($result->Title); 72 | $email->setBody(_t( 73 | 'ScheduledReportJob.SEE_ATTACHED_REPORT', 'Please see the attached report file' 74 | )); 75 | $email->attachFile($result->PDFFile()->getFullPath(), $result->PDFFile()->Filename, 'application/pdf'); 76 | $email->send(); 77 | } 78 | 79 | $this->currentStep++; 80 | $this->isComplete = true; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /code/pages/ReportHolder.php: -------------------------------------------------------------------------------- 1 | canCreate()) { 21 | return null; 22 | } 23 | 24 | $classes = ClassInfo::subclassesFor('AdvancedReport'); 25 | $titles = array(); 26 | 27 | array_shift($classes); 28 | 29 | foreach($classes as $class) { 30 | $titles[$class] = singleton($class)->singular_name(); 31 | } 32 | 33 | return new Form( 34 | $this, 35 | 'Form', 36 | new FieldList( 37 | new TextField('Title', _t('ReportHolder.TITLE', 'Title')), 38 | new TextareaField('Description', _t('ReportHolder.DESCRIPTION', 'Description')), 39 | DropdownField::create('ClassName') 40 | ->setTitle(_t('ReportHolder.TYPE', 'Type')) 41 | ->setSource($titles) 42 | ->setHasEmptyDefault(true) 43 | ), 44 | new FieldList( 45 | new FormAction('doCreate', _t('ReportHolder.CREATE', 'Create')) 46 | ), 47 | new RequiredFields('Title', 'ClassName') 48 | ); 49 | } 50 | 51 | public function doCreate($data, $form) { 52 | if(!singleton('AdvancedReport')->canCreate()) { 53 | return Security::permissionFailure($this); 54 | } 55 | 56 | $data = $form->getData(); 57 | 58 | $description = $data['Description']; 59 | $class = $data['ClassName']; 60 | 61 | if(!is_subclass_of($class, 'AdvancedReport')) { 62 | $form->addErrorMessage( 63 | 'ClassName', 64 | _t('ReportHolder.INVALID_TYPE', 'An invalid report type was selected'), 65 | 'required' 66 | ); 67 | 68 | return $this->redirectBack(); 69 | } 70 | 71 | $page = new ReportPage(); 72 | 73 | $page->update(array( 74 | 'Title' => $data['Title'], 75 | 'Content' => $description ? "

$description

" : '', 76 | 'ReportType' => $class, 77 | 'ParentID' => $this->data()->ID 78 | )); 79 | 80 | $page->writeToStage('Stage'); 81 | 82 | if(Versioned::current_stage() == Versioned::get_live_stage()) { 83 | $page->doPublish(); 84 | } 85 | 86 | return $this->redirect($page->Link()); 87 | } 88 | 89 | } -------------------------------------------------------------------------------- /code/pages/ReportPage.php: -------------------------------------------------------------------------------- 1 | 'Varchar(64)' 16 | ); 17 | 18 | private static $has_one = array( 19 | 'ReportTemplate' => 'AdvancedReport' 20 | ); 21 | 22 | private static $dependencies = array( 23 | 'reportsService' => '%$AdvancedReportsServiceInterface' 24 | ); 25 | 26 | /** 27 | * @var AdvancedReportsServiceInterface 28 | */ 29 | private $reportsService; 30 | 31 | public function setReportsService(AdvancedReportsServiceInterface $service) { 32 | $this->reportsService = $service; 33 | } 34 | 35 | public function getCMSFields() { 36 | $fields = parent::getCMSFields(); 37 | 38 | $fields->addFieldToTab( 39 | 'Root.Main', 40 | DropdownField::create('ReportType') 41 | ->setTitle(_t('ReportPage.REPORT_TYPE', 'Report type')) 42 | ->setSource($this->reportsService->getReportTypes()) 43 | ->setHasEmptyDefault(true), 44 | 'Content' 45 | ); 46 | 47 | return $fields; 48 | } 49 | 50 | /** 51 | * Creates a report template instance if one does not exist. 52 | */ 53 | protected function onBeforeWrite() { 54 | parent::onBeforeWrite(); 55 | 56 | if(!$this->ReportTemplateID && $this->ReportType && ClassInfo::exists($this->ReportType)) { 57 | $template = Object::create($this->ReportType); 58 | $template->Title = $this->Title; 59 | $template->write(); 60 | 61 | $this->ReportTemplateID = $template->ID; 62 | } 63 | } 64 | 65 | } 66 | 67 | class ReportPage_Controller extends Page_Controller { 68 | 69 | private static $allowed_actions = array( 70 | 'settings', 71 | 'preview', 72 | 'GenerateForm', 73 | 'SettingsForm', 74 | 'DeleteGeneratedReportForm' 75 | ); 76 | 77 | public function settings() { 78 | if(!$this->CanEditTemplate()) { 79 | return Security::permissionFailure($this); 80 | } 81 | 82 | return $this->renderWith('Page', array( 83 | 'Title' => _t('ReportPage.EDIT_REPORT_SETTINGS', 'Edit Report Settings'), 84 | 'Form' => $this->SettingsForm() 85 | )); 86 | } 87 | 88 | public function preview() { 89 | if(!$this->CanGenerateReport()) { 90 | return Security::permissionFailure(); 91 | } 92 | 93 | return $this->ReportTemplate()->createReport('html')->content; 94 | } 95 | 96 | public function GenerateForm() { 97 | if(!$this->CanGenerateReport()) { 98 | return null; 99 | } 100 | 101 | return new Form( 102 | $this, 103 | 'GenerateForm', 104 | new FieldList( 105 | new TextField( 106 | 'Title', 107 | _t('AdvancedReport.GENERATED_REPORT_TITLE', 'Generated report title'), 108 | $this->ReportTemplate()->Title 109 | ) 110 | ), 111 | new FieldList( 112 | new FormAction('doPreview', _t('AdvancedReport.PREVIEW', 'Preview')), 113 | new FormAction('doGenerate', _t('AdvancedReport.GENERATE', 'Generate')) 114 | ), 115 | new RequiredFields('Title') 116 | ); 117 | } 118 | 119 | public function doPreview($data) { 120 | $report = clone $this->ReportTemplate(); 121 | $report->Title = $data['Title']; 122 | 123 | return $report->createReport('html')->content; 124 | } 125 | 126 | public function doGenerate($data, $form) { 127 | $report = clone $this->ReportTemplate(); 128 | 129 | if(!empty($data['Title'])) { 130 | $report->GeneratedReportTitle = $data['Title']; 131 | } 132 | 133 | $report->prepareAndGenerate(); 134 | 135 | return $this->redirect($this->Link()); 136 | } 137 | 138 | public function SettingsForm() { 139 | if(!$this->CanEditTemplate()) { 140 | return null; 141 | } 142 | 143 | $form = new Form( 144 | $this, 145 | 'SettingsForm', 146 | $this->ReportTemplate()->getSettingsFields(), 147 | new FieldList( 148 | new FormAction('doSaveSettings', _t('ReportPage.SAVE_SETTINGS', 'Save Settings')) 149 | ) 150 | ); 151 | $form->loadDataFrom($this->ReportTemplate()); 152 | 153 | return $form; 154 | } 155 | 156 | public function doSaveSettings($data, Form $form) { 157 | $template = $form->getRecord(); 158 | $form->saveInto($template); 159 | $template->write(); 160 | 161 | $form->sessionMessage( 162 | _t('ReportPage.SETTINGS_SAVED', 'The report settings have been saved'), 163 | 'good' 164 | ); 165 | 166 | return $this->redirectBack(); 167 | } 168 | 169 | public function DeleteGeneratedReportForm() { 170 | return new Form( 171 | $this, 172 | 'DeleteGeneratedReportForm', 173 | new FieldList(new HiddenField('ID')), 174 | new FieldList(new FormAction('doDeleteGeneratedReport', _t('ReportPage.DELETE', 'Delete'))), 175 | new RequiredFields('ID') 176 | ); 177 | } 178 | 179 | public function doDeleteGeneratedReport($data, $form) { 180 | if(!$this->ReportTemplateID) { 181 | $this->httpError(404); 182 | } 183 | 184 | $report = $this->ReportTemplate()->Reports()->byID($data['ID']); 185 | 186 | if(!$report) { 187 | $this->httpError(403); 188 | } 189 | 190 | if(!$report->canDelete()) { 191 | return Security::permissionFailure($this); 192 | } 193 | 194 | $report->delete(); 195 | 196 | return $this->redirectBack(); 197 | } 198 | 199 | /** 200 | * Gets a list of viewable reports, with attached delete forms. 201 | * 202 | * @return ArrayList 203 | */ 204 | public function GeneratedReports() { 205 | $result = new ArrayList(); 206 | 207 | if(!$this->ReportTemplateID) { 208 | return $result; 209 | } 210 | 211 | foreach($this->ReportTemplate()->Reports() as $report) { 212 | if(!$report->canView()) { 213 | continue; 214 | } 215 | 216 | if($report->canDelete()) { 217 | $form = $this->DeleteGeneratedReportForm(); 218 | $form->loadDataFrom($report); 219 | 220 | $report = $report->customise(array( 221 | 'DeleteForm' => $form 222 | )); 223 | } 224 | 225 | $result->push($report); 226 | } 227 | 228 | return $result; 229 | } 230 | 231 | public function CanEditTemplate() { 232 | return $this->ReportTemplateID && $this->ReportTemplate()->canEdit(); 233 | } 234 | 235 | public function CanGenerateReport() { 236 | return $this->ReportTemplateID && $this->ReportTemplate()->canGenerate(); 237 | } 238 | 239 | } 240 | -------------------------------------------------------------------------------- /code/services/AdvancedReportsService.php: -------------------------------------------------------------------------------- 1 | singular_name(); 15 | } 16 | 17 | return $result; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /code/services/AdvancedReportsServiceInterface.php: -------------------------------------------------------------------------------- 1 | headers.children().length) { 10 | var header = headers.find("input:last").val(field.val()).trigger("keyup"); 11 | 12 | field.change(function() { 13 | header.val(field.val()); 14 | }); 15 | } 16 | }) 17 | 18 | $(document).on('click', '#action_reportpreview', function (e) { 19 | e.preventDefault(); 20 | var form = $(this).parents('form'); 21 | var url = form.attr('action'); 22 | var base = $('base').attr('href'); 23 | 24 | 25 | url = base + url + '?action_reportpreview=1&' + form.serialize(); 26 | window.open(url); 27 | return false; 28 | }) 29 | })(jQuery); 30 | -------------------------------------------------------------------------------- /javascript/scheduled-report-settings.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | $.entwine("ss", function($) { 3 | $("#Form_ItemEditForm_ScheduleEvery").entwine({ 4 | onadd: function() { 5 | this.update(); 6 | }, 7 | onchange: function() { 8 | this.update(); 9 | }, 10 | update: function() { 11 | $("#ScheduleEveryCustom")[this.val() == "Custom" ? "show" : "hide"](); 12 | } 13 | }); 14 | }); 15 | })(jQuery); 16 | -------------------------------------------------------------------------------- /templates/AdvancedReport_csv.ss: -------------------------------------------------------------------------------- 1 | $ReportContent -------------------------------------------------------------------------------- /templates/AdvancedReport_html.ss: -------------------------------------------------------------------------------- 1 | 2 | 3 | <% base_tag %> 4 | 89 | 90 | 91 |
92 | 94 |

$Title

95 |

$Description.parse(BBCodeParser)

96 |

Generated $LastEdited.Nice

97 |
98 |
99 | <% if Format = pdf %> 100 | <% include PdfHeaderFooter %> 101 | <% end_if %> 102 | $ReportContent 103 |
104 | 105 | 106 | -------------------------------------------------------------------------------- /templates/CombinedReport_csv.ss: -------------------------------------------------------------------------------- 1 | <% loop Reports %> 2 | $ReportContent 3 | <% end_loop %> -------------------------------------------------------------------------------- /templates/CombinedReport_html.ss: -------------------------------------------------------------------------------- 1 | 2 | 3 | <% base_tag %> 4 | 89 | 90 | 91 |
92 | 94 |

$Title

95 |

$Description.Raw

96 |

Generated $LastEdited.Nice

97 |
98 | 99 | <% loop Reports %> 100 | 101 |
102 |

$Title

103 | <% if Format = pdf %> 104 | <% include PdfHeaderFooter %> 105 | <% end_if %> 106 | $ReportContent 107 |
108 | 109 | <% end_loop %> 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /templates/Layout/ReportHolder.ss: -------------------------------------------------------------------------------- 1 |

$Title

2 | 3 | $Content 4 | 5 | <% if $Children %> 6 |

Reports

7 | 8 | 15 | <% end_if %> 16 | 17 | <% if $Form %> 18 |

Create Report

19 | $Form 20 | <% end_if %> 21 | -------------------------------------------------------------------------------- /templates/Layout/ReportPage.ss: -------------------------------------------------------------------------------- 1 |

$Title

2 | 3 | $Content 4 | 5 | <% if $CanEditTemplate %> 6 |

Edit Report

7 | <% end_if %> 8 | 9 |
10 | 11 | <% if $GenerateForm %> 12 |

Generate Report

13 | $GenerateForm 14 | <% end_if %> 15 | 16 |
17 | 18 | <% if $GeneratedReports %> 19 |

Generated Reports

20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | <% loop $GeneratedReports %> 32 | 33 | 34 | 35 | 40 | 43 | 44 | <% end_loop %> 45 | 46 |
TitleGenerated AtLinksActions
$Title$Created.Nice 36 | HTML 37 | CSV 38 | PDF 39 | 41 | $DeleteForm 42 |
47 | <% end_if %> 48 | --------------------------------------------------------------------------------