From 624540a2d0fb0b89e1a942e68f57363568da5b82 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Mon, 11 Aug 2008 02:25:44 +0000 Subject: [PATCH] (merged from branches/roa. use "svn log -c -g " for detailed commit message) git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@60314 467b73ca-7a2a-4603-9d3b-597d59a354a9 --- core/Requirements.php | 106 ++++++++++++++----------- core/model/DataObject.php | 27 +++++++ core/model/DataObjectDecorator.php | 8 ++ core/model/SQLQuery.php | 2 +- core/model/SiteTree.php | 1 + forms/CheckboxSetField.php | 2 +- forms/ComplexTableField.php | 49 ++++++------ forms/TableListField.php | 11 ++- javascript/ComplexTableField.js | 62 +++------------ javascript/ComplexTableField_popup.js | 109 -------------------------- javascript/TableListField.js | 50 ++++++++---- search/SearchContext.php | 8 ++ security/Permission.php | 11 +++ 13 files changed, 197 insertions(+), 249 deletions(-) diff --git a/core/Requirements.php b/core/Requirements.php index 6313ee7f0..829968d4a 100644 --- a/core/Requirements.php +++ b/core/Requirements.php @@ -259,55 +259,71 @@ class Requirements { * @param string $content HTML content that has already been parsed from the $templateFilePath through {@link SSViewer}. * @return string HTML content thats augumented with the requirements before the closing tag. */ - static function includeInHTML($templateFilePath, $content) { + static function includeInHTML($templateFile, $content) { if(isset($_GET['debug_profile'])) Profiler::mark("Requirements::includeInHTML"); - if(strpos($content, ' $dummy) { + if(substr($file,0,7) == 'http://' || Director::fileExists($file)) { + if(Director::fileExists($file)) $mtimesuffix = "?m=" . filemtime(Director::baseFolder() . '/' . $file); + else $mtimesuffix = ''; + $jsRequirements .= "\n"; + } + } + + if(self::$customScript) { + foreach(array_diff_key(self::$customScript,self::$blocked) as $script) { + $jsRequirements .= "\n"; + } + } + + foreach(array_diff_key(self::$css,self::$blocked) as $file => $params) { + if(Director::fileExists($file)) { + $media = (isset($params['media']) && !empty($params['media'])) ? " media=\"{$params['media']}\"" : ""; + if(Director::fileExists($file)) $mtimesuffix = "?m=" . filemtime(Director::baseFolder() . '/' .$file); + else $mtimesuffix = ''; + $requirements .= "\n"; + } + } + foreach(array_diff_key(self::$customCSS,self::$blocked) as $css) { + $requirements .= "\n"; + } + + foreach(array_diff_key(self::$customHeadTags,self::$blocked) as $customHeadTag) { + $requirements .= "$customHeadTag\n"; + } + + if(isset($_GET['debug_profile'])) Profiler::unmark("Requirements::includeInHTML"); + + + // We put script tags into the body, for performance. + // If your template already has script tags in the body, then we put our script tags at the top of the body. + // Otherwise, we put it at the bottom. + $p1 = strripos($content, ' $p2) { + user_error("You have a script tag in the body, moving requirements to top of for compatibilty. I recommend removing the script tag from your template's body.", E_USER_NOTICE); + $content = eregi_replace("(]*>)", "\\1" . $jsRequirements, $content); + } else { + $content = eregi_replace("(]*>)", $jsRequirements . "\\1", $content); + } + + // Put CSS at the bottom of the head + return eregi_replace("(]*>)", $requirements . "\\1", $content); + + } else { + if(isset($_GET['debug_profile'])) Profiler::unmark("Requirements::includeInHTML"); return $content; } - - $prefix = Director::absoluteBaseURL(); - $requirements = ''; - $jsRequirements = ''; - - // Combine files - updates Requirements::$javascript and Requirements::$css - // to remove duplicate entries - self::process_combined_files(); - - foreach(array_diff_key(self::$javascript,self::$blocked) as $file => $dummy) { - if(substr($file,0,7) == 'http://' || Director::fileExists($file)) { - $requirements .= "\n"; - } - } - - if(self::$customScript) { - foreach(array_diff_key(self::$customScript,self::$blocked) as $script) { - $requirements .= "\n"; - } - } - - $jsRequirements = $requirements; - - foreach(array_diff_key(self::$css,self::$blocked) as $file => $params) { - if(Director::fileExists($file)) { - $media = (isset($params['media']) && !empty($params['media'])) ? " media=\"{$params['media']}\"" : ""; - $requirements .= "\n"; - } - } - foreach(array_diff_key(self::$customCSS,self::$blocked) as $css) { - $requirements .= "\n"; - } - - foreach(array_diff_key(self::$customHeadTags,self::$blocked) as $customHeadTag) { - $requirements .= "$customHeadTag\n"; - } - - if(isset($_GET['debug_profile'])) Profiler::unmark("Requirements::includeInHTML"); - - return eregi_replace("(]*>)", $requirements . "\\1", $content); } /** diff --git a/core/model/DataObject.php b/core/model/DataObject.php index af9d4035a..59cecabd9 100644 --- a/core/model/DataObject.php +++ b/core/model/DataObject.php @@ -285,6 +285,32 @@ class DataObject extends ViewableData implements DataObjectInterface { $name = $this->plural_name(); return _t($this->class.'.PLURALNAME', $name); } + + /** + * Standard implementation of a title/label for a specific + * record. Tries to find properties 'Title' or 'Name', + * and falls back to the 'ID'. Useful to provide + * user-friendly identification of a record, e.g. in errormessages + * or UI-selections. + * + * Overload this method to have a more specialized implementation, + * e.g. for an Address record this could be: + * + * public function getTitle() { + * return "{$this->StreetNumber} {$this->StreetName} {$this->City}"; + * } + * + * + * @usedby {@link DataObjectSet->toDropDownMap()} + * + * @return string + */ + public function getTitle() { + if($this->hasField('Title')) return $this->getField('Title'); + if($this->hasField('Name')) return $this->getField('Name'); + + return "#{$this->ID}"; + } /** * Returns the associated database record - in this case, the object itself. @@ -2492,6 +2518,7 @@ class DataObject extends ViewableData implements DataObjectInterface { public static $casting = array( "LastEdited" => "Datetime", "Created" => "Datetime", + "Title" => 'Text', ); /** diff --git a/core/model/DataObjectDecorator.php b/core/model/DataObjectDecorator.php index 2c5aa5633..8eaba137e 100755 --- a/core/model/DataObjectDecorator.php +++ b/core/model/DataObjectDecorator.php @@ -62,6 +62,14 @@ abstract class DataObjectDecorator extends Extension { */ function augmentDatabase() { } + + /** + * Augment a write-record request. + * + * @param SQLQuery $manipulation Query to augment. + */ + function augmentWrite(&$manipulation) { + } /** diff --git a/core/model/SQLQuery.php b/core/model/SQLQuery.php index e849f3e01..1f0d54b28 100755 --- a/core/model/SQLQuery.php +++ b/core/model/SQLQuery.php @@ -69,7 +69,7 @@ class SQLQuery extends Object { * The logical connective used to join WHERE clauses. Defaults to AND. * @var string */ - private $connective = 'AND'; + public $connective = 'AND'; /** * Construct a new SQLQuery. diff --git a/core/model/SiteTree.php b/core/model/SiteTree.php index 2539292b3..179b5c357 100644 --- a/core/model/SiteTree.php +++ b/core/model/SiteTree.php @@ -182,6 +182,7 @@ class SiteTree extends DataObject { static $searchable_fields = array( 'Title', + 'Content', ); diff --git a/forms/CheckboxSetField.php b/forms/CheckboxSetField.php index 2bc0559a6..67e93c0c4 100755 --- a/forms/CheckboxSetField.php +++ b/forms/CheckboxSetField.php @@ -32,7 +32,7 @@ class CheckboxSetField extends OptionsetField { if(!$values && $record && $record->hasMethod($this->name)) { $funcName = $this->name; $join = $record->$funcName(); - foreach($join as $joinItem) $values[] = $joinItem->ID; + if($join) foreach($join as $joinItem) $values[] = $joinItem->ID; } } $source = $this->source; diff --git a/forms/ComplexTableField.php b/forms/ComplexTableField.php index 805869f29..a62de175d 100755 --- a/forms/ComplexTableField.php +++ b/forms/ComplexTableField.php @@ -252,7 +252,7 @@ JS; * @return Boolean */ function IsAddMode() { - return ($this->methodName == "add"); + return ($this->methodName == "add" || $this->request->param('Action') == 'AddForm'); } function sourceID() { @@ -457,17 +457,20 @@ JS; $childData = new $className(); $form->saveInto($childData); $childData->write(); + + $closeLink = sprintf( + '(%s)', + _t('ComplexTableField.CLOSEPOPUP', 'Close Popup') + ); + $message = sprintf( + _t('ComplexTableField.SUCCESSADD', 'Added %s %s %s'), + $childData->singular_name(), + '' . $childData->Title . '', + $closeLink + ); + $form->sessionMessage($message, 'good'); - // if ajax-call in an iframe, update window - if(Director::is_ajax()) { - // Newly saved objects need their ID reflected in the reloaded form to avoid double saving - $childRequestHandler = new ComplexTableField_ItemRequest($this, $childData->ID); - $form = $childRequestHandler->DetailForm(); - FormResponse::update_dom_id($form->FormName(), $form->formHtmlContent(), true, 'update'); - return FormResponse::respond(); - } else { - Director::redirectBack(); - } + Director::redirectBack(); } } @@ -599,18 +602,20 @@ class ComplexTableField_ItemRequest extends RequestHandlingData { function saveComplexTableField($data, $form, $request) { $form->saveInto($this->dataObj()); $this->dataObj()->write(); + + $closeLink = sprintf( + '(%s)', + _t('ComplexTableField.CLOSEPOPUP', 'Close Popup') + ); + $message = sprintf( + _t('ComplexTableField.SUCCESSEDIT', 'Saved %s %s %s'), + $this->dataObj()->singular_name(), + '"' . $this->dataObj()->Title . '"', + $closeLink + ); + $form->sessionMessage($message, 'good'); - // if ajax-call in an iframe, update window - if(Director::is_ajax()) { - // Newly saved objects need their ID reflected in the reloaded form to avoid double saving - $form = $this->DetailForm(); - //$form->loadDataFrom($this->dataObject); - FormResponse::update_dom_id($form->FormName(), $form->formHtmlContent(), true, 'update'); - return FormResponse::respond(); - - } else { - Director::redirectBack(); - } + Director::redirectBack(); } function PopupCurrentItem() { diff --git a/forms/TableListField.php b/forms/TableListField.php index 3c45730f6..c3fa1c2c8 100755 --- a/forms/TableListField.php +++ b/forms/TableListField.php @@ -681,7 +681,7 @@ JS if(!isset($_REQUEST['ctf'][$this->Name()]['start']) || !is_numeric($_REQUEST['ctf'][$this->Name()]['start']) || $_REQUEST['ctf'][$this->Name()]['start'] == 0) { return null; } - $link = $this->Link() . "/ajax_refresh?ctf[{$this->Name()}][start]={$start}"; + $link = $this->Link() . "/?ctf[{$this->Name()}][start]={$start}"; if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams); return $link; } @@ -695,7 +695,7 @@ JS $start = ($_REQUEST['ctf'][$this->Name()]['start'] - $this->pageSize < 0) ? 0 : $_REQUEST['ctf'][$this->Name()]['start'] - $this->pageSize; - $link = $this->Link() . "/ajax_refresh?ctf[{$this->Name()}][start]={$start}"; + $link = $this->Link() . "/?ctf[{$this->Name()}][start]={$start}"; if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams); return $link; } @@ -706,7 +706,7 @@ JS if($currentStart >= $start-1) { return null; } - $link = $this->Link() . "/ajax_refresh?ctf[{$this->Name()}][start]={$start}"; + $link = $this->Link() . "/?ctf[{$this->Name()}][start]={$start}"; if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams); return $link; } @@ -719,7 +719,7 @@ JS return null; } - $link = $this->Link() . "/ajax_refresh?ctf[{$this->Name()}][start]={$start}"; + $link = $this->Link() . "/?ctf[{$this->Name()}][start]={$start}"; if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams); return $link; } @@ -928,6 +928,9 @@ JS } + /** + * @deprecated Please use the standard URL through Link() which gives you the FieldHolder + */ function ajax_refresh() { // compute sourceItems here instead of Items() to ensure that // pagination and filters are respected on template accessors diff --git a/javascript/ComplexTableField.js b/javascript/ComplexTableField.js index 0793788af..0ff8e4d17 100755 --- a/javascript/ComplexTableField.js +++ b/javascript/ComplexTableField.js @@ -8,12 +8,9 @@ ComplexTableField.prototype = { popupWidth: 560, popupHeight: 390, - deleteConfirmMessage: "Are you sure you want to delete this record?", - initialize: function() { var rules = {}; rules['#'+this.id+' table.data a.popuplink'] = {onclick: this.openPopup.bind(this)}; - rules['#'+this.id+' table.data a.deletelink'] = {onclick: this.deleteRecord.bind(this)}; rules['#'+this.id+' table.data tbody td'] = {onclick: this.openPopup.bind(this)}; Behaviour.register(rules); @@ -22,49 +19,6 @@ ComplexTableField.prototype = { if(window != top) $$('#'+this.id+' table.data a.addlink').each(function(el) {Element.hide(el);}); }, - /** - * Deletes the given dataobject record via an ajax request - * to complextablefield->Delete() - * @param {Object} e - */ - deleteRecord: function(e) { - var img = Event.element(e); - var link = Event.findElement(e,"a"); - var row = Event.findElement(e,"tr"); - - // TODO ajaxErrorHandler and loading-image are dependent on cms, but formfield is in sapphire - var confirmed = (this.deleteConfirmMessage != undefined) ? confirm(this.deleteConfirmMessage) : true; - if(confirmed) - { - img.setAttribute("src",'cms/images/network-save.gif'); // TODO doesn't work - new Ajax.Request( - link.getAttribute("href"), - { - method: 'post', - postBody: 'forceajax=1' + ($('SecurityID') ? '&SecurityID=' + $('SecurityID').value : ''), - onComplete: function(){ - Effect.Fade( - row, - { - afterFinish: function(obj) { - // remove row from DOM - obj.element.parentNode.removeChild(obj.element); - // recalculate summary if needed (assumes that TableListField.js is present) - // TODO Proper inheritance - if(this._summarise) this._summarise(); - // custom callback - if(this.callback_deleteRecord) this.callback_deleteRecord(e); - }.bind(this) - } - ); - }.bind(this), - onFailure: this.ajaxErrorHandler - } - ); - } - Event.stop(e); - }, - /** * @param href, table Optional dom object (use for external triggering without an event) */ @@ -117,10 +71,6 @@ ComplexTableField.prototype = { } } - GB_OpenerObj = this; - // use same url to refresh the table after saving the popup, but use a generic rendering method - GB_RefreshLink = this.getAttribute('href'); - if(this.GB_Caption) { var title = this.GB_Caption; } else { @@ -129,7 +79,17 @@ ComplexTableField.prototype = { var title = (type && type[1]) ? type[1].ucfirst() : ""; } - GB_show(title, popupLink, this.popupHeight, this.popupWidth); + // reset internal greybox callbacks, they are not properly unregistered + // and fire multiple times on each subsequent popup close action otherwise + if(GB_ONLY_ONE) GB_ONLY_ONE.callback_fn = []; + + GB_show( + title, + popupLink, + this.popupHeight, + this.popupWidth, + this.refresh.bind(this) + ); if(e) { Event.stop(e); diff --git a/javascript/ComplexTableField_popup.js b/javascript/ComplexTableField_popup.js index f8684344d..fa104f9b0 100755 --- a/javascript/ComplexTableField_popup.js +++ b/javascript/ComplexTableField_popup.js @@ -1,119 +1,10 @@ ComplexTableFieldPopupForm = Class.create(); ComplexTableFieldPopupForm.prototype = { - errorMessage: "Error talking to server", - initialize: function() { var rules = {}; - rules["#" + this.id + " .Actions input.action"] = { - 'onclick' : this.submitForm.bind(this) - }; Behaviour.register(rules); - }, - - loadNewPage : function(content) { - this.innerHTML = content; - }, - - submitForm : function(e) { - // if custom validation implementation (extend class to implement) - if(this.validate) { - if(!this.validate()) { - Event.stop(e); - return false; - } - } - - // only do ajaxy stuff for content loaded in an iframe - if(window != top && parent.parent.GB_hide) { - var theForm = Event.findElement(e,"form"); - if(parent.parent.statusMessage != undefined) parent.parent.statusMessage('saving'); - var submitButton = document.getElementsBySelector("input.action",theForm)[0]; - if(typeof submitButton != 'undefined') { - submitButton.disabled = true; - Element.addClassName(submitButton,'loading'); - } - new parent.parent.Ajax.Request( - theForm.getAttribute("action"), - { - parameters: Form.serialize(theForm)+"&ajax=1", - onComplete: this.updateTableAfterSave.bind(this), - onFailure: this.ajaxErrorHandler.bind(this) - } - ); - Event.stop(e); - return false; - } else { - return true; - } - }, - - updateTableAfterSave : function(response) { - try { - eval(response.responseText); - } catch(er) { alert(er.message); } - - var theForm = document.getElementsByTagName("form")[0]; - - // don't update when validation is present and failed - if(!this.validate || (this.validate && !hasHadFormError())) { - new parent.parent.Ajax.Request( - parent.parent.GB_RefreshLink, - { - onComplete: this.updateAndHide.bind(parent.parent), - onFailure : this.ajaxErrorHandler - } - ); - } else { - var submitButton = document.getElementsBySelector("input.action",theForm)[0]; - if(typeof submitButton != 'undefined') { - submitButton.disabled = false; - Element.removeClassName(submitButton,'loading'); - } - } - }, - - ajaxErrorHandler: function(response) { - var submitButton = document.getElementsBySelector("input.action",theForm)[0]; - if(typeof submitButton != 'undefined') { - submitButton.disabled = false; - Element.removeClassName(submitButton,'loading'); - } - - // TODO does not work due to sandbox-iframe restrictions? - if(typeof(parent.parent.ajaxErrorHandler) == 'function') { - parent.parent.ajaxErrorHandler(); - } else { - alert(this.errorMessage); - } - }, - - updateAndHide: function(response) { - var theForm =document.getElementsByTagName("form")[0]; - - var submitButton = document.getElementsBySelector("input.action",theForm)[0]; - if(typeof submitButton != 'undefined') { - submitButton.disabled = false; - Element.removeClassName(submitButton,'loading'); - } - - onload_init_tabstrip(); - - // TODO Fix DOM-relation after pagination inside popup - if(this.GB_OpenerObj) { - // apparently firefox doesn't remember its DOM after innerHTML, so we help out here... - var cachedObj = this.GB_OpenerObj; - var cachedParentObj = this.GB_OpenerObj.parentNode; - Element.replace(this.GB_OpenerObj, response.responseText); - this.Behaviour.apply(cachedParentObj); - cachedObj = null; - this.GB_OpenerObj = null; - } - - // causes IE6 to go nuts - this.GB_hide(); - } } ComplexTableFieldPopupForm.applyTo('#ComplexTableField_Popup_DetailForm'); diff --git a/javascript/TableListField.js b/javascript/TableListField.js index e351d749b..cbf42b6b2 100755 --- a/javascript/TableListField.js +++ b/javascript/TableListField.js @@ -13,7 +13,7 @@ TableListField.prototype = { }; rules['#'+this.id+' th a'] = { - onclick: this.paginate.bind(this) + onclick: this.refresh.bind(this) }; rules['#'+this.id+' th'] = { @@ -31,7 +31,7 @@ TableListField.prototype = { } }; - rules['#'+this.id+' div.PageControls a'] = {onclick: this.paginate.bind(this)}; + rules['#'+this.id+' div.PageControls a'] = {onclick: this.refresh.bind(this)}; rules['#'+this.id+' table.data tr td.markingcheckbox'] = { onclick : function(e) { @@ -71,7 +71,9 @@ TableListField.prototype = { }, /** - * TODO Evaluate server-status before visually deleting (might have caused an error) + * Deletes the given dataobject record via an ajax request + * to complextablefield->Delete() + * @param {Object} e */ deleteRecord: function(e) { var img = Event.element(e); @@ -82,16 +84,29 @@ TableListField.prototype = { var confirmed = (this.deleteConfirmMessage != undefined) ? confirm(this.deleteConfirmMessage) : true; if(confirmed) { - img.setAttribute("src",'cms/images/network-save.gif'); // TODO doesn't work in Firefox1.5+ + img.setAttribute("src",'cms/images/network-save.gif'); // TODO doesn't work new Ajax.Request( link.getAttribute("href"), { method: 'post', postBody: 'forceajax=1' + ($('SecurityID') ? '&SecurityID=' + $('SecurityID').value : ''), onComplete: function(){ - Effect.Fade(row); + Effect.Fade( + row, + { + afterFinish: function(obj) { + // remove row from DOM + obj.element.parentNode.removeChild(obj.element); + // recalculate summary if needed (assumes that TableListField.js is present) + // TODO Proper inheritance + if(this._summarise) this._summarise(); + // custom callback + if(this.callback_deleteRecord) this.callback_deleteRecord(e); + }.bind(this) + } + ); }.bind(this), - onFailure: this.ajaxErrorHandler.bind(this) + onFailure: this.ajaxErrorHandler } ); } @@ -104,22 +119,25 @@ TableListField.prototype = { this._summarise(); }, - paginate: function(e) { - var el = Event.element(e); - - if(el.nodeName != "a") { - var el = Event.findElement(e,"a"); + refresh: function(e) { + if(e) { + var el = Event.element(e); + if(el.nodeName != "a") el = Event.findElement(e,"a"); + } else { + var el = $(this.id); } - new Ajax.Request( - el.href, + new Ajax.Updater( + $(this.id), + el.getAttribute('href'), { postBody: 'update=1', - onComplete: Ajax.Evaluator, - onFailure: this.ajaxErrorHandler.bind(this) + onComplete: function() { + Behaviour.apply($(this.id)) + }.bind(this) } ); - Event.stop(e); + if(e) Event.stop(e); return false; }, diff --git a/search/SearchContext.php b/search/SearchContext.php index 288dbafc9..53ea51e41 100644 --- a/search/SearchContext.php +++ b/search/SearchContext.php @@ -46,6 +46,12 @@ class SearchContext extends Object { */ protected $filters; + /** + * The logical connective used to join WHERE clauses. Defaults to AND. + * @var string + */ + public $connective = 'AND'; + /** * A key value pair of values that should be searched for. * The keys should match the field names specified in {@link self::$fields}. @@ -123,6 +129,8 @@ class SearchContext extends Object { } } } + + $query->connective = $this->connective; return $query; } diff --git a/security/Permission.php b/security/Permission.php index 2c16ae54f..1693e659f 100755 --- a/security/Permission.php +++ b/security/Permission.php @@ -507,6 +507,17 @@ class Permission extends DataObject { return self::$declared_permissions_list; } + /** + * Look up the human-readable title for the permission as defined by Permission::declare_permissions + * + * @param $perm Permission code + * @return Label for the given permission, or the permission itself if the label doesn't exist + */ + public static function get_label_for_permission($perm) { + $list = self::get_declared_permissions_list(); + if(array_key_exists($perm, $list)) return $list[$perm]; + return $perm; + } /** * Recursively traverse the nested list of declared permissions and create