Version: current

       1 <?php
       2
       3 class DataObjectManager extends ComplexTableField {
       4
       5     protected static $allow_assets_override = true;
       6     protected static $allow_css_override = false;
       7     protected static $popup_width = 500;
       8     protected static $confirm_delete = true;
       9
      10     protected $template = "DataObjectManager";
      11     protected $start = "0";
      12     protected $per_page = "20";
      13     protected $showAll = "0";
      14     protected $search = "";
      15     protected $filter = "";
      16     protected $sort_dir = "DESC";
      17     protected $sort = "Created";
      18     protected $filter_map = array();
      19     protected $filtered_field;
      20     protected $filter_label = "Filter results";
      21     protected $filter_empty_string = "";
      22     protected $column_widths = array();
      23     protected $per_page_map = array('10','20','30','40','50');
      24     protected $use_view_all = true;
      25     protected $popupWidth;
      26     protected $confirmDelete;
      27     public $itemClass = "DataObjectManager_Item";
      28     public $addTitle;
      29     public $singleTitle;
      30     public $hasNested = false;
      31     public $isNested = false;
      32
      33
      34     public $actions = array(
      35             'edit' => array(
      36                             'label' => 'Edit',
      37                             'icon' => null,
      38                             'class' => 'popuplink editlink',
      39             ),
      40             'delete' => array(
      41                             'label' => 'Delete',
      42                             'icon' => null,
      43                             'class' => 'deletelink',
      44             )
      45     );
      46
      47     static $url_handlers = array(
      48             'duplicate/$ID' => 'handleDuplicate'
      49     );
      50
      51
      52     public $popupClass = "DataObjectManager_Popup";
      53     public $templatePopup = "DataObjectManager_popup";
      54
      55     public static function allow_assets_override($bool) {
      56         if($bool) {
      57             DataObject::add_extension("Folder","AssetManagerFolder");
      58             SortableDataObject::add_sortable_class("File");
      59         }
      60         else
      61             DataObject::remove_extension("Folder","AssetManagerFolder");
      62     }
      63
      64     public static function allow_css_override($bool) {
      65         self::$allow_css_override = $bool;
      66     }
      67
      68     public static function set_popup_width($width) {
      69         self::$popup_width = $width;
      70     }
      71
      72     public static function set_confirm_delete($bool) {
      73         self::$confirm_delete = $bool;
      74     }
      75
      76     function __construct($controller, $name = null, $sourceClass = null, $fieldList = null, $detailFormFields = null, $sourceFilter = "", $sourceSort = "Created DESC", $sourceJoin = "") {
      77         if(!class_exists("ComplexTableField_ItemRequest"))
      78             die("<strong>"._t('DataObjectManager.ERROR','Error')."</strong>: "._t('DataObjectManager.SILVERSTRIPEVERSION','DataObjectManager requires Silverstripe version 2.3 or higher.'));
      79
      80         // If no name is given, search the has_many for the first relation.
      81         if($name === null && $sourceClass === null) {
      82             if($has_manys = $controller->stat('has_many')) {
      83                 foreach($has_manys as $relation => $value) {
      84                     $name = $relation;
      85                     $sourceClass = $value;
      86                     break;
      87                 }
      88             }
      89         }
      90         $SNG = singleton($sourceClass);
      91         if($fieldList === null) {
      92             if($fields = $SNG->summaryFields()) {
      93                 $fieldList = $fields;
      94             }
      95             else if($db = $SNG->db()) {
      96                 $fieldList = array();
      97                 foreach($db as $field => $type) {
      98                     if($field != "SortOrder")
      99                         $fieldList[$field] = DOMUtil::readable_class($field);
     100                 }
     101             }
     102         }
     103         parent::__construct($controller, $name, $sourceClass, $fieldList, $detailFormFields, $sourceFilter, $sourceSort, $sourceJoin);
     104         Requirements::css('dataobject_manager/css/dataobject_manager.css');
     105         Requirements::css('dataobject_manager/css/facebox.css');
     106         if(self::$allow_css_override)
     107             Requirements::css('dataobject_manager/css/dataobjectmanager_override.css');
     108         Requirements::javascript(THIRDPARTY_DIR.'/jquery-livequery/jquery.livequery.js');
     109         Requirements::javascript('dataobject_manager/javascript/facebox.js');
     110         Requirements::javascript('dataobject_manager/javascript/dom_jquery_ui.js');
     111         Requirements::javascript('dataobject_manager/javascript/tooltip.js');
     112         Requirements::javascript('dataobject_manager/javascript/dataobject_manager.js');
     113
     114         $this->filter_empty_string = '-- '._t('DataObjectManager.NOFILTER','No filter').' --';
     115
     116         if($this->sourceSort) {
     117             $parts = explode(" " , $this->sourceSort);
     118             if(is_array($parts) && sizeof($parts) == 2) {
     119                 list($field,$dir) = $parts;
     120                 $this->sort = trim($field);
     121                 $this->sort_dir = trim($dir);
     122             }
     123             else {
     124                 $this->sort = $this->sourceSort;
     125                 $this->sort_dir = "ASC";
     126             }
     127         }
     128
     129         if(isset($_REQUEST['ctf'][$this->Name()])) {
     130             $this->start = isset($_REQUEST['ctf'][$this->Name()]['start']) ? $_REQUEST['ctf'][$this->Name()]['start'] : 0;
     131             $this->per_page = $_REQUEST['ctf'][$this->Name()]['per_page'];
     132             $this->showAll = $_REQUEST['ctf'][$this->Name()]['showall'];
     133             $this->search = $_REQUEST['ctf'][$this->Name()]['search'];
     134             $this->filter = $_REQUEST['ctf'][$this->Name()]['filter'];
     135             $this->sort = $_REQUEST['ctf'][$this->Name()]['sort'];
     136             $this->sort_dir = $_REQUEST['ctf'][$this->Name()]['sort_dir'];
     137         }
     138
     139
     140         $this->setPageSize($this->per_page);
     141         $this->loadSort();
     142         $this->loadSourceFilter();
     143
     144         $fields = $this->getRawDetailFields(singleton($this->sourceClass()));
     145         foreach($fields as $field) {
     146             if($field instanceof DataObjectManager && !($field->controller instanceof SiteTree)) {
     147                 $this->hasNested = true;
     148                 $this->setPopupWidth(850);
     149             }
     150         }
     151
     152     }
     153
     154     public function setSourceFilter($filter) {
     155         $this->sourceFilter = $filter;
     156     }
     157
     158     public function setUseViewAll($bool) {
     159         $this->use_view_all = $bool;
     160     }
     161
     162     public function setPerPageMap($values) {
     163         $this->per_page_map = $values;
     164     }
     165
     166     public function setPluralTitle($title) {
     167         $this->pluralTitle = $title;
     168     }
     169
     170     public function setWideMode($bool) {
     171         $this->hasNested = $bool;
     172     }
     173
     174     public function PluralTitle() {
     175         return $this->pluralTitle ? $this->pluralTitle : $this->AddTitle()."";
     176     }
     177
     178
     179     protected function loadSort() {
     180         if($this->ShowAll())
     181             $this->setPageSize(999);
     182
     183         if($this->Sortable() && (!isset($_REQUEST['ctf'][$this->Name()]['sort']) || $_REQUEST['ctf'][$this->Name()]['sort'] == "SortOrder")) {
     184             $this->sort = "SortOrder";
     185             $this->sourceSort = "SortOrder ASC";
     186             $this->sort_dir = "ASC";
     187         }
     188         elseif(isset($_REQUEST['ctf'][$this->Name()]['sort'])) {
     189             $this->sourceSort = $_REQUEST['ctf'][$this->Name()]['sort'] . " " . $this->sort_dir;
     190         }
     191
     192     }
     193
     194     protected function loadSourceFilter() {
     195         $filter_string = "";
     196         if(!empty($this->filter)) {
     197             $break = strpos($this->filter, "_");
     198             $field = substr($this->filter, 0, $break);
     199             $value = substr($this->filter, $break+1, strlen($this->filter) - strlen($field));
     200             $filter_string = $field . "='$value'";
     201         }
     202
     203         $search_string = "";
     204         if(!empty($this->search)) {
     205             $search = array();
     206             $SNG = singleton($this->sourceClass);
     207             foreach(parent::Headings() as $field) {
     208                 if($SNG->hasDatabaseField($field->Name))
     209                     $search[] = "UPPER($field->Name) LIKE '%".strtoupper($this->search)."%'";
     210             }
     211             $search_string = "(".implode(" OR ", $search).")";
     212         }
     213         $and = (!empty($this->filter) && !empty($this->search)) ? " AND " : "";
     214         $source_filter = $filter_string.$and.$search_string;
     215         if(!$this->sourceFilter) $this->sourceFilter = $source_filter;
     216         else if($this->sourceFilter && !empty($source_filter)) $this->sourceFilter .= " AND " . $source_filter;
     217     }
     218
     219     public function handleItem($request) {
     220         return new DataObjectManager_ItemRequest($this, $request->param('ID'));
     221     }
     222
     223     public function getQueryString($params = array()) {
     224         $start    = isset($params['start'])? $params['start']       :   $this->start;
     225         $per_page = isset($params['per_page'])? $params['per_page'] :   $this->per_page;
     226         $show_all = isset($params['show_all'])? $params['show_all'] :   $this->showAll;
     227         $sort     = isset($params['sort'])? $params['sort']             :   $this->sort;
     228         $sort_dir = isset($params['sort_dir'])? $params['sort_dir'] :   $this->sort_dir;
     229         $filter   = isset($params['filter'])? $params['filter']       :     $this->filter;
     230         $search   = isset($params['search'])? $params['search']       :     $this->search;
     231
     232         $url = "ctf[{$this->Name()}][start]={$start}&ctf[{$this->Name()}][per_page]={$per_page}&ctf[{$this->Name()}][showall]={$show_all}&ctf[{$this->Name()}][sort]={$sort}&ctf[{$this->Name()}][sort_dir]={$sort_dir}&ctf[{$this->Name()}][search]={$search}&ctf[{$this->Name()}][filter]={$filter}";
     233         if (Translatable::get_allowed_locales()) {
     234             $url .= "&locale=" . Translatable::get_current_locale();
     235         }
     236         return $url;
     237     }
     238
     239     public function getSetting($setting) {
     240         if($this->$setting) {
     241             return $this->$setting;
     242         }
     243         return Object::get_static($this->class,DOMUtil::to_underscore($setting));
     244     }
     245
     246     function FieldHolder() {
     247         if(!$this->controller->ID && $this->isNested)
     248             return $this->renderWith('DataObjectManager_holder');
     249         return parent::FieldHolder();
     250     }
     251
     252
     253     public function Headings() {
     254         $headings = array();
     255         foreach($this->fieldList as $fieldName => $fieldTitle) {
     256             if(isset($_REQUEST['ctf'][$this->Name()]['sort_dir']))
     257                 $dir = $_REQUEST['ctf'][$this->Name()]['sort_dir'] == "ASC" ? "DESC" : "ASC";
     258             else
     259                 $dir = "ASC";
     260             $headings[] = new ArrayData(array(
     261                             "Name" => $fieldName,
     262                             "Title" => ($this->sourceClass) ? singleton($this->sourceClass)->fieldLabel($fieldTitle) : $fieldTitle,
     263                             "IsSortable" => singleton($this->sourceClass)->hasDatabaseField($fieldName),
     264                             "SortLink" => $this->RelativeLink(array(
     265                             'sort_dir' => $dir,
     266                             'sort' => $fieldName
     267                             )),
     268                             "SortDirection" => $dir,
     269                             "IsSorted" => (isset($_REQUEST['ctf'][$this->Name()]['sort'])) && ($_REQUEST['ctf'][$this->Name()]['sort'] == $fieldName),
     270                             "ColumnWidthCSS" => !empty($this->column_widths) ? sprintf("style='width:%f%%;'",($this->column_widths[$fieldName] - 0.1)) : ""
     271             ));
     272         }
     273         return new DataObjectSet($headings);
     274     }
     275
     276     function saveComplexTableField($data, $form, $params) {
     277         $className = $this->sourceClass();
     278         $childData = new $className();
     279         $form->saveInto($childData);
     280         $childData->write();
     281         if($childData->many_many()) {
     282             $form->saveInto($childData);
     283             $childData->write();
     284         }
     285         $form->sessionMessage(sprintf(_t('DataObjectManager.ADDEDNEW','Added new %s successfully'),$this->SingleTitle()), 'good');
     286
     287         if($form->getFileFields() || $form->getNestedDOMs()) {
     288             $form->clearMessage();
     289             Director::redirect(Controller::join_links($this->BaseLink(),'/item/'.$childData->ID.'/edit'));
     290         }
     291         else Director::redirectBack();
     292
     293     }
     294
     295     function sourceID() {
     296         if($this->isNested)
     297             return $this->controller->ID;
     298         $idField = $this->form->dataFieldByName('ID');
     299         return ($idField && is_numeric($idField->Value())) ? $idField->Value() : (isset($_REQUEST['ctf']['ID']) ? $_REQUEST['ctf']['ID'] : null);
     300     }
     301
     302
     303     protected function getRawDetailFields($childData) {
     304         if(is_a($this->detailFormFields,"Fieldset"))
     305             $fields = $this->detailFormFields;
     306         else {
     307             if(!is_string($this->detailFormFields)) $this->detailFormFields = "getCMSFields";
     308             $functioncall = $this->detailFormFields;
     309             if(!$childData->hasMethod($functioncall)) $functioncall = "getCMSFields";
     310
     311             $fields = $childData->$functioncall();
     312         }
     313         return $fields;
     314     }
     315
     316     public function getCustomFieldsFor($childData) {
     317         $fields = $this->getRawDetailFields($childData);
     318         foreach($fields as $field) {
     319             if($field->class == "CalendarDateField")
     320                 $fields->replaceField($field->Name(), new DatePickerField($field->Name(), $field->Title(), $field->attrValue()));
     321         }
     322         return $fields;
     323     }
     324
     325     function AddForm($childID = null) {
     326         $form = parent::AddForm($childID);
     327         $actions = new FieldSet();
     328         $titles = array();
     329         if($files = $form->getFileFields()) {
     330             foreach($files as $field)   $titles[] = DOMUtil::readable_class($field->Title());
     331         }
     332         if($doms = $form->getNestedDOMs())
     333             foreach($doms as $field) $titles[] = $field->PluralTitle();
     334         if(empty($titles))
     335             $text = _t('DataObjectManager.SAVE','Save');
     336         elseif(sizeof($titles) > 3) {
     337             $first_three = array_slice($titles,0,3);
     338             $remaining = sizeof(array_slice($titles, 4));
     339             $text = sprintf(_t('DataObjectManager.SAVEANDADD','Save and add %s'), implode(', ',$first_three));
     340             $text .= ", " . sprintf(_t('DataObjectManager.ANDOTHERCOMPONENTS','and %d other components'),$remaining);
     341         }
     342         else
     343             $text = sprintf(_t('DataObjectManager.SAVEANDADD','Save and add %s'), DOMUtil::readable_list($titles));
     344
     345         $actions->push(
     346                 $saveAction = new FormAction("saveComplexTableField", $text)
     347         );
     348         $saveAction->addExtraClass('save');
     349         $form->setActions($actions);
     350         return $form;
     351     }
     352
     353     public function Link($action = null) {
     354         return Controller::join_links(parent::Link($action),'?'.$this->getQueryString());
     355     }
     356
     357     public function BaseLink() {
     358         return parent::Link();
     359     }
     360
     361     public function CurrentLink() {
     362         return $this->Link();
     363     }
     364
     365     public function RelativeLink($params = array()) {
     366         return Controller::join_links(parent::Link(),'?'.$this->getQueryString($params));
     367     }
     368     public function FirstLink() {
     369         return parent::FirstLink() ? $this->RelativeLink(array('start' => '0')) : false;
     370     }
     371
     372     public function PrevLink() {
     373         $start = ($this->start - $this->pageSize < 0)  ? 0 : $this->start - $this->pageSize;
     374         return parent::PrevLink() ? $this->RelativeLink(array('start' => $start)) : false;
     375     }
     376
     377     public function NextLink() {
     378         $currentStart = isset($_REQUEST['ctf'][$this->Name()]['start']) ? $_REQUEST['ctf'][$this->Name()]['start'] : 0;
     379         $start = ($currentStart + $this->pageSize < $this->TotalCount()) ? $currentStart + $this->pageSize : $this->TotalCount() % $this->pageSize > 0;
     380         return parent::NextLink() ? $this->RelativeLink(array('start' => $start)) : false;
     381     }
     382
     383     public function LastLink() {
     384         $pageSize = ($this->TotalCount() % $this->pageSize > 0) ? $this->TotalCount() % $this->pageSize : $this->pageSize;
     385         $start = $this->TotalCount() - $pageSize;
     386         return parent::LastLink() ? $this->RelativeLink(array('start' => $start)) : false;
     387     }
     388
     389     public function ShowAllLink() {
     390         return $this->RelativeLink(array('show_all' => '1'));
     391     }
     392
     393     public function PaginatedLink() {
     394         return $this->RelativeLink(array('show_all' => '0'));
     395     }
     396
     397     public function AddLink() {
     398         return Controller::join_links($this->BaseLink(), '/add');
     399     }
     400
     401     public function ShowAll() {
     402         return $this->showAll == "1";
     403     }
     404
     405     public function Paginated() {
     406         return $this->showAll == "0";
     407     }
     408
     409     public function Sortable() {
     410         return SortableDataObject::is_sortable_class($this->sourceClass());
     411     }
     412
     413     public function setFilter($field, $label, $map, $default = null) {
     414         if(is_array($map)) {
     415             $this->filter_map = $map;
     416             $this->filtered_field = $field;
     417             $this->filter_label = $label;
     418         }
     419         if($default) {
     420             $this->filter = $this->filtered_field.'_'.$default;
     421             $this->loadSourceFilter();
     422         }
     423     }
     424
     425     public function HasFilter() {
     426         return !empty($this->filter_map);
     427     }
     428
     429     public function FilterDropdown() {
     430         $map = $this->filter_empty_string ? array($this->RelativeLink(array('filter' => '')) => $this->filter_empty_string) : array();
     431         foreach($this->filter_map as $k => $v) {
     432             $map[$this->RelativeLink(array('filter' => $this->filtered_field.'_'.$k))] = $v;
     433         }
     434         $value = !empty($this->filter) ? $this->RelativeLink(array('filter' => $this->filter)) : null;
     435         $dropdown = new DropdownField('Filter',$this->filter_label . " (<a href='#' class='refresh'>"._t('DataObjectManager.REFRESH','refresh')."</a>)", $map, $value);
     436         return $dropdown->FieldHolder();
     437     }
     438
     439     public function PerPageDropdown() {
     440         $map = array();
     441         foreach($this->per_page_map as $num) $map[$this->RelativeLink(array('per_page' => $num))] = $num;
     442         if($this->use_view_all)
     443             $map[$this->RelativeLink(array('per_page' => '9999'))] = _t('DataObjectManager.ALL','All');
     444         $value = !empty($this->per_page) ? $this->RelativeLink(array('per_page' => $this->per_page)) : null;
     445         return new FieldGroup(
     446                 new LabelField('show', _t('DataObjectManager.PERPAGESHOW','Show').' '),
     447                 new DropdownField('PerPage',_t('DataObjectManager.DDPERPAGESHOW'),$map, $value),
     448                 new LabelField('results', ' '._t('DataObjectManager.PERPAGERESULTS','results per page'))
     449
     450         );
     451     }
     452     public function SearchValue() {
     453         return !empty($this->search) ? $this->search : false;
     454     }
     455
     456     public function AddTitle() {
     457         return $this->addTitle ? $this->addTitle : DOMUtil::readable_class($this->Title());
     458     }
     459
     460     public function SingleTitle() {
     461         return $this->singleTitle ? $this->singleTitle : DOMUtil::readable_class($this->AddTitle());
     462     }
     463
     464     public function setAddTitle($title) {
     465         $this->addTitle = $title;
     466     }
     467
     468     public function setSingleTitle($title) {
     469         $this->singleTitle = $title;
     470     }
     471
     472     public function getColumnWidths() {
     473         return $this->column_widths;
     474     }
     475
     476     public function setColumnWidths($widths) {
     477         if(is_array($widths)) {
     478             $total = 0;
     479             foreach($widths as $name => $value) $total += $value;
     480             if($total != 100)
     481                 die('<strong>DataObjectManager::setColumnWidths()</strong>:' . sprintf(_t('DataObjectManager.TOTALNOT100','Column widths must total 100 and not %s'), $total));
     482             else
     483                 $this->column_widths = $widths;
     484         }
     485     }
     486
     487     public function setFilterEmptyString($str) {
     488         $this->filter_empty_string = $str;
     489     }
     490
     491     public function addPermission($perm) {
     492         if(!in_array($perm,$this->permissions))
     493             $this->permissions[] = $perm;
     494     }
     495
     496     public function removePermission($perm) {
     497         $key = array_search($perm,$this->permissions);
     498         if($key !== false)
     499             unset($this->permissions[$key]);
     500     }
     501
     502     public function NestedType() {
     503         if($this->hasNested)
     504             return "hasNested";
     505         else if($this->isNested)
     506             return "isNested";
     507         else
     508             return "";
     509     }
     510
     511     public function handleDuplicate($request) {
     512         return new DataObjectManager_ItemRequest($this,$request->param('ID'));
     513     }
     514
     515     public function setPopupWidth($val) {
     516         $this->popupWidth = $val;
     517     }
     518
     519     public function setConfirmDelete($bool) {
     520         $this->confirmDelete = $bool;
     521     }
     522
     523     public function PopupWidth() {
     524         return $this->getSetting('popupWidth');
     525     }
     526
     527     public function ConfirmDelete() {
     528         return $this->getSetting('confirmDelete');
     529     }
     530
     531
     532
     533 }
     534
     535 class DataObjectManager_Item extends ComplexTableField_Item {
     536     function __construct(DataObject $item, DataObjectManager $parent) {
     537         parent::__construct($item, $parent);
     538     }
     539
     540     function Link($action = NULL) {
     541         return Controller::join_links($this->parent->BaseLink(), '/item/' . $this->item->ID);
     542     }
     543
     544     function Fields($xmlSafe = true) {
     545         $fields = parent::Fields($xmlSafe);
     546         $widths = $this->parent->getColumnWidths();
     547         if(!empty($widths)) {
     548             foreach($fields as $field) {
     549                 $field->ColumnWidthCSS = sprintf("style='width:%f%%;'",($widths[$field->Name] - 0.1));
     550             }
     551         }
     552         return $fields;
     553     }
     554
     555     public function CanViewOrEdit() {
     556         return $this->parent->Can('view') || $this->parent->Can('edit');
     557     }
     558
     559     function CanDelete() {
     560         return $this->parent->Can('delete');
     561     }
     562
     563     public function ViewOrEdit() {
     564         if($this->CanViewOrEdit())
     565             return $this->parent->Can('edit') ? "edit" : "view";
     566         return false;
     567     }
     568
     569     public function ViewOrEdit_i18n() {
     570         if($res = $this->ViewOrEdit()) {
     571             return ($res == "edit") ? _t('DataObjectManager.EDIT','Edit') : _t('DataObjectManager.VIEW','View');
     572         }
     573         return null;
     574     }
     575
     576     public function EditLink() {
     577         //return $this->Link()."/edit?".$this->parent->getQueryString();
     578         return Controller::join_links($this->Link(), "edit?").$this->parent->getQueryString();
     579     }
     580
     581     public function DuplicateLink() {
     582         return Controller::join_links($this->Link(), "/duplicate");
     583     }
     584
     585     public function CustomActions() {
     586         if($this->item->hasMethod('customDOMActions'))
     587             return $this->item->customDOMActions();
     588         return false;
     589     }
     590
     591     public function PopupWidth() {
     592         return $this->parent->PopupWidth();
     593     }
     594
     595     public function Actions() {
     596         $actions = new DataObjectSet();
     597         // Inxo FIX for PHP 5.2.0
     598         // TODO: After upgrade PHP 5.2.8+ need update
     599         // Не нужно, если с 2.4 subsites заработает
     600         // В 2.4 не заработало, а из-за смены ModelAdmin еще и усугубило
     601         $permList = (array) $this->parent->permissions;
     602         foreach($permList as $perm) {
     603             $action = false;
     604             switch($perm) {
     605                 case "edit":
     606                 case "view":
     607                     $actions->push(new DataObjectManagerAction(
     608                             $this->ViewOrEdit_i18n(),
     609                             $this->EditLink(),
     610                             "popup",
     611                             "dataobject_manager/images/page_white_{$this->ViewOrEdit()}.png",
     612                             "editlink"  ,
     613                             $this->parent->PopupWidth()
     614                     ));
     615                     break;
     616
     617                 case "delete":
     618                     $actions->push(new DataObjectManagerAction(
     619                             _t('DataObjectManager.DELETE','Delete'),
     620                             $this->DeleteLink(),
     621                             "delete",
     622                             "dataobject_manager/images/trash.gif",
     623                             null,
     624                             $this->parent->getSetting('confirmDelete') ? "confirm" : null
     625                     ));
     626                     break;
     627
     628                 case "duplicate":
     629                     $actions->push(new DataObjectManagerAction(
     630                             _t('DataObjectManager.DUPLICATE','Duplicate'),
     631                             $this->DuplicateLink(),
     632                             "popup",
     633                             "dataobject_manager/images/page_copy.png",
     634                             null,
     635                             400
     636                     ));
     637                     break;
     638             }
     639         }
     640         if($custom = $this->CustomActions()) {
     641             if($custom instanceof DataObjectSet)
     642                 $actions->merge($custom);
     643             else
     644                 $actions->push($custom);
     645         }
     646         return $actions;
     647     }
     648 }
     649
     650 class DataObjectManager_Controller extends Controller {
     651     public function dosort() {
     652         if(!empty($_POST) && is_array($_POST) && isset($this->urlParams['ID'])) {
     653             $className = $this->urlParams['ID'];
     654             if(stristr($className,"-") !== false) {
     655                 list($ownerClass, $className) = explode("-",$className);
     656             }
     657             $many_many = ((is_numeric($this->urlParams['OtherID'])) && SortableDataObject::is_sortable_many_many($className));
     658             foreach($_POST as $group => $map) {
     659                 if(substr($group, 0, 7) == "record-") {
     660                     if($many_many) {
     661                         $controllerID = $this->urlParams['OtherID'];
     662                         $candidates = singleton($ownerClass)->many_many();
     663                         if(is_array($candidates)) {
     664                             foreach($candidates as $name => $class)
     665                                 if($class == $className) {
     666                                     $relationName = $name;
     667                                     break;
     668                                 }
     669                         }
     670                         if(!isset($relationName)) return false;
     671                         list($parentClass, $componentClass, $parentField, $componentField, $table) = singleton($ownerClass)->many_many($relationName);
     672                         foreach($map as $sort => $id)
     673                             DB::query("UPDATE `$table` SET SortOrder = $sort WHERE {$className}ID = $id AND {$ownerClass}ID = $controllerID");
     674                     }
     675                     else {
     676                         foreach($map as $sort => $id) {
     677                             $obj = DataObject::get_by_id($className, $id);
     678                             $obj->SortOrder = $sort;
     679                             $obj->write();
     680                         }
     681                     }
     682                     break;
     683                 }
     684             }
     685         }
     686     }
     687     public function i18n_js() {
     688         return Convert::array2json(array(
     689                 'delete_confirm' => _t('DataObjectManager.CONFIRMDELETE','Delete?')
     690         ));
     691     }
     692 }
     693
     694
     695
     696 class DataObjectManager_Popup extends Form {
     697     protected $sourceClass;
     698     protected $dataObject;
     699     public $NestedController = false;
     700
     701     function __construct($controller, $name, $fields, $validator, $readonly, $dataObject) {
     702         $this->dataObject = $dataObject;
     703         Requirements::clear();
     704         Requirements::clear_combined_files();
     705         Requirements::unblock_all();
     706         Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/prototype/prototype.js');
     707         Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/behaviour/behaviour.js');
     708         Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/jquery/jquery.js');
     709         Requirements::javascript(SAPPHIRE_DIR . '/javascript/jquery_improvements.js');
     710         Requirements::javascript(THIRDPARTY_DIR.'/jquery-livequery/jquery.livequery.js');
     711
     712         //Requirements::block(THIRDPARTY_DIR.'/behaviour.js');
     713         //Requirements::block(SAPPHIRE_DIR.'/javascript/Validator.js');
     714         //Requirements::block(THIRDPARTY_DIR.'/prototype.js');
     715         //Requirements::clear(THIRDPARTY_DIR.'/behavior.js');
     716         //Requirements::block(THIRDPARTY_DIR.'/behavior.js');
     717         Requirements::block(SAPPHIRE_DIR.'/javascript/ComplexTableField.js');
     718         Requirements::block(SAPPHIRE_DIR.'/javascript/TableListField.js');
     719         Requirements::block(THIRDPARTY_DIR.'/greybox/greybox.js');
     720         Requirements::block(THIRDPARTY_DIR.'/greybox/AmiJS.js');
     721         Requirements::block(THIRDPARTY_DIR.'/greybox/greybox.css');
     722         Requirements::block(SAPPHIRE_DIR.'/css/TableListField.css');
     723         Requirements::block(SAPPHIRE_DIR.'/css/ComplexTableField.css');
     724         Requirements::block(ASSETS_DIR.'/leftandmain.js');
     725         Requirements::block(ASSETS_DIR.'/base.js');
     726         Requirements::block(SAPPHIRE_DIR.'/javascript/lang/en_US.js');
     727         Requirements::css(SAPPHIRE_DIR . '/css/Form.css');
     728         Requirements::css(CMS_DIR . '/css/typography.css');
     729         Requirements::css(CMS_DIR . '/css/cms_right.css');
     730         Requirements::css('dataobject_manager/css/dataobject_manager.css');
     731
     732         // Append requirements from instance callbacks
     733         $parent = $controller;
     734         if($parent instanceof DataObjectManager) {
     735             $callback = $parent->requirementsForPopupCallback;
     736         } else {
     737             $callback = $parent->getParentController()->requirementsForPopupCallback;
     738         }
     739         if($callback) call_user_func($callback, $this);
     740         // DEPREDICATED 2.4
     741         if($this->dataObject->hasMethod('getRequirementsForPopup')) {
     742             $this->dataObject->getRequirementsForPopup();
     743         }
     744         Requirements::javascript('dataobject_manager/javascript/dataobjectmanager_popup.js');
     745
     746
     747         $actions = new FieldSet();
     748         if(!$readonly) {
     749             $actions->push(
     750                     $saveAction = new FormAction("saveComplexTableField", _t('DataObjectManager.SAVE','Save'))
     751
     752             );
     753             $saveAction->addExtraClass('save');
     754         }
     755
     756         parent::__construct($controller, $name, $fields, $actions, $validator);
     757         $this->unsetValidator();
     758
     759         if($this->getNestedDOMs()) {
     760             Requirements::javascript(THIRDPARTY_DIR.'/jquery-livequery/jquery.livequery.js');
     761             Requirements::javascript('dataobject_manager/javascript/dom_jquery_ui.js');
     762             Requirements::javascript('dataobject_manager/javascript/tooltip.js');
     763             Requirements::javascript('dataobject_manager/javascript/dataobject_manager.js');
     764         }
     765         $this->NestedController = $this->controller->isNested;
     766     }
     767
     768     function FieldHolder() {
     769         return $this->renderWith('ComplexTableField_Form');
     770     }
     771
     772     public function getFileFields() {
     773         $file_fields = array();
     774         foreach($this->Fields() as $field) {
     775             if($field instanceof FileIFrameField || $field instanceof ImageField)
     776                 $file_fields[] = $field;
     777         }
     778         return !empty($file_fields)? $file_fields : false;
     779     }
     780
     781     public function getNestedDOMs() {
     782         $dom_fields = array();
     783         foreach($this->Fields()->dataFields() as $field) {
     784             if($field instanceof DataObjectManager) {
     785                 $field->isNested = true;
     786                 $dom_fields[] = $field;
     787             }
     788             elseif($field instanceof CompositeField) {
     789                 if($children = $field->children) {
     790                     foreach($children as $child) {
     791                         if($child instanceof DataObjectManager) {
     792                             $child->isNested = true;
     793                             $dom_fields[] = $child;
     794                         }
     795                     }
     796                 }
     797             }
     798         }
     799         return !empty($dom_fields)? $dom_fields : false;
     800     }
     801     function getParentController() {
     802         return $this->controller;
     803     }
     804
     805 }
     806
     807
     808
     809 class DataObjectManager_ItemRequest extends ComplexTableField_ItemRequest {
     810     public $isNested = false;
     811     protected $itemList;
     812     protected $currentIndex;
     813
     814     function __construct($ctf, $itemID) {
     815         parent::__construct($ctf, $itemID);
     816         $this->isNested = $this->ctf->isNested;
     817         if($this->ctf->Items()) {
     818             $this->itemList = $this->ctf->Items()->column();
     819             $this->currentIndex = array_search($this->itemID,$this->itemList);
     820         }
     821     }
     822
     823     function Link($action = NULL) {
     824         return Controller::join_links($this->ctf->BaseLink() , '/item/' . $this->itemID);
     825     }
     826
     827     function saveComplexTableField($data, $form, $request) {
     828         $dataObject = $this->dataObj();
     829         $form->saveInto($dataObject);
     830         $dataObject->write();
     831         // Save the many many relationship if it's available
     832         if(isset($data['ctf']['manyManyRelation'])) {
     833             $parentRecord = DataObject::get_by_id($data['ctf']['parentClass'], (int) $data['ctf']['sourceID']);
     834             $relationName = $data['ctf']['manyManyRelation'];
     835             $componentSet = $parentRecord->getManyManyComponents($relationName);
     836             $componentSet->add($dataObject);
     837         }
     838
     839         $form->sessionMessage(sprintf(_t('DataObjectManager.SAVED','Saved %s successfully'),$this->ctf->SingleTitle()), 'good');
     840
     841         Director::redirectBack();
     842     }
     843
     844     function DetailForm($childID = null) {
     845         $form = parent::DetailForm($childID);
     846         $form->Fields()->insertFirst(new LiteralField('open','<div id="field-holder"><div id="fade"></div>'));
     847         $o = $form->Fields()->Last();
     848         $form->Fields()->insertAfter(new LiteralField('close','</div>'),$o->Name());
     849         if(!$this->ctf->Can('edit')) {
     850             $form->makeReadonly();
     851             $form->setActions(null);
     852         }
     853         return $form;
     854     }
     855
     856     function edit() {
     857         if(!$this->ctf->Can('view') && !$this->ctf->Can('edit'))
     858             return false;
     859
     860         $this->methodName = "edit";
     861
     862         echo $this->renderWith($this->ctf->templatePopup);
     863     }
     864
     865     public function duplicate() {
     866         if(!$this->ctf->Can('duplicate'))
     867             return false;
     868         $this->methodName = "duplicate";
     869
     870         echo $this->renderWith(array('DataObjectManager_duplicate'));
     871     }
     872
     873     public function DuplicateForm() {
     874         return new Form(
     875                 $this,
     876                 "DuplicateForm",
     877                 new FieldSet(
     878                 new FieldGroup(
     879                 new LabelField('copy',_t('DataObjectManager.CREATE','Create ')),
     880                 new NumericField('Count','','1'),
     881                 new LabelField('times',sprintf(_t('DataObjectManager.COPIESOFOBJECT',' copies of this %s'),$this->ctf->SingleTitle()))
     882                 ),
     883                 new CheckboxField('Relations',_t('DataObjectManager.INCLUDERELATIONS','Include related objects'))
     884                 ),
     885                 new FieldSet(
     886                 new FormAction('doDuplicate',_t('DataObjectManager.DUPLICATE','Duplicate'))
     887                 )
     888         );
     889     }
     890
     891     public function doDuplicate($data,$form) {
     892         if($obj = $this->dataObj()) {
     893             for($i = 0;$i < $data['Count'];$i++) {
     894                 $new = $obj->duplicate();
     895                 if(isset($data['Relations']) && $data['Relations'] == "1") {
     896                     if($has_manys = $obj->has_many()) {
     897                         foreach($has_manys as $name => $class) {
     898                             // get the owner relation name
     899                             if($has_ones = singleton($class)->has_one()) {
     900                                 if($ownerRelation = array_search($this->ctf->SourceClass(),$has_ones)) {
     901                                     $ownerID = $ownerRelation."ID";
     902                                     if($related_objects = $obj->$name()) {
     903                                         foreach($related_objects as $related_obj) {
     904                                             $o = $related_obj->duplicate(false);
     905                                             $o->$ownerID = $new->ID;
     906                                             $o->write();
     907                                         }
     908                                     }
     909                                 }
     910                                 else
     911                                     die(sprintf(_t('DataObjectManager.COULDNOTFINDRELATION','Could not find owner relation for class %s'),$this->ctf->SourceClass()));
     912                             }
     913                             else
     914                                 die(sprintf(_t('DataObjectManager.COULDNOTFINDRELATION','Could not find owner relation for class %s'),$this->ctf->SourceClass()));
     915                         }
     916                     }
     917                     if($many_manys = $obj->many_many()) {
     918                         foreach($many_manys as $name => $class) {
     919                             if($obj->$name()) {
     920                                 $new->$name()->setByIdList($obj->$name()->column());
     921                             }
     922                         }
     923                         $new->write();
     924                     }
     925                 }
     926             }
     927             $ret = "$i " . _t('DataObjectManager.DUPLICATESCREATED','duplicate(s) created');
     928             if(isset($data['Relations']) && $data['Relations'] == "1") $ret .= ", " . _t('DataObjectManager.WITHRELATIONS','with relations included');
     929             $form->sessionMessage($ret,'good');
     930         }
     931         else
     932             $form->sessionMessage(_t('DataObjectManager.ERRORDUPLICATING','There was an error duplicating the object.'),'bad');
     933         Director::redirectBack();
     934     }
     935
     936
     937     protected function getPrevID() {
     938         return $this->itemList[$this->currentIndex - 1];
     939     }
     940
     941     protected function getNextID() {
     942         return $this->itemList[$this->currentIndex + 1];
     943     }
     944
     945     function NextRecordLink() {
     946         if(!$this->itemList || $this->currentIndex == sizeof($this->itemList)-1) return false;
     947         return Controller::join_links($this->ctf->BaseLink() , '/item/' . $this->getNextID().'/edit')."?".$this->ctf->getQueryString();
     948     }
     949
     950     function PrevRecordLink() {
     951         if(!$this->itemList || $this->currentIndex == 0) return false;
     952         return Controller::join_links($this->ctf->BaseLink() , '/item/' . $this->getPrevID().'/edit')."?".$this->ctf->getQueryString();
     953     }
     954
     955     function HasPagination() {
     956         return $this->NextRecordLink() || $this->PrevRecordLink();
     957     }
     958
     959     function HasDuplicate() {
     960         return $this->ctf->Can('duplicate');
     961     }
     962
     963     function SingleTitle() {
     964         return $this->ctf->SingleTitle();
     965     }
     966
     967     function DuplicateLink() {
     968         return Controller::join_links($this->ctf->BaseLink(),'/duplicate/'.$this->itemID);
     969     }
     970
     971     function HasRelated() {
     972         $has_many = singleton($this->ctf->SourceClass())->has_many();
     973         return is_array($has_many) && !empty($has_many);
     974     }
     975
     976 }
     977
     978 class DataObjectManagerAction extends ViewableData {
     979     static $behaviour_to_js = array (
     980             'popup' => 'popup-button',
     981             'delete' => 'delete-link',
     982             'refresh' => 'refresh-button'
     983     );
     984
     985     public $Title;
     986     public $Behaviour;
     987     public $ActionClass;
     988     public $Link;
     989     public $IconURL;
     990
     991     public function __construct($title, $link, $behaviour = "popup", $icon = null, $class = null, $rel = null) {
     992         parent::__construct();
     993         $this->Title = $title;
     994         $this->Link = $link;
     995         $this->Behaviour = self::$behaviour_to_js[$behaviour];
     996         $this->IconURL = $icon;
     997         $this->ActionClass = $class;
     998         $this->Rel = $rel;
     999     }
    1000 }
    1001
    1002 class RussianUpLower {
    1003     function str_to_upper($str) {
    1004         return strtr($str,
    1005                 "abcdefghijklmnopqrstuvwxyz".
    1006                 "\xE0\xE1\xE2\xE3\xE4\xE5".
    1007                 "\xb8\xe6\xe7\xe8\xe9\xea".
    1008                 "\xeb\xeC\xeD\xeE\xeF\xf0".
    1009                 "\xf1\xf2\xf3\xf4\xf5\xf6".
    1010                 "\xf7\xf8\xf9\xfA\xfB\xfC".
    1011                 "\xfD\xfE\xfF",
    1012                 "ABCDEFGHIJKLMNOPQRSTUVWXYZ".
    1013                 "\xC0\xC1\xC2\xC3\xC4\xC5".
    1014                 "\xA8\xC6\xC7\xC8\xC9\xCA".
    1015                 "\xCB\xCC\xCD\xCE\xCF\xD0".
    1016                 "\xD1\xD2\xD3\xD4\xD5\xD6".
    1017                 "\xD7\xD8\xD9\xDA\xDB\xDC".
    1018                 "\xDD\xDE\xDF");
    1019     }
    1020
    1021     function str_to_lower($str) {
    1022         return strtr($str,
    1023                 "ABCDEFGHIJKLMNOPQRSTUVWXYZ".
    1024                 "\xC0\xC1\xC2\xC3\xC4\xC5".
    1025                 "\xA8\xC6\xC7\xC8\xC9\xCA".
    1026                 "\xCB\xCC\xCD\xCE\xCF\xD0".
    1027                 "\xD1\xD2\xD3\xD4\xD5\xD6".
    1028                 "\xD7\xD8\xD9\xDA\xDB\xDC".
    1029                 "\xDD\xDE\xDF",
    1030                 "abcdefghijklmnopqrstuvwxyz".
    1031                 "\xE0\xE1\xE2\xE3\xE4\xE5".
    1032                 "\xb8\xe6\xe7\xe8\xe9\xea".
    1033                 "\xeb\xeC\xeD\xeE\xeF\xf0".
    1034                 "\xf1\xf2\xf3\xf4\xf5\xf6".
    1035                 "\xf7\xf8\xf9\xfA\xfB\xfC".
    1036                 "\xfD\xfE\xfF");
    1037     }
    1038 }
    1039
    1040 class DOMUtil {
    1041     public static function readable_list($array) {
    1042         if(!is_array($array))
    1043             return '';
    1044         $and = _t('DataObjectManager.AND','and');
    1045         switch(count($array)) {
    1046             case 0:
    1047                 return '';
    1048             case 1:
    1049             // This may not be a normal numerically-indexed array.
    1050                 return reset($array);
    1051             case 2:
    1052                 return reset($array)." $and ".end($array);
    1053             default:
    1054                 $last = array_pop($array);
    1055                 return implode(', ', $array).", $and $last";
    1056         }
    1057     }
    1058
    1059     public static function readable_class($string) {
    1060         /*
    1061         // FIX Inxo
    1062         $c = new RussianUpLower();
    1063         $clss = $c->str_to_upper(trim($c->str_to_lower(ereg_replace('([A-Z])',' \\1',$string))));
    1064         return $clss;
    1065         */
    1066         return ucwords(trim(strtolower(preg_replace('!([A-Z])!',' \\1',$string))));
    1067     }
    1068
    1069     /**
    1070      * Translates a camel case string into a string with underscores (e.g. firstName -&gt; first_name)
    1071      * @param    string   $str    String in camel case format
    1072      * @return    string            $str Translated into underscore format
    1073      */
    1074     public static function to_underscore($str) {
    1075         $str[0] = strtolower($str[0]);
    1076         $func = function($c) { return "_" . strtolower($c[1]); };
    1077         return preg_replace_callback('/([A-Z])/', $func, $str);
    1078     }
    1079
    1080     /**
    1081      * Translates a string with underscores into camel case (e.g. first_name -&gt; firstName)
    1082      * @param    string   $str                     String in underscore format
    1083      * @param    bool     $capitalise_first_char   If true, capitalise the first char in $str
    1084      * @return   string                              $str translated into camel caps
    1085      */
    1086     public static function to_camel_case($str, $capitalise_first_char = false) {
    1087         if($capitalise_first_char) {
    1088             $str[0] = strtoupper($str[0]);
    1089         }
    1090         $func = function($c) { return strtoupper($c[1]); };
    1091         return preg_replace_callback('/_([a-z])/', $func, $str);
    1092     }
    1093
    1094 }
    1095
    1096
    1097

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