Version: current

       1 <?php
       2 /**
       3  * Provides a tabuar list in your form with view, edit and add links to edit records
       4  * with a "has-one"-relationship. Detail-views are shown in a greybox-iframe.
       5  * Features pagination in the overview as well as the detail-views.
       6  *
       7  * CAUTION: You need to make sure that the original form-call to the main controller (e.g. EditForm())
       8  * returns a form which includes this field even if no data is loaded,
       9  * to provide a "starting point" for action_callfieldmethod and ReferencedField.
      10  *
      11  * All URL data sent to and from ComplexTableField is encapsulated in $_REQUEST['ctf']
      12  * to avoid side-effects with the main controller.
      13  *
      14  * Example-URL for a "DetailForm"-call explained:
      15  * "/admin/family/?executeForm=EditForm&action_callfieldmethod&fieldName=Individual&childID=7&methodName=edit"
      16  *  - executeForm           Name of the form on the main rendering page (e.g. "FamilyAdmin")
      17  *  - action_callfieldmethod    Trigger to call a method of a single field in "EditForm" instead of rendering the whole thing
      18  *  - fieldName             Name of the targeted formField
      19  *  - methodName                Method on the formfield (e.g. "ComplexTableField")
      20  *  - childID               Identifier of the database-record (the targeted table is determined by the $sourceClass parameter)
      21  *
      22  * @todo Find a less fragile solution for accessing this field through the main controller and ReferencedField, e.g.
      23  *      build a seperate CTF-instance (doesn't necessarly have to be connected to the original by ReferencedField)
      24  * @todo Control width/height of popup by constructor (hardcoded at the moment)
      25  * @todo Integrate search from MemberTableField.php
      26  * @todo Less performance-hungry implementation of detail-view paging (don't return all items on a single view)
      27  * @todo Use automatic has-many and many-many functions to return a ComponentSet rather than building the join manually
      28  * @package forms
      29  * @subpackage fields-relational
      30  */
      31 class ComplexTableField extends TableListField {
      32
      33     /**
      34      * Determines the fields of the detail pop-up form.  It can take many forms:
      35      *  - A FieldSet object: Use that field set directly.
      36      *  - A method name, eg, 'getCMSFields': Call that method on the child object to get the fields.
      37      */
      38     protected $addTitle;
      39
      40     protected $detailFormFields;
      41
      42     protected $viewAction, $sourceJoin, $sourceItems;
      43
      44     /**
      45      * @var Controller
      46      */
      47     protected $controller;
      48
      49     /**
      50      * @var string Classname of the parent-relation to correctly link new records.
      51      */
      52     public $parentClass;
      53
      54     /**
      55      * @var string Database column name for the used relation (e.g. FamilyID
      56      * if one Family has_many Individuals).
      57      */
      58     protected $parentIdName;
      59
      60     /**
      61      * @var array Influence output without having to subclass the template.
      62      */
      63     protected $permissions = array(
      64         "add",
      65         "edit",
      66         "show",
      67         "delete",
      68         //"export",
      69     );
      70
      71     /**
      72      * Template for main rendering
      73      *
      74      * @var string
      75      */
      76     protected $template = "ComplexTableField";
      77
      78     /**
      79      * Template for popup (form rendering)
      80      *
      81      * @var string
      82      */
      83     public $templatePopup = "ComplexTableField_popup";
      84
      85     /**
      86      * Classname for each row/item
      87      *
      88      * @var string
      89      */
      90     public $itemClass = 'ComplexTableField_Item';
      91
      92     /**
      93      * Classname for the popup form
      94      *
      95      * @var string
      96      */
      97     public $popupClass = 'ComplexTableField_Popup';
      98
      99     /**
     100      * @var boolean Trigger pagination (defaults to true for ComplexTableField)
     101      */
     102     protected $showPagination = true;
     103
     104     /**
     105      * @var string Caption the popup will show (defaults to the selected action).
     106      * This is set by javascript and used by greybox.
     107      */
     108     protected $popupCaption = null;
     109
     110     /**
     111      * @var callback A function callback invoked
     112      * after initializing the popup and its base calls to
     113      * the {@link Requirements} class.
     114      */
     115     public $requirementsForPopupCallback = null;
     116
     117     /**
     118      * @var $detailFormValidator Validator
     119      */
     120     protected $detailFormValidator = null;
     121
     122     /**
     123      * Automatically detect a has-one relationship
     124      * in the popup (=child-class) and save the relation ID.
     125      *
     126      * @var boolean
     127      */
     128     protected $relationAutoSetting = true;
     129
     130     /**
     131      * Default size for the popup box
     132      */
     133     protected $popupWidth = 560;
     134     protected $popupHeight = 390;
     135
     136     public $defaultAction = 'show';
     137
     138     public $actions = array(
     139         'show' => array(
     140             'label' => 'Show',
     141             'icon' => 'cms/images/show.png',
     142             'icon_disabled' => 'cms/images/show_disabled.png',
     143             'class' => 'popuplink showlink',
     144         ),
     145         'edit' => array(
     146             'label' => 'Edit',
     147             'icon' => 'cms/images/edit.gif',
     148             'icon_disabled' => 'cms/images/edit_disabled.gif',
     149             'class' => 'popuplink editlink',
     150         ),
     151         'delete' => array(
     152             'label' => 'Delete',
     153             'icon' => 'cms/images/delete.gif',
     154             'icon_disabled' => 'cms/images/delete_disabled.gif',
     155             'class' => 'popuplink deletelink',
     156         ),
     157     );
     158
     159     static $url_handlers = array(
     160         'item/$ID' => 'handleItem',
     161         '$Action!' => '$Action',
     162     );
     163
     164     function handleItem($request) {
     165         return new ComplexTableField_ItemRequest($this, $request->param('ID'));
     166     }
     167
     168     function getViewer() {
     169         return new SSViewer($this->template);
     170     }
     171
     172     function setPopupSize($width, $height) {
     173         $width = (int)$width;
     174         $height = (int)$height;
     175
     176         if($width < 0 || $height < 0) {
     177             user_error("setPopupSize expects non-negative arguments.", E_USER_WARNING);
     178             return;
     179         }
     180
     181         $this->popupWidth = $width;
     182         $this->popupHeight = $height;
     183     }
     184
     185     function PopupWidth() {
     186         return $this->popupWidth;
     187     }
     188
     189     function PopupHeight() {
     190         return $this->popupHeight;
     191     }
     192
     193     /**
     194      * See class comments
     195      *
     196      * @param ContentController $controller
     197      * @param string $name
     198      * @param string $sourceClass
     199      * @param array $fieldList
     200      * @param FieldSet $detailFormFields
     201      * @param string $sourceFilter
     202      * @param string $sourceSort
     203      * @param string $sourceJoin
     204      */
     205     function __construct($controller, $name, $sourceClass, $fieldList = null, $detailFormFields = null, $sourceFilter = "", $sourceSort = "", $sourceJoin = "") {
     206         $this->detailFormFields = $detailFormFields;
     207         $this->controller = $controller;
     208         $this->pageSize = 10;
     209
     210         parent::__construct($name, $sourceClass, $fieldList, $sourceFilter, $sourceSort, $sourceJoin);
     211     }
     212
     213     /**
     214      * Return the record filter for this table.
     215      * It will automatically add a relation filter if relationAutoSetting is true, and it can determine an appropriate
     216      * filter.
     217      */
     218     function sourceFilter() {
     219         $sourceFilter = parent::sourceFilter();
     220
     221         if($this->relationAutoSetting
     222                 && $this->getParentClass()
     223                 && ($filterKey = $this->getParentIdName($this->getParentClass(), $this->sourceClass()))
     224                 && ($filterValue = $this->sourceID()) ) {
     225
     226             $newFilter = "\"$filterKey\" = '" . Convert::raw2sql($filterValue) . "'";
     227
     228             if($sourceFilter && is_array($sourceFilter)) {
     229                 // Note that the brackets below are taken into account when building this
     230                 $sourceFilter = implode(") AND (", $sourceFilter);
     231             }
     232
     233             $sourceFilter = $sourceFilter ? "($sourceFilter) AND ($newFilter)" : $newFilter;
     234         }
     235         return $sourceFilter;
     236     }
     237
     238     function isComposite() {
     239         return false;
     240     }
     241
     242     /**
     243      * @return String
     244      */
     245     function FieldHolder() {
     246         Requirements::javascript(THIRDPARTY_DIR . "/greybox/AmiJS.js");
     247         Requirements::javascript(THIRDPARTY_DIR . "/greybox/greybox.js");
     248         Requirements::add_i18n_javascript(SAPPHIRE_DIR . '/javascript/lang');
     249         Requirements::javascript(SAPPHIRE_DIR . '/javascript/TableListField.js');
     250         Requirements::javascript(SAPPHIRE_DIR . "/javascript/ComplexTableField.js");
     251         Requirements::css(THIRDPARTY_DIR . "/greybox/greybox.css");
     252         Requirements::css(SAPPHIRE_DIR . "/css/TableListField.css");
     253         Requirements::css(SAPPHIRE_DIR . "/css/ComplexTableField.css");
     254
     255         // set caption if required
     256         if($this->popupCaption) {
     257             $id = $this->id();
     258             if(Director::is_ajax()) {
     259             $js = <<<JS
     260 $('$id').GB_Caption = '$this->popupCaption';
     261 JS;
     262                 FormResponse::add($js);
     263             } else {
     264             $js = <<<JS
     265 Event.observe(window, 'load', function() { \$('$id').GB_Caption = '$this->popupCaption'; });
     266 JS;
     267                 Requirements::customScript($js);
     268             }
     269         }
     270
     271         // compute sourceItems here instead of Items() to ensure that
     272         // pagination and filters are respected on template accessors
     273         $this->sourceItems();
     274
     275         return $this->renderWith($this->template);
     276     }
     277
     278     function sourceClass() {
     279         return $this->sourceClass;
     280     }
     281
     282     /**
     283      * @return DataObjectSet
     284      */
     285     function Items() {
     286         $this->sourceItems = $this->sourceItems();
     287
     288         if(!$this->sourceItems) {
     289             return null;
     290         }
     291
     292         $pageStart = (isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) ? $_REQUEST['ctf'][$this->Name()]['start'] : 0;
     293         $this->sourceItems->setPageLimits($pageStart, $this->pageSize, $this->totalCount);
     294
     295         $output = new DataObjectSet();
     296         foreach($this->sourceItems as $pageIndex=>$item) {
     297             $output->push(new $this->itemClass($item, $this));
     298         }
     299         return $output;
     300     }
     301
     302     /**
     303      * Sets the popup-title by javascript. Make sure to use FormResponse in ajax-requests,
     304      * otherwise the title-change will only take effect on items existing during page-load.
     305      *
     306      * @param $caption String
     307      */
     308     function setPopupCaption($caption) {
     309         $this->popupCaption = Convert::raw2js($caption);
     310     }
     311
     312     /**
     313      * @param $validator Validator
     314      */
     315     function setDetailFormValidator( Validator $validator ) {
     316         $this->detailFormValidator = $validator;
     317     }
     318
     319     function setAddTitle($addTitle) {
     320         if(is_string($addTitle))
     321             $this->addTitle = $addTitle;
     322     }
     323
     324     function Title() {
     325         return $this->addTitle ? $this->addTitle : parent::Title();
     326     }
     327
     328     /**
     329      * Calculates the number of columns needed for colspans
     330      * used in template
     331      *
     332      * @return Int
     333      */
     334     function ItemCount() {
     335         return count($this->fieldList);
     336     }
     337
     338     /**
     339      * Used to toggle paging (makes no sense when adding a record)
     340      *
     341      * @return Boolean
     342      */
     343     function IsAddMode() {
     344         return ($this->methodName == "add" || $this->request->param('Action') == 'AddForm');
     345     }
     346
     347     function sourceID() {
     348         $idField = $this->form->dataFieldByName('ID');
     349
     350         // disabled as it conflicts with scaffolded formfields, and not strictly necessary
     351         // if(!$idField) user_error("ComplexTableField needs a formfield named 'ID' to be present", E_USER_ERROR);
     352
     353         // because action_callfieldmethod never actually loads data into the form,
     354         // we can't rely on $idField being populated, and fall back to the request-params.
     355         // this is a workaround for a bug where each subsequent popup-call didn't have ID
     356         // of the parent set, and so didn't properly save the relation
     357         return ($idField) ? $idField->Value() : (isset($_REQUEST['ctf']['ID']) ? $_REQUEST['ctf']['ID'] : null);
     358     }
     359
     360
     361
     362     function AddLink() {
     363         return Controller::join_links($this->Link(), 'add');
     364     }
     365
     366     /**
     367      * @return FieldSet
     368      */
     369     function createFieldSet() {
     370         $fieldset = new FieldSet();
     371         foreach($this->fieldTypes as $key => $fieldType){
     372             $fieldset->push(new $fieldType($key));
     373         }
     374         return $fieldset;
     375     }
     376
     377     function setController($controller) {
     378         $this->controller = $controller;
     379     }
     380
     381     /**
     382      * Determines on which relation-class the DetailForm is saved
     383      * by looking at the surrounding form-record.
     384      *
     385      * @return String
     386      */
     387     function getParentClass() {
     388         if($this->parentClass === false) {
     389             // purposely set parent-relation to false
     390             return false;
     391         } elseif(!empty($this->parentClass)) {
     392             return $this->parentClass;
     393         } elseif($this->form && $this->form->getRecord()) {
     394             return $this->form->getRecord()->ClassName;
     395         }
     396     }
     397
     398     /**
     399      * Return the record in which the CTF resides, if it exists.
     400      */
     401     function getParentRecord() {
     402         if($this->form && $record = $this->form->getRecord()) {
     403             return $record;
     404         } else {
     405             $parentID = (int)$this->sourceID();
     406             $parentClass = $this->getParentClass();
     407
     408             if($parentClass) {
     409                 if($parentID) return DataObject::get_by_id($parentClass, $parentID);
     410                 else return singleton($parentClass);
     411             }
     412         }
     413     }
     414
     415     /**
     416      * (Optional) Setter for a correct parent-relation-class.
     417      * Defaults to the record loaded into the surrounding form as a fallback.
     418      * Caution: Please use the classname, not the actual column-name in the database.
     419      *
     420      * @param $className string
     421      */
     422     function setParentClass($className) {
     423         $this->parentClass = $className;
     424     }
     425
     426     /**
     427      * Returns the db-fieldname of the currently used has_one-relationship.
     428      */
     429     function getParentIdName($parentClass, $childClass) {
     430         return $this->getParentIdNameRelation($childClass, $parentClass, 'has_one');
     431     }
     432
     433     /**
     434      * Manually overwrites the parent-ID relations.
     435      * @see setParentClass()
     436      *
     437      * @param String $str Example: FamilyID (when one Individual has_one Family)
     438      */
     439     function setParentIdName($str) {
     440         $this->parentIdName = $str;
     441     }
     442
     443     /**
     444      * Returns the db-fieldname of the currently used relationship.
     445      * Note: constructed resolve ambiguous cases in the same manner as
     446      * DataObject::getComponentJoinField()
     447      */
     448     function getParentIdNameRelation($parentClass, $childClass, $relation) {
     449         if($this->parentIdName) return $this->parentIdName;
     450
     451         $relations = array_flip(singleton($parentClass)->$relation());
     452
     453         $classes = array_reverse(ClassInfo::ancestry($childClass));
     454         foreach($classes as $class) {
     455             if(isset($relations[$class])) return $relations[$class] . 'ID'; // !!! was comment out
     456         }
     457         return false;
     458     }
     459
     460     function setTemplatePopup($template) {
     461         $this->templatePopup = $template;
     462     }
     463
     464     ////////////////////////////////////////////////////////////////////////////////////////////////////
     465
     466     /**
     467      * Return the object-specific fields for the given record, to be shown in the detail pop-up
     468      *
     469      * This won't include all the CTF-specific 'plumbing; this method is called by self::getFieldsFor()
     470      * and the result is then processed further to get the actual FieldSet for the form.
     471      *
     472      * The default implementation of this processes the value of $this->detailFormFields; consequently, if you want to
     473      * set the value of the fields to something that $this->detailFormFields doesn't allow, you can do so by overloading
     474      * this method.
     475      */
     476     function getCustomFieldsFor($childData) {
     477         if($this->detailFormFields instanceof FieldSet) {
     478             return $this->detailFormFields;
     479         }
     480
     481         $fieldsMethod = $this->detailFormFields;
     482
     483         if(!is_string($fieldsMethod)) {
     484             $this->detailFormFields = 'getCMSFields';
     485             $fieldsMethod = 'getCMSFields';
     486         }
     487
     488         if(!$childData->hasMethod($fieldsMethod)) {
     489             $fieldsMethod = 'getCMSFields';
     490         }
     491
     492         return $childData->$fieldsMethod();
     493     }
     494
     495     function getFieldsFor($childData) {
     496         $hasManyRelationName = null;
     497         $manyManyRelationName = null;
     498
     499         // See if our parent class has any many_many relations by this source class
     500         if($parentClass = $this->getParentRecord()) {
     501             $manyManyRelations = $parentClass->many_many();
     502             $manyManyRelationName = null;
     503             $manyManyComponentSet = null;
     504
     505             $hasManyRelations = $parentClass->has_many();
     506             $hasManyRelationName = null;
     507             $hasManyComponentSet = null;
     508
     509             if($manyManyRelations) foreach($manyManyRelations as $relation => $class) {
     510                 if($class == $this->sourceClass()) {
     511                     $manyManyRelationName = $relation;
     512                 }
     513             }
     514
     515             if($hasManyRelations) foreach($hasManyRelations as $relation => $class) {
     516                 if($class == $this->sourceClass()) {
     517                     $hasManyRelationName = $relation;
     518                 }
     519             }
     520         }
     521
     522         // Add the relation value to related records
     523         if(!$childData && !$childData->ID && $this->getParentClass()) {
     524             // make sure the relation-link is existing, even if we just add the sourceClass and didn't save it
     525             $parentIDName = $this->getParentIdName($this->getParentClass(), $this->sourceClass());
     526             $childData->$parentIDName = $this->sourceID();
     527         }
     528
     529         $detailFields = $this->getCustomFieldsFor($childData);
     530
     531         if($this->getParentClass() && $hasManyRelationName && $childData->ID) {
     532             $hasManyComponentSet = $parentClass->getComponents($hasManyRelationName);
     533         }
     534
     535         // the ID field confuses the Controller-logic in finding the right view for ReferencedField
     536         $detailFields->removeByName('ID');
     537
     538         // only add childID if we're not adding a record
     539         if($childData->ID) {
     540             $detailFields->push(new HiddenField('ctf[childID]', '', $childData->ID));
     541         }
     542
     543         // add a namespaced ID instead thats "converted" by saveComplexTableField()
     544         $detailFields->push(new HiddenField('ctf[ClassName]', '', $this->sourceClass()));
     545
     546         if($this->getParentClass()) {
     547             $detailFields->push(new HiddenField('ctf[parentClass]', '', $this->getParentClass()));
     548
     549             if($manyManyRelationName && $this->relationAutoSetting) {
     550                 $detailFields->push(new HiddenField('ctf[manyManyRelation]', '', $manyManyRelationName));
     551             }
     552
     553             if($hasManyRelationName && $this->relationAutoSetting) {
     554                 $detailFields->push(new HiddenField('ctf[hasManyRelation]', '', $hasManyRelationName));
     555             }
     556
     557             if($manyManyRelationName || $hasManyRelationName) {
     558                 $detailFields->push(new HiddenField('ctf[sourceID]', '', $this->sourceID()));
     559             }
     560
     561             $parentIdName = $this->getParentIdName($this->getParentClass(), $this->sourceClass());
     562
     563             if($parentIdName) {
     564                 if($this->relationAutoSetting) {
     565                     // Hack for model admin: model admin will have included a dropdown for the relation itself
     566                     $detailFields->removeByName($parentIdName);
     567                     $detailFields->push(new HiddenField($parentIdName, '', $this->sourceID()));
     568                 }
     569             }
     570         }
     571
     572         return $detailFields;
     573     }
     574
     575     function getValidatorFor($childData) {
     576         // if no custom validator is set, and there's on present on the object (e.g. Member), use it
     577         if(!isset($this->detailFormValidator) && $childData->hasMethod('getValidator')) {
     578             $this->detailFormValidator = $childData->getValidator();
     579         }
     580         return $this->detailFormValidator;
     581     }
     582
     583     ////////////////////////////////////////////////////////////////////////////////////////////////////
     584
     585     function add() {
     586         if(!$this->can('add')) return;
     587
     588         return $this->customise(array(
     589             'DetailForm' => $this->AddForm(),
     590         ))->renderWith($this->templatePopup);
     591     }
     592
     593     function AddForm($childID = null) {
     594         $className = $this->sourceClass();
     595         $childData = new $className();
     596
     597         $fields = $this->getFieldsFor($childData);
     598         $validator = $this->getValidatorFor($childData);
     599
     600         $form = new $this->popupClass(
     601             $this,
     602             'AddForm',
     603             $fields,
     604             $validator,
     605             false,
     606             $childData
     607         );
     608
     609         $form->loadDataFrom($childData);
     610
     611         return $form;
     612     }
     613
     614     /**
     615      * By default, a ComplexTableField will assume that the field name is the name of a has-many relation on the object being
     616      * edited.  It will identify the foreign key in the object being listed, and filter on that column, as well as auto-setting
     617      * that column for newly created records.
     618      *
     619      * Calling $this->setRelationAutoSetting(false) will disable this functionality.
     620      *
     621      * @param boolean $value Should the relation auto-setting functionality be enabled?
     622      */
     623     function setRelationAutoSetting($value) {
     624         $this->relationAutoSetting = $value;
     625     }
     626
     627     /**
     628      * Use the URL-Parameter "action_saveComplexTableField"
     629      * to provide a clue to the main controller if the main form has to be rendered,
     630      * even if there is no action relevant for the main controller (to provide the instance of ComplexTableField
     631      * which in turn saves the record.
     632      *
     633      * This is for adding new item records. {@link ComplexTableField_ItemRequest::saveComplexTableField()}
     634      *
     635      * @see Form::ReferencedField
     636      */
     637     function saveComplexTableField($data, $form, $params) {
     638         $className = $this->sourceClass();
     639         $childData = new $className();
     640         $form->saveInto($childData);
     641
     642         try {
     643             $childData->write();
     644         } catch(ValidationException $e) {
     645             $form->sessionMessage($e->getResult()->message(), 'bad');
     646             return Director::redirectBack();
     647         }
     648
     649         // Save the many many relationship if it's available
     650         if(isset($data['ctf']['manyManyRelation'])) {
     651             $parentRecord = DataObject::get_by_id($data['ctf']['parentClass'], (int) $data['ctf']['sourceID']);
     652             $relationName = $data['ctf']['manyManyRelation'];
     653             $componentSet = $parentRecord->getManyManyComponents($relationName);
     654             $componentSet->add($childData);
     655         }
     656
     657         if(isset($data['ctf']['hasManyRelation'])) {
     658             $parentRecord = DataObject::get_by_id($data['ctf']['parentClass'], (int) $data['ctf']['sourceID']);
     659             $relationName = $data['ctf']['hasManyRelation'];
     660
     661             $componentSet = $parentRecord->getComponents($relationName);
     662             $componentSet->add($childData);
     663         }
     664
     665         $referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null;
     666
     667         $closeLink = sprintf(
     668             '<small><a href="%s" onclick="javascript:window.top.GB_hide(); return false;">(%s)</a></small>',
     669             $referrer,
     670             _t('ComplexTableField.CLOSEPOPUP', 'Close Popup')
     671         );
     672
     673         $editLink = Controller::join_links($this->Link(), 'item/' . $childData->ID . '/edit');
     674
     675         $message = sprintf(
     676             _t('ComplexTableField.SUCCESSADD', 'Added %s %s %s'),
     677             $childData->i18n_singular_name(),
     678             '<a href="' . $editLink . '">' . $childData->Title . '</a>',
     679             $closeLink
     680         );
     681
     682         $form->sessionMessage($message, 'good');
     683
     684         Director::redirectBack();
     685     }
     686 }
     687
     688 /**
     689  * @todo Tie this into ComplexTableField_Item better.
     690  * @package forms
     691  * @subpackage fields-relational
     692  */
     693 class ComplexTableField_ItemRequest extends TableListField_ItemRequest {
     694     protected $ctf;
     695     protected $itemID;
     696     protected $methodName;
     697
     698     static $url_handlers = array(
     699         '$Action!' => '$Action',
     700         '' => 'index',
     701     );
     702
     703     function Link($action = null) {
     704         return Controller::join_links($this->ctf->Link(), '/item/', $this->itemID, $action);
     705     }
     706
     707     function index() {
     708         return $this->show();
     709     }
     710
     711     /**
     712      * Just a hook, processed in {DetailForm()}
     713      *
     714      * @return String
     715      */
     716     function show() {
     717         if($this->ctf->Can('show') !== true) {
     718             return false;
     719         }
     720
     721         $this->methodName = "show";
     722         return $this->renderWith($this->ctf->templatePopup);
     723     }
     724
     725     /**
     726      * Returns a 1-element data object set that can be used for pagination.
     727      */
     728     /* this doesn't actually work :-(
     729     function Paginator() {
     730         $paginatingSet = new DataObjectSet(array($this->dataObj()));
     731         $start = isset($_REQUEST['ctf']['start']) ? $_REQUEST['ctf']['start'] : 0;
     732         $paginatingSet->setPageLimits($start, 1, $this->ctf->TotalCount());
     733         return $paginatingSet;
     734     }
     735     */
     736
     737     /**
     738      * Just a hook, processed in {DetailForm()}
     739      *
     740      * @return String
     741      */
     742     function edit() {
     743         if($this->ctf->Can('edit') !== true) {
     744             return false;
     745         }
     746
     747         $this->methodName = "edit";
     748
     749         return $this->renderWith($this->ctf->templatePopup);
     750     }
     751
     752     function delete() {
     753         if($this->ctf->Can('delete') !== true) {
     754             return false;
     755         }
     756
     757         $this->dataObj()->delete();
     758     }
     759
     760     ///////////////////////////////////////////////////////////////////////////////////////////////////
     761
     762     /**
     763      * Return the data object being manipulated
     764      */
     765     function dataObj() {
     766         // used to discover fields if requested and for population of field
     767         if(is_numeric($this->itemID)) {
     768             // we have to use the basedataclass, otherwise we might exclude other subclasses
     769             return DataObject::get_by_id(ClassInfo::baseDataClass(Object::getCustomClass($this->ctf->sourceClass())), $this->itemID);
     770         }
     771
     772     }
     773
     774     /**
     775      * Renders view, edit and add, depending on the given information.
     776      * The form needs several parameters to function independently of its "parent-form", some derived from the context into a hidden-field,
     777      * some derived from the parent context (which is not accessible here) and delivered by GET:
     778      * ID, Identifier of the currently edited record (only if record is loaded).
     779      * <parentIDName>, Link back to the correct parent record (e.g. "parentID").
     780      * parentClass, Link back to correct container-class (the parent-record might have many 'has-one'-relationships)
     781      * CAUTION: "ID" in the DetailForm would be the "childID" in the overview table.
     782      *
     783      * @param int $childID
     784      */
     785     function DetailForm($childID = null) {
     786         $childData = $this->dataObj();
     787
     788         $fields = $this->ctf->getFieldsFor($childData);
     789         $validator = $this->ctf->getValidatorFor($childData);
     790         $readonly = ($this->methodName == "show");
     791
     792         $form = new $this->ctf->popupClass(
     793             $this,
     794             "DetailForm",
     795             $fields,
     796             $validator,
     797             $readonly,
     798             $childData
     799         );
     800
     801         $form->loadDataFrom($childData);
     802         if ($readonly) $form->makeReadonly();
     803
     804         return $form;
     805     }
     806
     807     /**
     808      * Use the URL-Parameter "action_saveComplexTableField"
     809      * to provide a clue to the main controller if the main form has to be rendered,
     810      * even if there is no action relevant for the main controller (to provide the instance of ComplexTableField
     811      * which in turn saves the record.
     812      *
     813      * This is for editing existing item records. {@link ComplexTableField::saveComplexTableField()}
     814      *
     815      * @see Form::ReferencedField
     816      */
     817     function saveComplexTableField($data, $form, $request) {
     818         $dataObject = $this->dataObj();
     819
     820         try {
     821             $form->saveInto($dataObject);
     822             $dataObject->write();
     823         } catch(ValidationException $e) {
     824             $form->sessionMessage($e->getResult()->message(), 'bad');
     825             return Director::redirectBack();
     826         }
     827
     828         // Save the many many relationship if it's available
     829         if(isset($data['ctf']['manyManyRelation'])) {
     830             $parentRecord = DataObject::get_by_id($data['ctf']['parentClass'], (int) $data['ctf']['sourceID']);
     831             $relationName = $data['ctf']['manyManyRelation'];
     832             $componentSet = $parentRecord->getManyManyComponents($relationName);
     833             $componentSet->add($dataObject);
     834         }
     835
     836         $referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null;
     837
     838         $closeLink = sprintf(
     839             '<small><a href="%s" onclick="javascript:window.top.GB_hide(); return false;">(%s)</a></small>',
     840             $referrer,
     841             _t('ComplexTableField.CLOSEPOPUP', 'Close Popup')
     842         );
     843         $message = sprintf(
     844             _t('ComplexTableField.SUCCESSEDIT', 'Saved %s %s %s'),
     845             $dataObject->i18n_singular_name(),
     846             '<a href="' . $this->Link() . '">"' . htmlspecialchars($dataObject->Title, ENT_QUOTES) . '"</a>',
     847             $closeLink
     848         );
     849
     850         $form->sessionMessage($message, 'good');
     851
     852         Director::redirectBack();
     853     }
     854
     855     function PopupCurrentItem() {
     856         return $_REQUEST['ctf']['start']+1;
     857     }
     858
     859     function PopupFirstLink() {
     860         $this->ctf->LinkToItem();
     861
     862         if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == 0) {
     863             return null;
     864         }
     865
     866         $start = 0;
     867         return Controller::join_links($this->Link(), "$this->methodName?ctf[start]={$start}");
     868     }
     869
     870     function PopupLastLink() {
     871         if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == $this->totalCount-1) {
     872             return null;
     873         }
     874
     875         $start = $this->totalCount - 1;
     876         return Controller::join_links($this->Link(), "$this->methodName?ctf[start]={$start}");
     877     }
     878
     879     function PopupNextLink() {
     880         if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == $this->totalCount-1) {
     881             return null;
     882         }
     883
     884         $start = $_REQUEST['ctf']['start'] + 1;
     885         return Controller::join_links($this->Link(), "$this->methodName?ctf[start]={$start}");
     886     }
     887
     888     function PopupPrevLink() {
     889         if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == 0) {
     890             return null;
     891         }
     892
     893         $start = $_REQUEST['ctf']['start'] - 1;
     894         return Controller::join_links($this->Link(), "$this->methodName?ctf[start]={$start}");
     895     }
     896
     897     /**
     898      * Method handles pagination in asset popup.
     899      *
     900      * @return Object DataObjectSet
     901      */
     902
     903     function Pagination() {
     904         $this->pageSize = 9;
     905         $currentItem  = $this->PopupCurrentItem();
     906         $result = new DataObjectSet();
     907         if($currentItem < 6) {
     908             $offset = 1;
     909         } elseif($this->totalCount - $currentItem <= 4) {
     910             $offset = $currentItem - (10 - ($this->totalCount - $currentItem));
     911             $offset = $offset <= 0 ? 1 : $offset;
     912         } else {
     913             $offset = $currentItem  - 5;
     914         }
     915         for($i = $offset;$i <= $offset + $this->pageSize && $i <= $this->totalCount;$i++) {
     916             $start = $i - 1;
     917             $links['link'] = Controller::join_links($this->Link() . "$this->methodName?ctf[start]={$start}");
     918             $links['number'] = $i;
     919             $links['active'] = $i == $currentItem ? false : true;
     920             $result->push(new ArrayData($links));
     921         }
     922         return $result;
     923     }
     924
     925     function ShowPagination() {
     926         return false;
     927     }
     928
     929
     930     /**
     931      * #################################
     932      *           Utility
     933      * #################################
     934      */
     935
     936     /**
     937      * Get part of class ancestry (even if popup is not subclassed it might be styled differently in css)
     938      */
     939     function PopupClasses() {
     940         global $_ALL_CLASSES;
     941
     942         $items = array();
     943         $parents = isset($_ALL_CLASSES['parents'][$this->class]) ? $_ALL_CLASSES['parents'][$this->class] : null;
     944
     945         if($parents) {
     946             foreach($parents as $parent) {
     947                 if(!in_array($parent, $_ALL_CLASSES['parents']['TableListField'])) {
     948                     $items[] = $parent . '_Popup';
     949                 }
     950             }
     951         }
     952
     953         $items[] = $this->class . '_Popup';
     954
     955         return implode(' ', $items);
     956     }
     957
     958
     959     /**
     960      * Returns the db-fieldname of the currently used has_one-relationship.
     961      */
     962     function getParentIdName($parentClass, $childClass) {
     963         return $this->getParentIdNameRelation($childClass, $parentClass, 'has_one');
     964     }
     965
     966     /**
     967      * Manually overwrites the parent-ID relations.
     968      * @see setParentClass()
     969      *
     970      * @param String $str Example: FamilyID (when one Individual has_one Family)
     971      */
     972     function setParentIdName($str) {
     973         $this->parentIdName = $str;
     974     }
     975
     976     /**
     977      * Returns the db-fieldname of the currently used relationship.
     978      */
     979     function getParentIdNameRelation($parentClass, $childClass, $relation) {
     980         if($this->parentIdName) return $this->parentIdName;
     981
     982         $relations = singleton($parentClass)->$relation();
     983         $classes = ClassInfo::ancestry($childClass);
     984         if($relations) {
     985             foreach($relations as $k => $v) {
     986                 if(array_key_exists($v, $classes)) return $k . 'ID';
     987             }
     988         }
     989         return false;
     990     }
     991
     992     function setTemplatePopup($template) {
     993         $this->templatePopup = $template;
     994     }
     995
     996
     997 }
     998
     999 /**
    1000  * Single row of a {@link ComplexTableField}.
    1001  * @package forms
    1002  * @subpackage fields-relational
    1003  */
    1004 class ComplexTableField_Item extends TableListField_Item {
    1005     function Link($action = null) {
    1006         return Controller::join_links($this->parent->Link(), '/item/', $this->item->ID, $action);
    1007     }
    1008
    1009     function EditLink() {
    1010         return Controller::join_links($this->Link(), "edit");
    1011     }
    1012
    1013     function ShowLink() {
    1014         return Controller::join_links($this->Link(), "show");
    1015     }
    1016
    1017     function DeleteLink() {
    1018         return Controller::join_links($this->Link(), "delete");
    1019     }
    1020
    1021     /**
    1022      * @param String $action
    1023      * @return boolean
    1024      */
    1025     function IsDefaultAction($action) {
    1026         return ($action == $this->parent->defaultAction);
    1027     }
    1028 }
    1029
    1030
    1031 /**
    1032  * ComplexTablefield_popup is rendered with a lightbox and can load a more
    1033  * detailed view of the source class your presenting.
    1034  * You can customise the fields and requirements as well as any
    1035  * permissions you might need.
    1036  * @package forms
    1037  * @subpackage fields-relational
    1038  */
    1039 class ComplexTableField_Popup extends Form {
    1040     protected $sourceClass;
    1041
    1042     protected $dataObject;
    1043
    1044     function __construct($controller, $name, $fields, $validator, $readonly, $dataObject) {
    1045         $this->dataObject = $dataObject;
    1046
    1047         Requirements::clear();
    1048         Requirements::unblock_all();
    1049
    1050         $actions = new FieldSet();
    1051         if(!$readonly) {
    1052             $actions->push(
    1053                 $saveAction = new FormAction(
    1054                     "saveComplexTableField",
    1055                     _t('CMSMain.SAVE')
    1056                 )
    1057             );
    1058             $saveAction->addExtraClass('save');
    1059         }
    1060
    1061         parent::__construct($controller, $name, $fields, $actions, $validator);
    1062     }
    1063
    1064     function forTemplate() {
    1065         $ret = parent::forTemplate();
    1066
    1067         /**
    1068          * WARNING: DO NOT CHANGE THE ORDER OF THESE JS FILES
    1069          * Some have special requirements.
    1070          */
    1071         Requirements::css(SAPPHIRE_DIR . '/css/Form.css');
    1072         Requirements::css(SAPPHIRE_DIR . '/css/ComplexTableField_popup.css');
    1073         Requirements::css(CMS_DIR . '/css/typography.css');
    1074         Requirements::css(CMS_DIR . '/css/cms_right.css');
    1075         Requirements::javascript(SAPPHIRE_DIR . "/thirdparty/prototype/prototype.js");
    1076         Requirements::javascript(SAPPHIRE_DIR . "/thirdparty/behaviour/behaviour.js");
    1077         Requirements::javascript(SAPPHIRE_DIR . "/javascript/prototype_improvements.js");
    1078         Requirements::javascript(THIRDPARTY_DIR . "/scriptaculous/scriptaculous.js");
    1079         Requirements::javascript(THIRDPARTY_DIR . "/scriptaculous/controls.js");
    1080         Requirements::javascript(SAPPHIRE_DIR . "/javascript/layout_helpers.js");
    1081         Requirements::add_i18n_javascript(SAPPHIRE_DIR . '/javascript/lang');
    1082         Requirements::javascript(SAPPHIRE_DIR . "/javascript/ComplexTableField_popup.js");
    1083
    1084         // Append requirements from instance callbacks
    1085         $parent = $this->getParentController();
    1086         if($parent instanceof ComplexTableField) {
    1087             $callback = $parent->requirementsForPopupCallback;
    1088         } else {
    1089             $callback = $parent->getParentController()->requirementsForPopupCallback;
    1090         }
    1091         if($callback) call_user_func($callback, $this);
    1092
    1093         // Append requirements from DataObject
    1094         // DEPRECATED 2.4 Use ComplexTableField->requirementsForPopupCallback
    1095         if($this->dataObject && $this->dataObject->hasMethod('getRequirementsForPopup')) {
    1096             $this->dataObject->getRequirementsForPopup();
    1097         }
    1098
    1099         return $ret;
    1100     }
    1101
    1102     /**
    1103      * @return ComplexTableField_ItemRequest
    1104      */
    1105     function getParentController() {
    1106         return $this->controller;
    1107     }
    1108 }
    1109
    1110 ?>
    1111

[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. -