1 <?php
2
3 4 5 6
7
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
23 class TableListField extends FormField {
24
25 26 27
28 protected $cachedSourceItems;
29
30 protected $sourceClass;
31
32 protected $sourceFilter = "";
33
34 protected $sourceSort = "";
35
36 protected $sourceJoin = array();
37
38 protected $fieldList;
39
40 protected $disableSorting = false;
41
42 43 44
45 protected $fieldListCsv;
46
47 48 49
50 protected $clickAction;
51
52 53 54
55 public $IsReadOnly;
56
57 58 59
60 protected $methodName;
61
62 63 64 65
66 protected $summaryFieldList;
67
68 69 70 71
72 protected $summaryTitle;
73
74 75 76
77 protected $template = "TableListField";
78
79 80 81
82 public $itemClass = 'TableListField_Item';
83
84 85 86
87 public $Markable;
88
89 public $MarkableTitle = null;
90
91 92 93
94 protected $readOnly;
95
96 97 98 99
100 protected $permissions = array(
101
102
103 "delete"
104 );
105
106 107 108 109 110 111 112 113 114 115 116 117 118 119
120 public $actions = array(
121 'delete' => array(
122 'label' => 'Delete',
123 'icon' => 'cms/images/delete.gif',
124 'icon_disabled' => 'cms/images/delete_disabled.gif',
125 'class' => 'deletelink'
126 )
127 );
128
129 130 131 132
133 public $defaultAction = '';
134
135 136 137 138 139 140
141 protected $customQuery;
142
143 144 145
146 protected $customCsvQuery;
147
148 149 150 151 152 153
154 protected $customSourceItems;
155
156 157 158
159 protected $csvSeparator = ";";
160
161 162 163
164 protected = true;
165
166 167 168 169 170 171 172
173 public $csvFieldEscape = array(
174 "\""=>"\"\"",
175 "\r\n"=>"",
176 "\r"=>"",
177 "\n"=>"",
178 );
179
180
181 182 183
184 protected $totalCount;
185
186 187 188
189 protected = false;
190
191 192 193 194 195
196 public = null;
197
198 199 200
201 protected $pageSize = 10;
202
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
219 public $highlightConditions = array();
220
221 222 223 224
225 public $fieldCasting = array();
226
227 228 229 230 231
232 public $fieldFormatting = array();
233
234 public $csvFieldFormatting = array();
235
236 237 238
239 public $exportButtonLabel = 'Export as CSV';
240
241 242 243 244
245 public $groupByField = null;
246
247 248 249
250 protected ;
251
252 protected $__cachedQuery;
253
254 function __construct($name, $sourceClass, $fieldList = null, $sourceFilter = null,
255 $sourceSort = null, $sourceJoin = null) {
256
257 $this->fieldList = ($fieldList) ? $fieldList : singleton($sourceClass)->summaryFields();
258 $this->sourceClass = $sourceClass;
259 $this->sourceFilter = $sourceFilter;
260 $this->sourceSort = $sourceSort;
261 $this->sourceJoin = $sourceJoin;
262 $this->readOnly = false;
263
264 parent::__construct($name);
265 }
266
267 268 269
270 function sourceFilter() {
271 return $this->sourceFilter;
272 }
273
274 function index() {
275 return $this->FieldHolder();
276 }
277
278 static $url_handlers = array(
279 'item/$ID' => 'handleItem',
280 '$Action' => '$Action',
281 );
282
283 function sourceClass() {
284 return $this->sourceClass;
285 }
286
287 function handleItem($request) {
288 return new TableListField_ItemRequest($this, $request->param('ID'));
289 }
290
291 function FieldHolder() {
292 Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/prototype/prototype.js');
293 Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/behaviour/behaviour.js');
294 Requirements::javascript(SAPPHIRE_DIR . '/javascript/prototype_improvements.js');
295 Requirements::javascript(THIRDPARTY_DIR . '/scriptaculous/effects.js');
296 Requirements::add_i18n_javascript(SAPPHIRE_DIR . '/javascript/lang');
297 Requirements::javascript(SAPPHIRE_DIR . '/javascript/TableListField.js');
298 Requirements::css(SAPPHIRE_DIR . '/css/TableListField.css');
299
300 if($this->clickAction) {
301 $id = $this->id();
302 Requirements::customScript(<<<JS
303 Behaviour.register({
304 '#$id tr' : {
305 onclick : function() {
306 $this->clickAction
307 return false;
308 }
309 }
310 });
311 JS
312 );}
313 return $this->renderWith($this->template);
314 }
315
316 function Headings() {
317 $headings = array();
318 foreach($this->fieldList as $fieldName => $fieldTitle) {
319 $isSorted = (isset($_REQUEST['ctf'][$this->Name()]['sort']) && $fieldName == $_REQUEST['ctf'][$this->Name()]['sort']);
320 // we can't allow sorting with partial summaries (groupByField)
321 $isSortable = ($this->form && $this->isFieldSortable($fieldName) && !$this->groupByField);
322
323 // sorting links (only if we have a form to refresh with)
324 if($this->form) {
325 $sortLink = $this->Link();
326 $sortLink = HTTP::setGetVar("ctf[{$this->Name()}][sort]", $fieldName, $sortLink,'&');
327
328 // Apply sort direction to the current sort field
329 if(!empty($_REQUEST['ctf'][$this->Name()]['sort']) && ($_REQUEST['ctf'][$this->Name()]['sort'] == $fieldName)) {
330 $dir = isset($_REQUEST['ctf'][$this->Name()]['dir']) ? $_REQUEST['ctf'][$this->Name()]['dir'] : null;
331 $dir = trim(strtolower($dir));
332 $newDir = ($dir == 'desc') ? null : 'desc';
333 $sortLink = HTTP::setGetVar("ctf[{$this->Name()}][dir]", Convert::raw2xml($newDir), $sortLink,'&');
334 }
335
336 if(isset($_REQUEST['ctf'][$this->Name()]['search']) && is_array($_REQUEST['ctf'][$this->Name()]['search'])) {
337 foreach($_REQUEST['ctf'][$this->Name()]['search'] as $parameter => $value) {
338 $XML_search = Convert::raw2xml($value);
339 $sortLink = HTTP::setGetVar("ctf[{$this->Name()}][search][$parameter]", $XML_search, $sortLink,'&');
340 }
341 }
342 } else {
343 $sortLink = '#';
344 }
345
346 $headings[] = new ArrayData(array(
347 "Name" => $fieldName,
348 "Title" => ($this->sourceClass) ? singleton($this->sourceClass)->fieldLabel($fieldTitle) : $fieldTitle,
349 "IsSortable" => $isSortable,
350 "SortLink" => $sortLink,
351 "SortBy" => $isSorted,
352 "SortDirection" => (isset($_REQUEST['ctf'][$this->Name()]['dir'])) ? $_REQUEST['ctf'][$this->Name()]['dir'] : null
353 ));
354 }
355 return new DataObjectSet($headings);
356 }
357
358 function disableSorting($to = true) {
359 $this->disableSorting = $to;
360 }
361
362 /**
363 * Determines if a field is "sortable".
364 * If the field is generated by a custom getter, we can't sort on it
365 * without generating all objects first (which would be a huge performance impact).
366 *
367 * @param string $fieldName
368 * @return bool
369 */
370 function isFieldSortable($fieldName) {
371 if($this->customSourceItems || $this->disableSorting) {
372 return false;
373 }
374
375 if(!$this->__cachedQuery) $this->__cachedQuery = $this->getQuery();
376
377 return $this->__cachedQuery->canSortBy($fieldName);
378 }
379
380 /**
381 * Dummy function to get number of actions originally generated in
382 * TableListField_Item.
383 *
384 * @return DataObjectSet
385 */
386 function Actions() {
387 $allowedActions = new DataObjectSet();
388 foreach($this->actions as $actionName => $actionSettings) {
389 if($this->Can($actionName)) {
390 $allowedActions->push(new ViewableData());
391 }
392 }
393
394 return $allowedActions;
395 }
396
397 /**
398 * Provide a custom query to compute sourceItems. This is the preferred way to using
399 * {@setSourceItems}, because we can still paginate.
400 * Caution: Other parameters such as {@sourceFilter} will be ignored.
401 * Please use this only as a fallback for really complex queries (e.g. involving HAVING and GROUPBY).
402 *
403 * @param $query SS_Query
404 */
405 function setCustomQuery(SQLQuery $query) {
406 // The type-hinting above doesn't seem to work consistently
407 if($query instanceof SQLQuery) {
408 $this->customQuery = $query;
409 } else {
410 user_error('TableList::setCustomQuery() should be passed a SQLQuery', E_USER_WARNING);
411 }
412 }
413
414 function setCustomCsvQuery(SQLQuery $query) {
415 // The type-hinting above doesn't seem to work consistently
416 if($query instanceof SQLQuery) {
417 $this->customCsvQuery = $query;
418 } else {
419 user_error('TableList::setCustomCsvQuery() should be passed a SQLQuery', E_USER_WARNING);
420 }
421 }
422
423 function setCustomSourceItems(DataObjectSet $items) {
424 // The type-hinting above doesn't seem to work consistently
425 if($items instanceof DataObjectSet) {
426 $this->customSourceItems = $items;
427 } else {
428 user_error('TableList::setCustomSourceItems() should be passed a DataObjectSet', E_USER_WARNING);
429 }
430 }
431
432 function sourceItems() {
433 $SQL_limit = ($this->showPagination && $this->pageSize) ? "{$this->pageSize}" : null;
434 if(isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) {
435 $SQL_start = (isset($_REQUEST['ctf'][$this->Name()]['start'])) ? intval($_REQUEST['ctf'][$this->Name()]['start']) : "0";
436 } else {
437 $SQL_start = 0;
438 }
439 if(isset($this->customSourceItems)) {
440 if($this->showPagination && $this->pageSize) {
441 $items = $this->customSourceItems->getRange($SQL_start, $SQL_limit);
442 } else {
443 $items = $this->customSourceItems;
444 }
445 } elseif(isset($this->cachedSourceItems)) {
446 $items = $this->cachedSourceItems;
447 } else {
448 // get query
449 $dataQuery = $this->getQuery();
450
451 // we don't limit when doing certain actions T
452 $methodName = isset($_REQUEST['url']) ? array_pop(explode('/', $_REQUEST['url'])) : null;
453 if(!$methodName || !in_array($methodName,array('printall','export'))) {
454 $dataQuery->limit(array(
455 'limit' => $SQL_limit,
456 'start' => (isset($SQL_start)) ? $SQL_start : null
457 ));
458 }
459
460 // get data
461 $records = $dataQuery->execute();
462 $sourceClass = $this->sourceClass;
463 $dataobject = new $sourceClass();
464 $items = $dataobject->buildDataObjectSet($records, 'DataObjectSet');
465
466 $this->cachedSourceItems = $items;
467 }
468
469 return $items;
470 }
471
472 function Items() {
473 $fieldItems = new DataObjectSet();
474 if($items = $this->sourceItems()) foreach($items as $item) {
475 if($item) $fieldItems->push(new $this->itemClass($item, $this));
476 }
477 return $fieldItems;
478 }
479
480 /**
481 * Generates the query for sourceitems (without pagination/limit-clause)
482 *
483 * @return string
484 */
485 function getQuery() {
486 if($this->customQuery) {
487 $query = clone $this->customQuery;
488 $baseClass = ClassInfo::baseDataClass($this->sourceClass);
489 } else {
490 $query = singleton($this->sourceClass)->extendedSQL($this->sourceFilter(), $this->sourceSort, null, $this->sourceJoin);
491 }
492
493 if(!empty($_REQUEST['ctf'][$this->Name()]['sort'])) {
494 $column = $_REQUEST['ctf'][$this->Name()]['sort'];
495 $dir = 'ASC';
496 if(!empty($_REQUEST['ctf'][$this->Name()]['dir'])) {
497 $dir = $_REQUEST['ctf'][$this->Name()]['dir'];
498 if(strtoupper(trim($dir)) == 'DESC') $dir = 'DESC';
499 }
500 if(!empty($_REQUEST['ctf'][$this->Name()]['sort_dir'])) {
501 $dir = $_REQUEST['ctf'][$this->Name()]['sort_dir'];
502 if(strtoupper(trim($dir)) == 'DESC') $dir = 'DESC';
503 }
504 if($query->canSortBy($column)) $query->orderby = $column.' '.$dir;
505 }
506
507 return $query;
508 }
509
510 function getCsvQuery() {
511 $baseClass = ClassInfo::baseDataClass($this->sourceClass);
512 if($this->customCsvQuery || $this->customQuery) {
513 $query = $this->customCsvQuery ? $this->customCsvQuery : $this->customQuery;
514 } else {
515 $query = singleton($this->sourceClass)->extendedSQL($this->sourceFilter(), $this->sourceSort, null, $this->sourceJoin);
516 }
517
518 return clone $query;
519 }
520
521 function FieldList() {
522 return $this->fieldList;
523 }
524
525 /**
526 * Configure this table to load content into a subform via ajax
527 */
528 function setClick_AjaxLoad($urlBase, $formID) {
529 $this->clickAction = "this.ajaxRequest('" . addslashes($urlBase) . "', '" . addslashes($formID) . "')";
530 }
531
532 /**
533 * Configure this table to open a popup window
534 */
535 function setClick_PopupLoad($urlBase) {
536 $this->clickAction = "var w = window.open(baseHref() + '$urlBase' + this.id.replace(/.*-(\d*)$/,'$1'), 'popup'); w.focus();";
537 }
538
539 function performReadonlyTransformation() {
540 $clone = clone $this;
541 $clone->setShowPagination(false);
542
543 // Only include the show action if it was in the original CTF.
544 $clone->setPermissions(in_array('show', $this->permissions) ? array('show') : array());
545
546 $clone->addExtraClass( 'readonly' );
547 $clone->setReadonly(true);
548 return $clone;
549 }
550
551 /**
552 * #################################
553 * CRUD
554 * #################################
555 */
556
557 /**
558 * @return String
559 */
560 function delete() {
561 if($this->Can('delete') !== true) {
562 return false;
563 }
564
565 $this->methodName = "delete";
566
567 $childId = Convert::raw2sql($_REQUEST['ctf']['childID']);
568
569 if (is_numeric($childId)) {
570 $childObject = DataObject::get_by_id($this->sourceClass, $childId);
571 if($childObject) $childObject->delete();
572 }
573
574 // TODO return status in JSON etc.
575 //return $this->renderWith($this->template);
576 }
577
578
579 /**
580 * #################################
581 * Summary-Row
582 * #################################
583 */
584
585 /**
586 * Can utilize some built-in summary-functions, with optional casting.
587 * Currently supported:
588 * - sum
589 * - avg
590 *
591 * @param $summaryTitle string
592 * @param $summaryFields array
593 * Simple Format: array("MyFieldName"=>"sum")
594 * With Casting: array("MyFieldname"=>array("sum","Currency->Nice"))
595 */
596 function addSummary($summaryTitle, $summaryFieldList) {
597 $this->summaryTitle = $summaryTitle;
598 $this->summaryFieldList = $summaryFieldList;
599 }
600
601 function removeSummary() {
602 $this->summaryTitle = null;
603 $this->summaryFields = null;
604 }
605
606 function HasSummary() {
607 return (isset($this->summaryFieldList));
608 }
609
610 function SummaryTitle() {
611 return $this->summaryTitle;
612 }
613
614 /**
615 * @param DataObjectSet $items Only used to pass grouped sourceItems for creating
616 * partial summaries.
617 */
618 function SummaryFields($items = null) {
619 if(!isset($this->summaryFieldList)) {
620 return false;
621 }
622 $summaryFields = array();
623 $fieldListWithoutFirst = $this->fieldList;
624 if(!empty($this->summaryTitle)) {
625 array_shift($fieldListWithoutFirst);
626 }
627 foreach($fieldListWithoutFirst as $fieldName => $fieldTitle) {
628
629 if(in_array($fieldName, array_keys($this->summaryFieldList))) {
630 if(is_array($this->summaryFieldList[$fieldName])) {
631 $summaryFunction = "colFunction_{$this->summaryFieldList[$fieldName][0]}";
632 $casting = $this->summaryFieldList[$fieldName][1];
633 } else {
634 $summaryFunction = "colFunction_{$this->summaryFieldList[$fieldName]}";
635 $casting = null;
636 }
637
638 // fall back to integrated sourceitems if not passed
639 if(!$items) $items = $this->sourceItems();
640
641 $summaryValue = ($items) ? $this->$summaryFunction($items->column($fieldName)) : null;
642
643 // Optional casting, Format: array('MyFieldName'=>array('sum','Currency->Nice'))
644 if(isset($casting)) {
645 $summaryValue = $this->getCastedValue($summaryValue, $casting);
646 }
647 } else {
648 $summaryValue = null;
649 $function = null;
650 }
651
652 $summaryFields[] = new ArrayData(array(
653 'Function' => $function,
654 'SummaryValue' => $summaryValue,
655 'Name' => DBField::create('Varchar', $fieldName),
656 'Title' => DBField::create('Varchar', $fieldTitle),
657 ));
658 }
659 return new DataObjectSet($summaryFields);
660 }
661
662 function HasGroupedItems() {
663 return ($this->groupByField);
664 }
665
666 function GroupedItems() {
667 if(!$this->groupByField) {
668 return false;
669 }
670
671 $items = $this->sourceItems();
672 if(!$items || !$items->Count()) {
673 return false;
674 }
675
676 $groupedItems = $items->groupBy($this->groupByField);
677 $groupedArrItems = new DataObjectSet();
678 foreach($groupedItems as $key => $group) {
679 $fieldItems = new DataObjectSet();
680 foreach($group as $item) {
681 if($item) $fieldItems->push(new $this->itemClass($item, $this));
682 }
683 $groupedArrItems->push(new ArrayData(array(
684 'Items' => $fieldItems,
685 'SummaryFields' => $this->SummaryFields($group)
686 )));
687 }
688
689 return $groupedArrItems;
690 }
691
692 function colFunction_sum($values) {
693 return array_sum($values);
694 }
695
696 function colFunction_avg($values) {
697 return array_sum($values)/count($values);
698 }
699
700
701 /**
702 * #################################
703 * Permissions
704 * #################################
705 */
706
707 /**
708 * Template accessor for Permissions.
709 * See {@link TableListField_Item->Can()} for object-specific
710 * permissions.
711 *
712 * @return boolean
713 */
714 function Can($mode) {
715 if($mode == 'add' && $this->isReadonly()) {
716 return false;
717 } else if($mode == 'delete' && $this->isReadonly()) {
718 return false;
719 } else if($mode == 'edit' && $this->isReadonly()) {
720 return false;
721 } else {
722 return (in_array($mode, $this->permissions));
723 }
724
725 }
726
727 function setPermissions($arr) {
728 $this->permissions = $arr;
729 }
730
731 /**
732 * @return array
733 */
734 function getPermissions() {
735 return $this->permissions;
736 }
737
738 /**
739 * #################################
740 * Pagination
741 * #################################
742 */
743 function setShowPagination($bool) {
744 $this->showPagination = (bool)$bool;
745 }
746
747 /**
748 * @return boolean
749 */
750 function ShowPagination() {
751 if($this->showPagination && !empty($this->summaryFieldList)) {
752 user_error("You can't combine pagination and summaries - please disable one of them.", E_USER_ERROR);
753 }
754 return $this->showPagination;
755 }
756
757 function setPageSize($pageSize) {
758 $this->pageSize = $pageSize;
759 }
760
761 function PageSize() {
762 return $this->pageSize;
763 }
764
765 function ListStart() {
766 return $_REQUEST['ctf'][$this->Name()]['start'];
767 }
768
769 /**
770 * @param array
771 * @deprecated Put the query string onto your form's link instead :-)
772 */
773 function setExtraLinkParams($params){
774 user_error("TableListField::setExtraLinkParams() deprecated - put the query string onto your form's FormAction instead; it will be handed down to all field with special handlers", E_USER_NOTICE);
775 $this->extraLinkParams = $params;
776 }
777
778 /**
779 * @return array
780 */
781 function getExtraLinkParams(){
782 return $this->extraLinkParams;
783 }
784
785 function FirstLink() {
786 $start = 0;
787
788 if(!isset($_REQUEST['ctf'][$this->Name()]['start']) || !is_numeric($_REQUEST['ctf'][$this->Name()]['start']) || $_REQUEST['ctf'][$this->Name()]['start'] == 0) {
789 return null;
790 }
791 $baseLink = ($this->paginationBaseLink) ? $this->paginationBaseLink : $this->Link();
792 $link = Controller::join_links($baseLink, "?ctf[{$this->Name()}][start]={$start}");
793 if(isset($_REQUEST['ctf'][$this->Name()]['sort']) && ($sort = $_REQUEST['ctf'][$this->Name()]['sort'])) {
794 $sortString = "ctf[{$this->Name()}][sort]={$sort}";
795 if(isset($_REQUEST['ctf'][$this->Name()]['dir']) && ($dir = $_REQUEST['ctf'][$this->Name()]['dir'])) {
796 $sortString .= "&ctf[{$this->Name()}][dir]={$dir}";
797 }
798 $link .= "&" .$sortString;
799 }
800 if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
801 return $link;
802 }
803
804 function PrevLink() {
805 $currentStart = isset($_REQUEST['ctf'][$this->Name()]['start']) ? $_REQUEST['ctf'][$this->Name()]['start'] : 0;
806
807 if($currentStart == 0) {
808 return null;
809 }
810
811 $start = ($_REQUEST['ctf'][$this->Name()]['start'] - $this->pageSize < 0) ? 0 : $_REQUEST['ctf'][$this->Name()]['start'] - $this->pageSize;
812
813 $baseLink = ($this->paginationBaseLink) ? $this->paginationBaseLink : $this->Link();
814 $link = Controller::join_links($baseLink, "?ctf[{$this->Name()}][start]={$start}");
815 if(isset($_REQUEST['ctf'][$this->Name()]['sort']) && ($sort = $_REQUEST['ctf'][$this->Name()]['sort'])) {
816 $sortString = "ctf[{$this->Name()}][sort]={$sort}";
817 if(isset($_REQUEST['ctf'][$this->Name()]['dir']) && ($dir = $_REQUEST['ctf'][$this->Name()]['dir'])) {
818 $sortString .= "&ctf[{$this->Name()}][dir]={$dir}";
819 }
820 $link .= "&" .$sortString;
821 }
822 if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
823 return $link;
824 }
825
826
827 function NextLink() {
828 $currentStart = isset($_REQUEST['ctf'][$this->Name()]['start']) ? $_REQUEST['ctf'][$this->Name()]['start'] : 0;
829 $start = ($currentStart + $this->pageSize < $this->TotalCount()) ? $currentStart + $this->pageSize : $this->TotalCount() % $this->pageSize > 0;
830 if($currentStart >= $start-1) {
831 return null;
832 }
833 $baseLink = ($this->paginationBaseLink) ? $this->paginationBaseLink : $this->Link();
834 $link = Controller::join_links($baseLink, "?ctf[{$this->Name()}][start]={$start}");
835 if(isset($_REQUEST['ctf'][$this->Name()]['sort']) && ($sort = $_REQUEST['ctf'][$this->Name()]['sort'])) {
836 $sortString = "ctf[{$this->Name()}][sort]={$sort}";
837 if(isset($_REQUEST['ctf'][$this->Name()]['dir']) && ($dir = $_REQUEST['ctf'][$this->Name()]['dir'])) {
838 $sortString .= "&ctf[{$this->Name()}][dir]={$dir}";
839 }
840 $link .= "&" .$sortString;
841 }
842 if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
843 return $link;
844 }
845
846 function LastLink() {
847 $pageSize = ($this->TotalCount() % $this->pageSize > 0) ? $this->TotalCount() % $this->pageSize : $this->pageSize;
848 $start = $this->TotalCount() - $pageSize;
849 // Check if there is only one page, or if we are on last page
850 if($this->TotalCount() <= $pageSize || (isset($_REQUEST['ctf'][$this->Name()]['start']) && $_REQUEST['ctf'][$this->Name()]['start'] >= $start)) {
851 return null;
852 }
853
854 $baseLink = ($this->paginationBaseLink) ? $this->paginationBaseLink : $this->Link();
855 $link = Controller::join_links($baseLink, "?ctf[{$this->Name()}][start]={$start}");
856 if(isset($_REQUEST['ctf'][$this->Name()]['sort']) && ($sort = $_REQUEST['ctf'][$this->Name()]['sort'])) {
857 $sortString = "ctf[{$this->Name()}][sort]={$sort}";
858 if(isset($_REQUEST['ctf'][$this->Name()]['dir']) && ($dir = $_REQUEST['ctf'][$this->Name()]['dir'])) {
859 $sortString .= "&ctf[{$this->Name()}][dir]={$dir}";
860 }
861 $link .= "&" .$sortString;
862 }
863 if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
864 return $link;
865 }
866
867 function FirstItem() {
868 if ($this->TotalCount() < 1) return 0;
869 return isset($_REQUEST['ctf'][$this->Name()]['start']) ? $_REQUEST['ctf'][$this->Name()]['start'] + 1 : 1;
870 }
871
872 function LastItem() {
873 if(isset($_REQUEST['ctf'][$this->Name()]['start'])) {
874 return $_REQUEST['ctf'][$this->Name()]['start'] + min($this->pageSize, $this->TotalCount() - $_REQUEST['ctf'][$this->Name()]['start']);
875 } else {
876 return min($this->pageSize, $this->TotalCount());
877 }
878 }
879
880 function TotalCount() {
881 if($this->totalCount) {
882 return $this->totalCount;
883 }
884 if($this->customSourceItems) {
885 return $this->customSourceItems->Count();
886 }
887
888 $this->totalCount = $this->getQuery()->unlimitedRowCount();
889 return $this->totalCount;
890 }
891
892
893
894 /**
895 * #################################
896 * Search
897 * #################################
898 *
899 * @todo Not fully implemented at the moment
900 */
901
902 /**
903 * Compile all request-parameters for search and pagination
904 * (except the actual list-positions) as a query-string.
905 *
906 * @return String URL-parameters
907 */
908 function filterString() {
909
910 }
911
912
913
914 /**
915 * #################################
916 * CSV Export
917 * #################################
918 */
919 function setFieldListCsv($fields) {
920 $this->fieldListCsv = $fields;
921 }
922
923 /**
924 * Set the CSV separator character. Defaults to ,
925 */
926 function setCsvSeparator($csvSeparator) {
927 $this->csvSeparator = $csvSeparator;
928 }
929
930 /**
931 * Get the CSV separator character. Defaults to ,
932 */
933 function getCsvSeparator() {
934 return $this->csvSeparator;
935 }
936
937 /**
938 * Remove the header row from the CSV export
939 */
940 function removeCsvHeader() {
941 $this->csvHasHeader = false;
942 }
943
944 /**
945 * Exports a given set of comma-separated IDs (from a previous search-query, stored in a HiddenField).
946 * Uses {$csv_columns} if present, and falls back to {$result_columns}.
947 * We move the most filedata generation code to the function {@link generateExportFileData()} so that a child class
948 * could reuse the filedata generation code while overwrite export function.
949 *
950 * @todo Make relation-syntax available (at the moment you'll have to use custom sql)
951 */
952 function export() {
953 $now = Date("d-m-Y-H-i");
954 $fileName = "export-$now.csv";
955
956 if($fileData = $this->generateExportFileData($numColumns, $numRows)){
957 return SS_HTTPRequest::send_file($fileData, $fileName);
958 }else{
959 user_error("No records found", E_USER_ERROR);
960 }
961 }
962
963 function generateExportFileData(&$numColumns, &$numRows) {
964 $separator = $this->csvSeparator;
965 $csvColumns = ($this->fieldListCsv) ? $this->fieldListCsv : $this->fieldList;
966 $fileData = "\xEF\xBB\xBF"; // Make UTF-8 + BOM
967 $columnData = array();
968 $fieldItems = new DataObjectSet();
969
970 if($this->csvHasHeader) {
971 $fileData .= "\"" . implode("\"{$separator}\"", array_values($csvColumns)) . "\"";
972 $fileData .= "\n";
973 }
974
975 if(isset($this->customSourceItems)) {
976 $items = $this->customSourceItems;
977 } else {
978 $dataQuery = $this->getCsvQuery();
979 $items = $dataQuery->execute();
980 }
981
982 // temporary override to adjust TableListField_Item behaviour
983 $this->setFieldFormatting(array());
984 $this->fieldList = $csvColumns;
985
986 if($items) {
987 foreach($items as $item) {
988 if(is_array($item)) {
989 $className = isset($item['RecordClassName']) ? $item['RecordClassName'] : $item['ClassName'];
990 $item = new $className($item);
991 }
992 $fieldItem = new $this->itemClass($item, $this);
993
994 $fields = $fieldItem->Fields(false);
995 $columnData = array();
996 if($fields) foreach($fields as $field) {
997 $value = $field->Value;
998
999 // TODO This should be replaced with casting
1000 if(array_key_exists($field->Name, $this->csvFieldFormatting)) {
1001 $format = str_replace('$value', "__VAL__", $this->csvFieldFormatting[$field->Name]);
1002 $format = preg_replace('/\$([A-Za-z0-9-_]+)/','$item->$1', $format);
1003 $format = str_replace('__VAL__', '$value', $format);
1004 eval('$value = "' . $format . '";');
1005 }
1006
1007 $value = str_replace(array("\r", "\n"), "\n", $value);
1008 $value = str_replace("\n", "\r\n", $value);
1009 $tmpColumnData = '"' . str_replace('"', '""', $value) . '"';
1010 $columnData[] = $tmpColumnData;
1011 }
1012 $fileData .= implode($separator, $columnData);
1013 $fileData .= "\r\n";
1014
1015 $item->destroy();
1016 unset($item);
1017 unset($fieldItem);
1018 }
1019
1020 $numColumns = count($columnData);
1021 $numRows = $fieldItems->count();
1022 return $fileData;
1023 } else {
1024 return null;
1025 }
1026 }
1027
1028 /**
1029 * We need to instanciate this button manually as a normal button has no means of adding inline onclick-behaviour.
1030 */
1031 function ExportLink() {
1032 $exportLink = Controller::join_links($this->Link(), 'export');
1033
1034 if($this->extraLinkParams) $exportLink .= "?" . http_build_query($this->extraLinkParams);
1035 return $exportLink;
1036 }
1037
1038 function printall() {
1039 Requirements::clear();
1040 Requirements::css(CMS_DIR . '/css/typography.css');
1041 Requirements::css(CMS_DIR . '/css/cms_right.css');
1042 Requirements::css(SAPPHIRE_DIR . '/css/TableListField_print.css');
1043
1044 $this->cachedSourceItems = null;
1045 $oldShowPagination = $this->showPagination;
1046 $this->showPagination = false;
1047
1048 increase_time_limit_to();
1049 $this->Print = true;
1050
1051 $result = $this->renderWith(array($this->template . '_printable', 'TableListField_printable'));
1052
1053 $this->showPagination = $oldShowPagination;
1054
1055 return $result;
1056 }
1057
1058 function PrintLink() {
1059 $link = Controller::join_links($this->Link(), 'printall');
1060 if(isset($_REQUEST['ctf'][$this->Name()]['sort'])) {
1061 $link = HTTP::setGetVar("ctf[{$this->Name()}][sort]",Convert::raw2xml($_REQUEST['ctf'][$this->Name()]['sort']), $link);
1062 }
1063 return $link;
1064 }
1065
1066 /**
1067 * #################################
1068 * Utilty
1069 * #################################
1070 */
1071 function Utility() {
1072 $links = new DataObjectSet();
1073 if($this->can('export')) {
1074 $links->push(new ArrayData(array(
1075 'Title' => _t('TableListField.CSVEXPORT', 'Export to CSV'),
1076 'Link' => $this->ExportLink()
1077 )));
1078 }
1079 if($this->can('print')) {
1080 $links->push(new ArrayData(array(
1081 'Title' => _t('TableListField.PRINT', 'Print'),
1082 'Link' => $this->PrintLink()
1083 )));
1084 }
1085 return $links;
1086
1087 }
1088
1089 /**
1090 * Returns the content of the TableListField as a piece of FormResponse javascript
1091 * @deprecated Please use the standard URL through Link() which gives you the FieldHolder as an HTML fragment.
1092 */
1093 function ajax_refresh() {
1094 // compute sourceItems here instead of Items() to ensure that
1095 // pagination and filters are respected on template accessors
1096 //$this->sourceItems();
1097
1098 $response = $this->renderWith($this->template);
1099 FormResponse::update_dom_id($this->id(), $response, 1);
1100 FormResponse::set_non_ajax_content($response);
1101 return FormResponse::respond();
1102 }
1103
1104 function setFieldCasting($casting) {
1105 $this->fieldCasting = $casting;
1106 }
1107
1108 function setFieldFormatting($formatting) {
1109 $this->fieldFormatting = $formatting;
1110 }
1111
1112 function setCSVFieldFormatting($formatting) {
1113 $this->csvFieldFormatting = $formatting;
1114 }
1115
1116 /**
1117 * Edit the field list
1118 */
1119 function setFieldList($fieldList) {
1120 $this->fieldList = $fieldList;
1121 }
1122
1123 /**
1124 * @return String
1125 */
1126 function Name() {
1127 return $this->name;
1128 }
1129
1130 function Title() {
1131 // adding translating functionality
1132 // this is a bit complicated, because this parameter is passed to this class
1133 // and should come here translated already
1134 // adding this to TODO probably add a method to the classes
1135 // to return they're translated string
1136 // added by ruibarreiros @ 27/11/2007
1137 return $this->sourceClass ? singleton($this->sourceClass)->i18n_plural_name() : $this->Name();
1138 }
1139
1140 function NameSingular() {
1141 // same as Title()
1142 // added by ruibarreiros @ 27/11/2007
1143 return $this->sourceClass ? singleton($this->sourceClass)->i18n_singular_name() : $this->Name();
1144 }
1145
1146 function NamePlural() {
1147 // same as Title()
1148 // added by ruibarreiros @ 27/11/2007
1149 return $this->sourceClass ? singleton($this->sourceClass)->i18n_plural_name() : $this->Name();
1150 }
1151
1152 function setTemplate($template) {
1153 $this->template = $template;
1154 }
1155
1156 function CurrentLink() {
1157 $link = $this->Link();
1158
1159 if(isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) {
1160 $start = ($_REQUEST['ctf'][$this->Name()]['start'] < 0) ? 0 : $_REQUEST['ctf'][$this->Name()]['start'];
1161 $link = Controller::join_links($link, "?ctf[{$this->Name()}][start]={$start}");
1162 }
1163
1164 if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
1165
1166 return $link;
1167 }
1168
1169 function BaseLink() {
1170 user_error("TableListField::BaseLink() deprecated, use Link() instead", E_USER_NOTICE);
1171 return $this->Link();
1172 }
1173
1174 /**
1175 * @return Int
1176 */
1177 function sourceID() {
1178 $idField = $this->form->dataFieldByName('ID');
1179 if(!isset($idField)) {
1180 user_error("TableListField needs a formfield named 'ID' to be present", E_USER_ERROR);
1181 }
1182 return $idField->Value();
1183 }
1184
1185 /**
1186 * Helper method to determine permissions for a scaffolded
1187 * TableListField (or subclasses) - currently used in {@link ModelAdmin} and {@link DataObject->scaffoldFormFields()}.
1188 * Returns true for each permission that doesn't have an explicit getter.
1189 *
1190 * @todo Temporary method, implement directly in FormField subclasses with object-level permissions.
1191 *
1192 * @param string $class
1193 * @param numeric $id
1194 * @return array
1195 */
1196 public static function permissions_for_object($class, $id = null) {
1197 $permissions = array();
1198 $obj = ($id) ? DataObject::get_by_id($class, $id) : singleton($class);
1199
1200 if(!$obj->hasMethod('canView') || $obj->canView()) $permissions[] = 'show';
1201 if(!$obj->hasMethod('canEdit') || $obj->canEdit()) $permissions[] = 'edit';
1202 if(!$obj->hasMethod('canDelete') || $obj->canDelete()) $permissions[] = 'delete';
1203 if(!$obj->hasMethod('canCreate') || $obj->canCreate()) $permissions[] = 'add';
1204
1205 return $permissions;
1206 }
1207
1208 /**
1209 * @param $value
1210 *
1211 */
1212 function getCastedValue($value, $castingDefinition) {
1213 if(is_array($castingDefinition)) {
1214 $castingParams = $castingDefinition;
1215 array_shift($castingParams);
1216 $castingDefinition = array_shift($castingDefinition);
1217 } else {
1218 $castingParams = array();
1219 }
1220 if(strpos($castingDefinition,'->') === false) {
1221 $castingFieldType = $castingDefinition;
1222 $castingField = DBField::create($castingFieldType, $value);
1223 $value = call_user_func_array(array($castingField,'XML'),$castingParams);
1224 } else {
1225 $fieldTypeParts = explode('->', $castingDefinition);
1226 $castingFieldType = $fieldTypeParts[0];
1227 $castingMethod = $fieldTypeParts[1];
1228 $castingField = DBField::create($castingFieldType, $value);
1229 $value = call_user_func_array(array($castingField,$castingMethod),$castingParams);
1230 }
1231
1232 return $value;
1233 }
1234
1235 /**
1236 * #########################
1237 * Highlighting
1238 * #########################
1239 */
1240 function setHighlightConditions($conditions) {
1241 $this->highlightConditions = $conditions;
1242 }
1243 }
1244
1245 /**
1246 * A single record in a TableListField.
1247 * @package forms
1248 * @subpackage fields-relational
1249 * @see TableListField
1250 */
1251 class TableListField_Item extends ViewableData {
1252
1253 /**
1254 * @var DataObject The underlying data record,
1255 * usually an element of {@link TableListField->sourceItems()}.
1256 */
1257 protected $item;
1258
1259 /**
1260 * @var TableListField
1261 */
1262 protected $parent;
1263
1264 function __construct($item, $parent) {
1265 $this->failover = $this->item = $item;
1266 $this->parent = $parent;
1267 parent::__construct();
1268 }
1269
1270 function ID() {
1271 return $this->item->ID;
1272 }
1273
1274 function Parent() {
1275 return $this->parent;
1276 }
1277
1278 function Fields($xmlSafe = true) {
1279 $list = $this->parent->FieldList();
1280 foreach($list as $fieldName => $fieldTitle) {
1281 $value = "";
1282
1283 // This supports simple FieldName syntax
1284 if(strpos($fieldName,'.') === false) {
1285 $value = ($this->item->XML_val($fieldName) && $xmlSafe) ? $this->item->XML_val($fieldName) : $this->item->RAW_val($fieldName);
1286 // This support the syntax fieldName = Relation.RelatedField
1287 } else {
1288 $fieldNameParts = explode('.', $fieldName) ;
1289 $tmpItem = $this->item;
1290 for($j=0;$j<sizeof($fieldNameParts);$j++) {
1291 $relationMethod = $fieldNameParts[$j];
1292 $idField = $relationMethod . 'ID';
1293 if($j == sizeof($fieldNameParts)-1) {
1294 if($tmpItem) $value = $tmpItem->$relationMethod;
1295 } else {
1296 if($tmpItem) $tmpItem = $tmpItem->$relationMethod();
1297 }
1298 }
1299 }
1300
1301 // casting
1302 if(array_key_exists($fieldName, $this->parent->fieldCasting)) {
1303 $value = $this->parent->getCastedValue($value, $this->parent->fieldCasting[$fieldName]);
1304 } elseif(is_object($value) && method_exists($value, 'Nice')) {
1305 $value = $value->Nice();
1306 }
1307
1308 // formatting
1309 $item = $this->item;
1310 if(array_key_exists($fieldName, $this->parent->fieldFormatting)) {
1311 $format = str_replace('$value', "__VAL__", $this->parent->fieldFormatting[$fieldName]);
1312 $format = preg_replace('/\$([A-Za-z0-9-_]+)/','$item->$1', $format);
1313 $format = str_replace('__VAL__', '$value', $format);
1314 eval('$value = "' . $format . '";');
1315 }
1316
1317 //escape
1318 if($escape = $this->parent->fieldEscape){
1319 foreach($escape as $search => $replace){
1320 $value = str_replace($search, $replace, $value);
1321 }
1322 }
1323
1324 $fields[] = new ArrayData(array(
1325 "Name" => $fieldName,
1326 "Title" => $fieldTitle,
1327 "Value" => $value,
1328 "CsvSeparator" => $this->parent->getCsvSeparator(),
1329 ));
1330 }
1331 return new DataObjectSet($fields);
1332 }
1333
1334 function Markable() {
1335 return $this->parent->Markable;
1336 }
1337
1338 /**
1339 * Checks global permissions for field in {@link TableListField->Can()}.
1340 * If they are allowed, it checks for object permissions by assuming
1341 * a method with "can" + $mode parameter naming, e.g. canDelete().
1342 *
1343 * @param string $mode See {@link TableListField::$permissions} array.
1344 * @return boolean
1345 */
1346 function Can($mode) {
1347 $canMethod = "can" . ucfirst($mode);
1348 if(!$this->parent->Can($mode)) {
1349 // check global settings for the field instance
1350 return false;
1351 } elseif($this->item->hasMethod($canMethod)) {
1352 // if global allows, check object specific permissions (e.g. canDelete())
1353 return $this->item->$canMethod();
1354 } else {
1355 // otherwise global allowed this action, so return TRUE
1356 return true;
1357 }
1358 }
1359
1360 function Link($action = null) {
1361 if($this->parent->getForm()) {
1362 $parentUrlParts = parse_url($this->parent->Link());
1363 $queryPart = (isset($parentUrlParts['query'])) ? '?' . $parentUrlParts['query'] : null;
1364 return Controller::join_links($parentUrlParts['path'], 'item', $this->item->ID, $action, $queryPart);
1365 } else {
1366 // allow for instanciation of this FormField outside of a controller/form
1367 // context (e.g. for unit tests)
1368 return false;
1369 }
1370 }
1371
1372 /**
1373 * Returns all row-based actions not disallowed through permissions.
1374 * See TableListField->Action for a similiar dummy-function to work
1375 * around template-inheritance issues.
1376 *
1377 * @return DataObjectSet
1378 */
1379 function Actions() {
1380 $allowedActions = new DataObjectSet();
1381 foreach($this->parent->actions as $actionName => $actionSettings) {
1382 if($this->parent->Can($actionName)) {
1383 $allowedActions->push(new ArrayData(array(
1384 'Name' => $actionName,
1385 'Link' => $this->{ucfirst($actionName).'Link'}(),
1386 'Icon' => $actionSettings['icon'],
1387 'IconDisabled' => $actionSettings['icon_disabled'],
1388 'Label' => $actionSettings['label'],
1389 'Class' => $actionSettings['class'],
1390 'Default' => ($actionName == $this->parent->defaultAction),
1391 'IsAllowed' => $this->Can($actionName),
1392 )));
1393 }
1394 }
1395
1396 return $allowedActions;
1397 }
1398
1399 function BaseLink() {
1400 user_error("TableListField_Item::BaseLink() deprecated, use Link() instead", E_USER_NOTICE);
1401 return $this->Link();
1402 }
1403
1404 function DeleteLink() {
1405 return Controller::join_links($this->Link(), "delete");
1406 }
1407
1408 function MarkingCheckbox() {
1409 $name = $this->parent->Name() . '[]';
1410
1411 if($this->parent->isReadonly())
1412 return "<input class=\"checkbox\" type=\"checkbox\" name=\"$name\" value=\"{$this->item->ID}\" disabled=\"disabled\" />";
1413 else
1414 return "<input class=\"checkbox\" type=\"checkbox\" name=\"$name\" value=\"{$this->item->ID}\" />";
1415 }
1416
1417 function HighlightClasses() {
1418 $classes = array();
1419 foreach($this->parent->highlightConditions as $condition) {
1420 $rule = str_replace("\$","\$this->item->", $condition['rule']);
1421 $ruleApplies = null;
1422 eval('$ruleApplies = ('.$rule.');');
1423 if($ruleApplies) {
1424 if(isset($condition['exclusive']) && $condition['exclusive']) {
1425 return $condition['class'];
1426 } else {
1427 $classes[] = $condition['class'];
1428 }
1429 }
1430 }
1431
1432 return (count($classes) > 0) ? " " . implode(" ", $classes) : false;
1433 }
1434
1435 /**
1436 * Legacy: Please use permissions instead
1437 */
1438 function isReadonly() {
1439 return $this->parent->Can('delete');
1440 }
1441 }
1442
1443 /**
1444 * @package forms
1445 * @subpackage fields-relational
1446 */
1447 class TableListField_ItemRequest extends RequestHandler {
1448 protected $ctf;
1449 protected $itemID;
1450 protected $methodName;
1451
1452 static $url_handlers = array(
1453 '$Action!' => '$Action',
1454 '' => 'index',
1455 );
1456
1457 function Link() {
1458 return Controller::join_links($this->ctf->Link(), 'item/' . $this->itemID);
1459 }
1460
1461 function __construct($ctf, $itemID) {
1462 $this->ctf = $ctf;
1463 $this->itemID = $itemID;
1464
1465 parent::__construct();
1466 }
1467
1468 function delete() {
1469 if($this->ctf->Can('delete') !== true) {
1470 return false;
1471 }
1472
1473 $this->dataObj()->delete();
1474 }
1475
1476 ///////////////////////////////////////////////////////////////////////////////////////////////////
1477
1478 /**
1479 * Return the data object being manipulated
1480 */
1481 function dataObj() {
1482 // used to discover fields if requested and for population of field
1483 if(is_numeric($this->itemID)) {
1484 // we have to use the basedataclass, otherwise we might exclude other subclasses
1485 return DataObject::get_by_id(ClassInfo::baseDataClass(Object::getCustomClass($this->ctf->sourceClass())), $this->itemID);
1486 }
1487
1488 }
1489
1490 /**
1491 * Returns the db-fieldname of the currently used has_one-relationship.
1492 */
1493 function getParentIdName( $parentClass, $childClass ) {
1494 return $this->getParentIdNameRelation( $childClass, $parentClass, 'has_one' );
1495 }
1496
1497 /**
1498 * Manually overwrites the parent-ID relations.
1499 * @see setParentClass()
1500 *
1501 * @param String $str Example: FamilyID (when one Individual has_one Family)
1502 */
1503 function setParentIdName($str) {
1504 $this->parentIdName = $str;
1505 }
1506
1507 /**
1508 * Returns the db-fieldname of the currently used relationship.
1509 */
1510 function getParentIdNameRelation($parentClass, $childClass, $relation) {
1511 if($this->parentIdName) return $this->parentIdName;
1512
1513 $relations = singleton($parentClass)->$relation();
1514 $classes = ClassInfo::ancestry($childClass);
1515 foreach($relations as $k => $v) {
1516 if(array_key_exists($v, $classes)) return $k . 'ID';
1517 }
1518 return false;
1519 }
1520
1521 /**
1522 * @return TableListField
1523 */
1524 function getParentController() {
1525 return $this->ctf;
1526 }
1527 }
1528 ?>
1529
[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the
Silverstripe Forums to ask development related questions.
-