(merged from branches/roa. use "svn log -c <changeset> -g <module-svn-path>" for detailed commit message)

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@60314 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Ingo Schommer 2008-08-11 02:25:44 +00:00
parent 2d9c833de5
commit 624540a2d0
13 changed files with 197 additions and 249 deletions

View File

@ -259,42 +259,39 @@ 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 <head> tag.
*/
static function includeInHTML($templateFilePath, $content) {
static function includeInHTML($templateFile, $content) {
if(isset($_GET['debug_profile'])) Profiler::mark("Requirements::includeInHTML");
if(strpos($content, '</head') === false) {
if(isset($_GET['debug_profile'])) Profiler::unmark("Requirements::includeInHTML");
return $content;
}
if(strpos($content, '</head') !== false && (Requirements::$javascript || Requirements::$css || Requirements::$customScript || Requirements::$customHeadTags)) {
$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 .= "<script type=\"text/javascript\" src=\"$prefix$file\"></script>\n";
if(Director::fileExists($file)) $mtimesuffix = "?m=" . filemtime(Director::baseFolder() . '/' . $file);
else $mtimesuffix = '';
$jsRequirements .= "<script type=\"text/javascript\" src=\"$prefix$file$mtimesuffix\"></script>\n";
}
}
if(self::$customScript) {
foreach(array_diff_key(self::$customScript,self::$blocked) as $script) {
$requirements .= "<script type=\"text/javascript\">\n//<![CDATA[\n";
$requirements .= "$script\n";
$requirements .= "\n//]]>\n</script>\n";
$jsRequirements .= "<script type=\"text/javascript\">\n//<![CDATA[\n";
$jsRequirements .= "$script\n";
$jsRequirements .= "\n//]]>\n</script>\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 .= "<link rel=\"stylesheet\" type=\"text/css\"{$media} href=\"$prefix$file\" />\n";
if(Director::fileExists($file)) $mtimesuffix = "?m=" . filemtime(Director::baseFolder() . '/' .$file);
else $mtimesuffix = '';
$requirements .= "<link rel=\"stylesheet\" type=\"text/css\"{$media} href=\"$prefix$file$mtimesuffix\" />\n";
}
}
foreach(array_diff_key(self::$customCSS,self::$blocked) as $css) {
@ -307,7 +304,26 @@ class Requirements {
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, '<script');
$p2 = stripos($content, '<body');
if($p1 !== false && $p1 > $p2) {
user_error("You have a script tag in the body, moving requirements to top of <body> for compatibilty. I recommend removing the script tag from your template's body.", E_USER_NOTICE);
$content = eregi_replace("(<body[^>]*>)", "\\1" . $jsRequirements, $content);
} else {
$content = eregi_replace("(</body[^>]*>)", $jsRequirements . "\\1", $content);
}
// Put CSS at the bottom of the head
return eregi_replace("(</head[^>]*>)", $requirements . "\\1", $content);
} else {
if(isset($_GET['debug_profile'])) Profiler::unmark("Requirements::includeInHTML");
return $content;
}
}
/**

View File

@ -286,6 +286,32 @@ class DataObject extends ViewableData implements DataObjectInterface {
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:
* <code>
* public function getTitle() {
* return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
* }
* </code>
*
* @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.
* This is included so that you can call $dataOrController->data() and get a DataObject all the time.
@ -2492,6 +2518,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
public static $casting = array(
"LastEdited" => "Datetime",
"Created" => "Datetime",
"Title" => 'Text',
);
/**

View File

@ -63,6 +63,14 @@ abstract class DataObjectDecorator extends Extension {
function augmentDatabase() {
}
/**
* Augment a write-record request.
*
* @param SQLQuery $manipulation Query to augment.
*/
function augmentWrite(&$manipulation) {
}
/**
* Define extra database fields

View File

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

View File

@ -182,6 +182,7 @@ class SiteTree extends DataObject {
static $searchable_fields = array(
'Title',
'Content',
);

View File

@ -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;

View File

@ -252,7 +252,7 @@ JS;
* @return Boolean
*/
function IsAddMode() {
return ($this->methodName == "add");
return ($this->methodName == "add" || $this->request->param('Action') == 'AddForm');
}
function sourceID() {
@ -458,18 +458,21 @@ JS;
$form->saveInto($childData);
$childData->write();
// 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 {
$closeLink = sprintf(
'<small><a href="' . $_SERVER['HTTP_REFERER'] . '" onclick="javascript:window.top.GB_hide(); return false;">(%s)</a></small>',
_t('ComplexTableField.CLOSEPOPUP', 'Close Popup')
);
$message = sprintf(
_t('ComplexTableField.SUCCESSADD', 'Added %s %s %s'),
$childData->singular_name(),
'<a href="' . $this->Link() . '">' . $childData->Title . '</a>',
$closeLink
);
$form->sessionMessage($message, 'good');
Director::redirectBack();
}
}
}
/**
* @todo Tie this into ComplexTableField_Item better.
@ -600,18 +603,20 @@ class ComplexTableField_ItemRequest extends RequestHandlingData {
$form->saveInto($this->dataObj());
$this->dataObj()->write();
// 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();
$closeLink = sprintf(
'<small><a href="' . $_SERVER['HTTP_REFERER'] . '" onclick="javascript:window.top.GB_hide(); return false;">(%s)</a></small>',
_t('ComplexTableField.CLOSEPOPUP', 'Close Popup')
);
$message = sprintf(
_t('ComplexTableField.SUCCESSEDIT', 'Saved %s %s %s'),
$this->dataObj()->singular_name(),
'<a href="' . $this->Link() . '">"' . $this->dataObj()->Title . '"</a>',
$closeLink
);
$form->sessionMessage($message, 'good');
} else {
Director::redirectBack();
}
}
function PopupCurrentItem() {
return $_REQUEST['ctf']['start']+1;

View File

@ -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

View File

@ -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);

View File

@ -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');

View File

@ -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) {
refresh: function(e) {
if(e) {
var el = Event.element(e);
if(el.nodeName != "a") {
var el = Event.findElement(e,"a");
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;
},

View File

@ -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}.
@ -124,6 +130,8 @@ class SearchContext extends Object {
}
}
$query->connective = $this->connective;
return $query;
}

View File

@ -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 <code>Permission::declare_permissions</code>
*
* @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