Merge remote-tracking branch 'origin/3.0'

This commit is contained in:
Hamish Friedlander 2012-07-25 11:44:53 +12:00
commit 95d0be636c
25 changed files with 732 additions and 265 deletions

View File

@ -72,6 +72,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
'save', 'save',
'savetreenode', 'savetreenode',
'getsubtree', 'getsubtree',
'updatetreenodes',
'printable', 'printable',
'show', 'show',
'ping', 'ping',
@ -678,16 +679,8 @@ class LeftAndMain extends Controller implements PermissionProvider {
$controller = $this; $controller = $this;
$recordController = ($this->stat('tree_class') == 'SiteTree') ? singleton('CMSPageEditController') : $this; $recordController = ($this->stat('tree_class') == 'SiteTree') ? singleton('CMSPageEditController') : $this;
$titleFn = function(&$child) use(&$controller, &$recordController) { $titleFn = function(&$child) use(&$controller, &$recordController) {
$classes = $child->CMSTreeClasses(); $link = Controller::join_links($recordController->Link("show"), $child->ID);
if($controller->isCurrentPage($child)) $classes .= " current"; return LeftAndMain_TreeNode::create($child, $link, $controller->isCurrentPage($child))->forTemplate();
$flags = $child->hasMethod('getStatusFlags') ? $child->getStatusFlags() : false;
if($flags) $classes .= ' ' . implode(' ', array_keys($flags));
return "<li id=\"record-$child->ID\" data-id=\"$child->ID\" data-pagetype=\"$child->ClassName\" class=\"" . $classes . "\">" .
"<ins class=\"jstree-icon\">&nbsp;</ins>" .
"<a href=\"" . Controller::join_links($recordController->Link("show"), $child->ID) . "\" title=\"" .
_t('LeftAndMain.PAGETYPE','Page type: ') .
"$child->class\" ><ins class=\"jstree-icon\">&nbsp;</ins><span class=\"text\">" . ($child->TreeTitle).
"</span></a>";
}; };
$html = $obj->getChildrenAsUL( $html = $obj->getChildrenAsUL(
"", "",
@ -740,6 +733,45 @@ class LeftAndMain extends Controller implements PermissionProvider {
return $html; return $html;
} }
/**
* Allows requesting a view update on specific tree nodes.
* Similar to {@link getsubtree()}, but doesn't enforce loading
* all children with the node. Useful to refresh views after
* state modifications, e.g. saving a form.
*
* @return String JSON
*/
public function updatetreenodes($request) {
$data = array();
$ids = explode(',', $request->getVar('ids'));
foreach($ids as $id) {
$record = $this->getRecord($id);
$recordController = ($this->stat('tree_class') == 'SiteTree') ? singleton('CMSPageEditController') : $this;
// Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset)
// TODO: These methods should really be in hierarchy - for a start it assumes Sort exists
$next = $prev = null;
$className = $this->stat('tree_class');
$next = DataObject::get($className, 'ParentID = '.$record->ParentID.' AND Sort > '.$record->Sort)->first();
if (!$next) {
$prev = DataObject::get($className, 'ParentID = '.$record->ParentID.' AND Sort < '.$record->Sort)->reverse()->first();
}
$link = Controller::join_links($recordController->Link("show"), $record->ID);
$html = LeftAndMain_TreeNode::create($record, $link, $this->isCurrentPage($record))->forTemplate() . '</li>';
$data[$id] = array(
'html' => $html,
'ParentID' => $record->ParentID,
'NextID' => $next ? $next->ID : null,
'PrevID' => $prev ? $prev->ID : null
);
}
$this->response->addHeader('Content-Type', 'text/json');
return Convert::raw2json($data);
}
/** /**
* Save handler * Save handler
@ -1499,3 +1531,87 @@ class LeftAndMain_HTTPResponse extends SS_HTTPResponse {
} }
} }
/**
* Wrapper around objects being displayed in a tree.
* Caution: Volatile API.
*
* @todo Implement recursive tree node rendering
*/
class LeftAndMain_TreeNode extends ViewableData {
/**
* @var obj
*/
protected $obj;
/**
* @var String Edit link to the current record in the CMS
*/
protected $link;
/**
* @var Bool
*/
protected $isCurrent;
function __construct($obj, $link = null, $isCurrent = false) {
$this->obj = $obj;
$this->link = $link;
$this->isCurrent = $isCurrent;
}
/**
* Returns template, for further processing by {@link Hierarchy->getChildrenAsUL()}.
* Does not include closing tag to allow this method to inject its own children.
*
* @todo Remove hardcoded assumptions around returning an <li>, by implementing recursive tree node rendering
*
* @return String
*/
function forTemplate() {
$obj = $this->obj;
return "<li id=\"record-$obj->ID\" data-id=\"$obj->ID\" data-pagetype=\"$obj->ClassName\" class=\"" . $this->getClasses() . "\">" .
"<ins class=\"jstree-icon\">&nbsp;</ins>" .
"<a href=\"" . $this->getLink() . "\" title=\"" .
_t('LeftAndMain.PAGETYPE','Page type: ') .
"$obj->class\" ><ins class=\"jstree-icon\">&nbsp;</ins><span class=\"text\">" . ($obj->TreeTitle).
"</span></a>";
}
function getClasses() {
$classes = $this->obj->CMSTreeClasses();
if($this->isCurrent) $classes .= " current";
$flags = $this->obj->hasMethod('getStatusFlags') ? $this->obj->getStatusFlags() : false;
if($flags) $classes .= ' ' . implode(' ', array_keys($flags));
return $classes;
}
function getObj() {
return $this->obj;
}
function setObj($obj) {
$this->obj = $obj;
return $this;
}
function getLink() {
return $this->link;
}
function setLink($link) {
$this->link = $link;
return $this;
}
function getIsCurrent() {
return $this->isCurrent;
}
function setIsCurrent($bool) {
$this->isCurrent = $bool;
return $this;
}
}

View File

@ -339,11 +339,11 @@ body.cms { overflow: hidden; }
.cms-content-actions { margin: 0; padding: 12px 16px; z-index: 0; border-top: 1px solid rgba(201, 205, 206, 0.8); border-top: 1px solid #FAFAFA; -webkit-box-shadow: #cccccc 0 -1px 1px; -moz-box-shadow: #cccccc 0 -1px 1px; box-shadow: #cccccc 0 -1px 1px; } .cms-content-actions { margin: 0; padding: 12px 16px; z-index: 0; border-top: 1px solid rgba(201, 205, 206, 0.8); border-top: 1px solid #FAFAFA; -webkit-box-shadow: #cccccc 0 -1px 1px; -moz-box-shadow: #cccccc 0 -1px 1px; box-shadow: #cccccc 0 -1px 1px; }
/** -------------------------------------------- Messages -------------------------------------------- */ /** -------------------------------------------- Messages -------------------------------------------- */
.message { margin: 0 0 8px 0; padding: 7px 7px; font-weight: bold; border: 1px black solid; } .message { display: block; clear: both; margin: 0 0 8px 0; padding: 7px 7px; font-weight: bold; border: 1px black solid; }
.message.notice { background-color: #ffbe66; border-color: #ff9300; } .message.notice { background-color: #ffbe66; border-color: #ff9300; }
.message.notice a { color: #999; } .message.notice a { color: #999; }
.message.warning { background-color: #ffbe66; border-color: #ff9300; } .message.warning { background-color: #ffbe66; border-color: #ff9300; }
.message.error, .message.bad, .message.required { background-color: #ffbe66; border-color: #ff9300; } .message.error, .message.bad, .message.required, .message.validation { background-color: #ffbe66; border-color: #ff9300; }
.message.good { background-color: #65a839; background-color: rgba(101, 168, 57, 0.7); border-color: #65a839; color: #fff; text-shadow: 1px -1px 0 #1f9433; -webkit-border-radius: 3px 3px 3px 3px; -moz-border-radius: 3px 3px 3px 3px; -ms-border-radius: 3px 3px 3px 3px; -o-border-radius: 3px 3px 3px 3px; border-radius: 3px 3px 3px 3px; } .message.good { background-color: #65a839; background-color: rgba(101, 168, 57, 0.7); border-color: #65a839; color: #fff; text-shadow: 1px -1px 0 #1f9433; -webkit-border-radius: 3px 3px 3px 3px; -moz-border-radius: 3px 3px 3px 3px; -ms-border-radius: 3px 3px 3px 3px; -o-border-radius: 3px 3px 3px 3px; border-radius: 3px 3px 3px 3px; }
.message.good a { text-shadow: none; } .message.good a { text-shadow: none; }
.message p { margin: 0; } .message p { margin: 0; }

View File

@ -88,6 +88,11 @@
if(this.hasClass('validationerror')) { if(this.hasClass('validationerror')) {
// TODO validation shouldnt need a special case // TODO validation shouldnt need a special case
statusMessage(ss.i18n._t('ModelAdmin.VALIDATIONERROR', 'Validation Error'), 'bad'); statusMessage(ss.i18n._t('ModelAdmin.VALIDATIONERROR', 'Validation Error'), 'bad');
// Ensure the first validation error is visible
var firstTabWithErrors = this.find('.message.validation:first').closest('.tab');
$('.cms-container').clearCurrentTabState(); // clear state to avoid override later on
firstTabWithErrors.closest('.tabset').tabs('select', firstTabWithErrors.attr('id'));
} }
// Move navigator to preview if one is available. // Move navigator to preview if one is available.

View File

@ -10,6 +10,10 @@
Hints: null, Hints: null,
IsUpdatingTree: false,
IsLoaded: false,
onadd: function(){ onadd: function(){
this._super(); this._super();
@ -22,7 +26,6 @@
/** /**
* @todo Icon and page type hover support * @todo Icon and page type hover support
* @todo Sorting of sub nodes (originally placed in context menu) * @todo Sorting of sub nodes (originally placed in context menu)
* @todo Refresh after language <select> change (with Translatable enabled)
* @todo Automatic load of full subtree via ajax on node checkbox selection (minNodeCount = 0) * @todo Automatic load of full subtree via ajax on node checkbox selection (minNodeCount = 0)
* to avoid doing partial selection with "hidden nodes" (unloaded markup) * to avoid doing partial selection with "hidden nodes" (unloaded markup)
* @todo Disallow drag'n'drop when node has "noChildren" set (see siteTreeHints) * @todo Disallow drag'n'drop when node has "noChildren" set (see siteTreeHints)
@ -37,13 +40,12 @@
* @todo Context menu - to be replaced by a bezel UI * @todo Context menu - to be replaced by a bezel UI
* @todo Refresh form for selected tree node if affected by reordering (new parent relationship) * @todo Refresh form for selected tree node if affected by reordering (new parent relationship)
* @todo Cancel current form load via ajax when new load is requested (synchronous loading) * @todo Cancel current form load via ajax when new load is requested (synchronous loading)
* @todo When new edit form is loaded, automatically: Select matching node, set correct parent,
* update icon and title
*/ */
var self = this; var self = this;
this this
.jstree(this.getTreeConfig()) .jstree(this.getTreeConfig())
.bind('loaded.jstree', function(e, data) { .bind('loaded.jstree', function(e, data) {
self.setIsLoaded(true);
self.updateFromEditForm(); self.updateFromEditForm();
self.css('visibility', 'visible'); self.css('visibility', 'visible');
// Add ajax settings after init period to avoid unnecessary initial ajax load // Add ajax settings after init period to avoid unnecessary initial ajax load
@ -82,6 +84,8 @@
} }
}) })
.bind('move_node.jstree', function(e, data) { .bind('move_node.jstree', function(e, data) {
if(self.getIsUpdatingTree()) return;
var movedNode = data.rslt.o, newParentNode = data.rslt.np, oldParentNode = data.inst._get_parent(movedNode); var movedNode = data.rslt.o, newParentNode = data.rslt.np, oldParentNode = data.inst._get_parent(movedNode);
var siblingIDs = $.map($(movedNode).siblings().andSelf(), function(el) { var siblingIDs = $.map($(movedNode).siblings().andSelf(), function(el) {
return $(el).data('id'); return $(el).data('id');
@ -104,7 +108,7 @@
// Make some jstree events delegatable // Make some jstree events delegatable
.bind('select_node.jstree check_node.jstree uncheck_node.jstree', function(e, data) { .bind('select_node.jstree check_node.jstree uncheck_node.jstree', function(e, data) {
$(document).triggerHandler(e, data); $(document).triggerHandler(e, data);
}) });
}, },
onremove: function(){ onremove: function(){
this.jstree('destroy'); this.jstree('destroy');
@ -113,11 +117,17 @@
'from .cms-container': { 'from .cms-container': {
onafterstatechange: function(e){ onafterstatechange: function(e){
this.updateFromEditForm(e.origData); this.updateFromEditForm();
}, // No need to refresh tree nodes, we assume only form submits cause state changes
}
},
'from .cms-container form': {
onaftersubmitform: function(e){ onaftersubmitform: function(e){
this.updateFromEditForm(e.origData); var id = $('.cms-edit-form :input[name=ID]').val();
// TODO Trigger by implementing and inspecting "changed records" metadata
// sent by form submission response (as HTTP response headers)
this.updateNodesFromServer([id]);
} }
}, },
@ -216,87 +226,165 @@
getNodeByID: function(id) { getNodeByID: function(id) {
return this.find('*[data-id='+id+']'); return this.find('*[data-id='+id+']');
}, },
/**
* Creates a new node from the given HTML.
* Wrapping around jstree API because we want the flexibility to define
* the node's <li> ourselves. Places the node in the tree
* according to data.ParentID
*
* Parameters:
* (String) HTML New node content (<li>)
* (Object) Map of additional data, e.g. ParentID
* (Function) Success callback
*/
createNode: function(html, data, callback) {
var self = this,
parentNode = data.ParentID ? self.find('li[data-id='+data.ParentID+']') : false,
newNode = $(html);
this.jstree(
'create_node',
parentNode.length ? parentNode : -1,
'last',
'',
function(node) {
var origClasses = node.attr('class');
// Copy attributes
for(var i=0; i<newNode[0].attributes.length; i++){
var attr = newNode[0].attributes[i];
node.attr(attr.name, attr.value);
}
node.addClass(origClasses).html(newNode.html());
callback(node);
}
);
},
/**
* Updates a node's state in the tree,
* including all of its HTML, as well as its position.
*
* Parameters:
* (DOMElement) Existing node
* (String) HTML New node content (<li>)
* (Object) Map of additional data, e.g. ParentID
*/
updateNode: function(node, html, data) {
var self = this, newNode = $(html), origClasses = node.attr('class');
var nextNode = data.NextID ? this.find('li[data-id='+data.NextID+']') : false;
var prevNode = data.PrevID ? this.find('li[data-id='+data.PrevID+']') : false;
var parentNode = data.ParentID ? this.find('li[data-id='+data.ParentID+']') : false;
// Copy attributes. We can't replace the node completely
// without removing or detaching its children nodes.
for(var i=0; i<newNode[0].attributes.length; i++){
var attr = newNode[0].attributes[i];
node.attr(attr.name, attr.value);
}
// Replace inner content
node.addClass(origClasses).html(newNode.html());
if (nextNode && nextNode.length) {
this.jstree('move_node', node, nextNode, 'before');
}
else if (prevNode && prevNode.length) {
this.jstree('move_node', node, prevNode, 'after');
}
else {
this.jstree('move_node', node, parentNode.length ? parentNode : -1);
}
},
/** /**
* Assumes to be triggered by a form element with the following input fields: * Sets the current state based on the form the tree is managing.
* ID, ParentID, TreeTitle (or Title), ClassName.
*
* @todo Serverside node refresh, see http://open.silverstripe.org/ticket/7450
*/ */
updateFromEditForm: function(origData) { updateFromEditForm: function() {
var self = this, var node, id = $('.cms-edit-form :input[name=ID]').val();
form = $('.cms-edit-form').get(0),
id = form ? $(form.ID).val() : null,
urlEditPage = this.data('urlEditpage');
// check if a form with a valid ID exists
if(id) { if(id) {
var parentID = $(form.ParentID).val(), node = this.getNodeByID(id);
parentNode = this.find('li[data-id='+parentID+']');
node = this.find('li[data-id='+id+']'),
title = $((form.TreeTitle) ? form.TreeTitle : form.Title).val(),
className = $(form.ClassName).val();
// set title (either from TreeTitle or from Title fields)
// Treetitle has special HTML formatting to denote the status changes.
// only update immediate text element, we don't want to update all the nested ones
if(title) node.find('.text:first').html(title);
// Collect flag classes and also apply to parent
var statusFlags = [];
node.children('a').find('.badge').each(function() {
statusFlags = statusFlags.concat($(this).attr('class').replace('badge', '').split(' '));
});
// TODO Doesn't remove classes, gets too complex: Best handled through complete serverside replacement
node.addClass(statusFlags.join(' '));
// check if node exists, might have been created instead
if(!node.length && urlEditPage) {
this.jstree(
'create_node',
parentNode,
'inside',
{
data: '',
attr: {
'data-class': className,
'class': 'class-' + className,
'data-id': id
}
},
function() {
var newNode = self.find('li[data-id='+id+']');
// TODO Fix replacement of jstree-icon inside <a> tag
newNode.find('a:first').html(title).attr('href', ss.i18n.sprintf(
urlEditPage, id
));
self.jstree('deselect_all');
self.jstree('select_node', newNode);
}
);
}
if(node.length) { if(node.length) {
// set correct parent (only if it has changed)
if(parentID && parentID != node.parents('li:first').data('id')) {
this.jstree('move_node', node, parentNode.length ? parentNode : -1, 'last');
}
// Only single selection is supported on initial load
this.jstree('deselect_all');
this.jstree('select_node', node); this.jstree('select_node', node);
} else {
// If form is showing an ID that doesn't exist in the tree,
// get it from the server
this.updateNodesFromServer([id]);
} }
} else { } else {
// If no ID exists in a form view, we're displaying the tree on its own, // If no ID exists in a form view, we're displaying the tree on its own,
// hence to page should show as active // hence to page should show as active
this.jstree('deselect_all'); this.jstree('deselect_all');
if(typeof origData != 'undefined') {
var node = this.find('li[data-id='+origData.ID+']');
if(node && node.data('id') !== 0) this.jstree('delete_node', node);
}
} }
},
/**
* Reloads the view of one or more tree nodes
* from the server, ensuring that their state is up to date
* (icon, title, hierarchy, badges, etc).
* This is easier, more consistent and more extensible
* than trying to correct all aspects via DOM modifications,
* based on the sparse data available in the current edit form.
*
* Parameters:
* (Array) List of IDs to retrieve
*/
updateNodesFromServer: function(ids) {
if(this.getIsUpdatingTree() || !this.getIsLoaded()) return;
var self = this, includesNewNode = false;
this.setIsUpdatingTree(true);
// TODO 'initially_opened' config doesn't apply here
self.jstree('open_node', this.getNodeByID(0));
self.jstree('save_opened');
self.jstree('save_selected');
$.ajax({
url: this.data('urlUpdatetreenodes') + '?ids=' + ids.join(','),
dataType: 'json',
success: function(data, xhr) {
$.each(data, function(nodeId, nodeData) {
var node = self.getNodeByID(nodeId);
// If no node data is given, assume the node has been removed
if(!nodeData) {
self.jstree('delete_node', node);
return;
}
// Check if node exists, create if necessary
if(node.length) {
self.updateNode(node, nodeData.html, nodeData);
setTimeout(function() {
self.jstree('deselect_all');
self.jstree('select_node', node);
// Manually correct state, which checks for children and
// removes toggle arrow (should really be done by jstree internally)
self.jstree('correct_state', node);
}, 500);
} else {
includesNewNode = true;
self.createNode(nodeData.html, nodeData, function(newNode) {
self.jstree('deselect_all');
self.jstree('select_node', newNode);
// Manually remove toggle node, see above
self.jstree('correct_state', newNode);
});
}
});
if(!includesNewNode) {
self.jstree('deselect_all');
self.jstree('reselect');
self.jstree('reopen');
}
},
complete: function() {
self.setIsUpdatingTree(false);
}
});
} }
}); });

View File

@ -328,7 +328,7 @@ jQuery.noConflict();
* Can be hooked into an ajax 'success' callback. * Can be hooked into an ajax 'success' callback.
*/ */
handleAjaxResponse: function(data, status, xhr) { handleAjaxResponse: function(data, status, xhr) {
var self = this, url, selectedTabs; var self = this, url, selectedTabs, guessFragment;
// Pseudo-redirects via X-ControllerURL might return empty data, in which // Pseudo-redirects via X-ControllerURL might return empty data, in which
// case we'll ignore the response // case we'll ignore the response
@ -343,7 +343,9 @@ jQuery.noConflict();
newFragments = data; newFragments = data;
} else { } else {
// Fall back to replacing the content fragment if HTML is returned // Fall back to replacing the content fragment if HTML is returned
newFragments['Content'] = data; $data = $(data);
guessFragment = $data.is('form') ? 'CurrentForm' : 'Content';
newFragments[guessFragment] = $data;
} }
// Replace each fragment individually // Replace each fragment individually
@ -471,6 +473,30 @@ jQuery.noConflict();
} }
}, },
/**
* Remove any previously saved state.
*
* Parameters:
* (String) url Optional (sanitized) URL to clear a specific state.
*/
clearTabState: function(url) {
if(typeof(window.sessionStorage)=="undefined") return;
var s = window.sessionStorage;
if(url) {
s.removeItem('tabs-' + url);
} else {
for(var i=0;i<s.length;i++) s.removeItem(s.key(i));
}
},
/**
* Remove tab state for the current URL.
*/
clearCurrentTabState: function() {
this.clearTabState(this._tabStateUrl());
},
_tabStateUrl: function() { _tabStateUrl: function() {
return History.getState().url return History.getState().url
.replace(/\?.*/, '') .replace(/\?.*/, '')

View File

@ -0,0 +1,14 @@
if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
if(typeof(console) != 'undefined') console.error('Class ss.i18n not defined');
} else {
ss.i18n.addDictionary('ja_JP', {
'LeftAndMain.CONFIRMUNSAVED': "このページから移動しても良いですか?\n\n警告: あなたの変更は保存されていません.\n\n続行するにはOKを押してくださいキャンセルをクリックするとこのページにとどまります",
'LeftAndMain.CONFIRMUNSAVEDSHORT': "警告: あなたの変更は保存されていません.",
'SecurityAdmin.BATCHACTIONSDELETECONFIRM': "%sグループを本当に削除しても良いですか?",
'ModelAdmin.SAVED': "保存しました",
'ModelAdmin.REALLYDELETE': "本当に削除しますか?",
'ModelAdmin.DELETED': "削除しました",
'ModelAdmin.VALIDATIONERROR': "検証エラー",
'LeftAndMain.PAGEWASDELETED': "このページは削除されました.ページを編集するには,左から選択してください."
});
}

View File

@ -431,6 +431,8 @@ body.cms {
* -------------------------------------------- */ * -------------------------------------------- */
.message { .message {
display: block;
clear: both;
margin: 0 0 $grid-y 0; margin: 0 0 $grid-y 0;
padding: $grid-y - 1 $grid-x - 1; padding: $grid-y - 1 $grid-x - 1;
font-weight: bold; font-weight: bold;
@ -450,7 +452,7 @@ body.cms {
background-color: lighten($color-warning, 20%); background-color: lighten($color-warning, 20%);
border-color: $color-warning; border-color: $color-warning;
} }
&.error, &.bad, &.required { &.error, &.bad, &.required, &.validation {
background-color: lighten($color-error, 20%); background-color: lighten($color-error, 20%);
border-color: $color-error; border-color: $color-error;
} }

View File

@ -339,6 +339,6 @@ class RSSFeed_Entry extends ViewableData {
function AbsoluteLink() { function AbsoluteLink() {
if($this->failover->hasMethod('AbsoluteLink')) return $this->failover->AbsoluteLink(); if($this->failover->hasMethod('AbsoluteLink')) return $this->failover->AbsoluteLink();
else if($this->failover->hasMethod('Link')) return Director::absoluteURL($this->failover->Link()); else if($this->failover->hasMethod('Link')) return Director::absoluteURL($this->failover->Link());
else user_error($this->failover->class . " object has either an AbsoluteLink nor a Link method. Can't put a link in the RSS feed", E_USER_WARNING); else user_error($this->failover->class . " object has neither an AbsoluteLink nor a Link method. Can't put a link in the RSS feed", E_USER_WARNING);
} }
} }

View File

@ -14,6 +14,10 @@ class RequestProcessor {
$this->filters = $filters; $this->filters = $filters;
} }
public function setFilters($filters) {
$this->filters = $filters;
}
public function preRequest(SS_HTTPRequest $request, Session $session, DataModel $model) { public function preRequest(SS_HTTPRequest $request, Session $session, DataModel $model) {
foreach ($this->filters as $filter) { foreach ($this->filters as $filter) {
$res = $filter->preRequest($request, $session, $model); $res = $filter->preRequest($request, $session, $model);

View File

@ -195,7 +195,7 @@ extending SiteTree (or `[api:Page]`) to create a page type, we extend DataObject
} }
If we then rebuild the database ([http://localhost/db/build?flush=1](http://localhost/db/build?flush=1)), we will see If we then rebuild the database ([http://localhost/dev/build?flush=1](http://localhost/dev/build?flush=1)), we will see
that the *BrowserPollSubmission* table is created. Now we just need to define 'doBrowserPoll' on *HomePage_Controller*. that the *BrowserPollSubmission* table is created. Now we just need to define 'doBrowserPoll' on *HomePage_Controller*.
*mysite/code/HomePage.php* *mysite/code/HomePage.php*
@ -313,7 +313,7 @@ Create the function 'BrowserPollResults' on the *HomePage_Controller* class.
$list = new ArrayList(); $list = new ArrayList();
foreach($submissions->groupBy('Browser') as $browserName => $browserSubmissions) { foreach($submissions->groupBy('Browser') as $browserName => $browserSubmissions) {
$percentage = (int) ($data->Count() / $total * 100); $percentage = (int) ($browserSubmissions->Count() / $total * 100);
$list->push(new ArrayData(array( $list->push(new ArrayData(array(
'Browser' => $browserName, 'Browser' => $browserName,
'Percentage' => $percentage 'Percentage' => $percentage

View File

@ -75,21 +75,12 @@ class i18nTextCollector extends Object {
public function run($restrictToModules = null) { public function run($restrictToModules = null) {
//Debug::message("Collecting text...", false); //Debug::message("Collecting text...", false);
$modules = array(); $modules = scandir($this->basePath);
$themeFolders = array(); $themeFolders = array();
// A master string tables array (one mst per module) // A master string tables array (one mst per module)
$entitiesByModule = array(); $entitiesByModule = array();
//Search for and process existent modules, or use the passed one instead
if($restrictToModules && count($restrictToModules)) {
foreach($restrictToModules as $restrictToModule) {
$modules[] = basename($restrictToModule);
}
} else {
$modules = scandir($this->basePath);
}
foreach($modules as $index => $module){ foreach($modules as $index => $module){
if($module != 'themes') continue; if($module != 'themes') continue;
else { else {
@ -145,6 +136,13 @@ class i18nTextCollector extends Object {
} }
} }
// Restrict modules we update to just the specified ones (if any passed)
if($restrictToModules && count($restrictToModules)) {
foreach (array_diff(array_keys($entitiesByModule), $restrictToModules) as $module) {
unset($entitiesByModule[$module]);
}
}
// Write each module language file // Write each module language file
if($entitiesByModule) foreach($entitiesByModule as $module => $entities) { if($entitiesByModule) foreach($entitiesByModule as $module => $entities) {
$this->getWriter()->write($entities, $this->defaultLocale, $this->baseSavePath . '/' . $module); $this->getWriter()->write($entities, $this->defaultLocale, $this->baseSavePath . '/' . $module);

View File

@ -102,10 +102,13 @@
this.fileupload($.extend(true, this.fileupload($.extend(true,
{ {
formData: function(form) { formData: function(form) {
var idVal = $(form).find(':input[name=ID]').val();
if(!idVal) {
idVal = 0;
}
return [ return [
{name: 'SecurityID', value: $(form).find(':input[name=SecurityID]').val()}, {name: 'SecurityID', value: $(form).find(':input[name=SecurityID]').val()},
{name: 'ID', value: $(form).find(':input[name=ID]').val()} {name: 'ID', value: idVal}
]; ];
}, },
errorMessages: { errorMessages: {

39
javascript/lang/ja_JP.js Normal file
View File

@ -0,0 +1,39 @@
if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
if(typeof(console) != 'undefined') console.error('Class ss.i18n not defined');
} else {
ss.i18n.addDictionary('ja_JP', {
'VALIDATOR.FIELDREQUIRED': '"%s"を入力してください,必須項目です.',
'HASMANYFILEFIELD.UPLOADING': 'アップロード中です... %s',
'TABLEFIELD.DELETECONFIRMMESSAGE': 'このレコードを本当に削除しますか?',
'LOADING': '読み込み中...',
'UNIQUEFIELD.SUGGESTED': "'%s'へ値を変更しました : %s",
'UNIQUEFIELD.ENTERNEWVALUE': 'このフィールドに新しい値を入力する必要があります.',
'UNIQUEFIELD.CANNOTLEAVEEMPTY': 'このフィールドは空にすることができません.',
'RESTRICTEDTEXTFIELD.CHARCANTBEUSED': "文字'%s'はこのフィールドでは利用することができません.",
'UPDATEURL.CONFIRM': 'URLを次へ変更しますか?:\n\n%s/\n\nOKをクリックするとURLが変更されますキャンセルをクリックするとURLは保持されます:\n\n%s',
'UPDATEURL.CONFIRMURLCHANGED':'URLは次へ変更されました\n"%s"',
'FILEIFRAMEFIELD.DELETEFILE': 'ファイルを削除',
'FILEIFRAMEFIELD.UNATTACHFILE': 'Un-Attach File',
'FILEIFRAMEFIELD.DELETEIMAGE': '画像を削除',
'FILEIFRAMEFIELD.CONFIRMDELETE': 'このファイルを本当に削除しても良いですか?',
'LeftAndMain.IncompatBrowserWarning': 'ご利用のブラウザはCMSのインターフェイスと互換性がありませんInternet Explorer 7以上, Google Chrome 10以上またはMozilla Firefox 3.5以上をご利用ください',
'GRIDFIELD.ERRORINTRANSACTION': 'サーバーからデータを取得中にエラーが発生しました.\n 後ほど改めてお試しください.',
'UploadField.ConfirmDelete': 'サーバーのファイルシステムからこのファイルを本当に削除しても良いですか?',
'UploadField.PHP_MAXFILESIZE': 'upload_max_filesize(最大アップロードファイルサイズ)をファイルが超えています.(php.iniで指定されています)',
'UploadField.HTML_MAXFILESIZE': 'MAX_FILE_SIZE(最大ファイルサイズ)をファイルが超えています.(HTMLフォームで指定されています)',
'UploadField.ONLYPARTIALUPLOADED': 'ファイルは部分的にアップロードされました.',
'UploadField.NOFILEUPLOADED': 'ファイルはアップロードされませんでした.',
'UploadField.NOTMPFOLDER': '一時フォルダがありません.',
'UploadField.WRITEFAILED': 'ディスクへのファイル書き込みに失敗しました.',
'UploadField.STOPEDBYEXTENSION': '拡張子によりファイルアップロードが停止しました.',
'UploadField.TOOLARGE': 'ファイルサイズが大きすぎます.',
'UploadField.TOOSMALL': 'ファイルサイズが小さすぎます.',
'UploadField.INVALIDEXTENSION': '拡張子は許可されていません.',
'UploadField.MAXNUMBEROFFILESSIMPLE': 'ファイルの最大数を超えました.',
'UploadField.UPLOADEDBYTES': 'アップロードされたバイトはファイルサイズを超えました.',
'UploadField.EMPTYRESULT': 'Empty file upload result',
'UploadField.LOADING': '読み込み中...',
'UploadField.Editing': '編集中...',
'UploadField.Uploaded': 'アップロードしました.'
});
}

View File

@ -300,9 +300,11 @@ class ArrayList extends ViewableData implements SS_List, SS_Filterable, SS_Sorta
* @return ArrayList * @return ArrayList
*/ */
public function reverse() { public function reverse() {
$this->items = array_reverse($this->items); // TODO 3.1: This currently mutates existing array
$list = /* clone */ $this;
$list->items = array_reverse($this->items);
return $this; return $list;
} }
/** /**
@ -362,11 +364,13 @@ class ArrayList extends ViewableData implements SS_List, SS_Filterable, SS_Sorta
// First argument is the direction to be sorted, // First argument is the direction to be sorted,
$multisortArgs[] = &$sortDirection[$column]; $multisortArgs[] = &$sortDirection[$column];
} }
// As the last argument we pass in a reference to the items that all the sorting will be
// applied upon // TODO 3.1: This currently mutates existing array
$multisortArgs[] = &$this->items; $list = /* clone */ $this;
// As the last argument we pass in a reference to the items that all the sorting will be applied upon
$multisortArgs[] = &$list->items;
call_user_func_array('array_multisort', $multisortArgs); call_user_func_array('array_multisort', $multisortArgs);
return $this; return $list;
} }
/** /**
@ -424,8 +428,10 @@ class ArrayList extends ViewableData implements SS_List, SS_Filterable, SS_Sorta
} }
} }
$this->items = $itemsToKeep; // TODO 3.1: This currently mutates existing array
return $this; $list = /* clone */ $this;
$list->items = $itemsToKeep;
return $list;
} }
public function byID($id) { public function byID($id) {
@ -478,12 +484,14 @@ class ArrayList extends ViewableData implements SS_List, SS_Filterable, SS_Sorta
} }
$keysToRemove = array_keys($matches,$hitsRequiredToRemove); $keysToRemove = array_keys($matches,$hitsRequiredToRemove);
// TODO 3.1: This currently mutates existing array
$list = /* clone */ $this;
foreach($keysToRemove as $itemToRemoveIdx){ foreach($keysToRemove as $itemToRemoveIdx){
$this->remove($this->items[$itemToRemoveIdx]); $list->remove($this->items[$itemToRemoveIdx]);
} }
return;
return $list;
return $this;
} }
protected function shouldExclude($item, $args) { protected function shouldExclude($item, $args) {

View File

@ -2,7 +2,21 @@
/** /**
* Implements a "lazy loading" DataObjectSet. * Implements a "lazy loading" DataObjectSet.
* Uses {@link DataQuery} to do the actual query generation. * Uses {@link DataQuery} to do the actual query generation.
* *
* todo 3.1: In 3.0 the below is not currently true for backwards compatible reasons, but code should not rely on current behaviour
*
* DataLists have two sets of methods.
*
* 1). Selection methods (SS_Filterable, SS_Sortable, SS_Limitable) change the way the list is built, but does not
* alter underlying data. There are no external affects from selection methods once this list instance is destructed.
*
* 2). Mutation methods change the underlying data. The change persists into the underlying data storage layer.
*
* DataLists are _immutable_ as far as selection methods go - they all return new instances of DataList, rather
* than change the current list.
*
* DataLists are _mutable_ as far as mutation methods go - they all act on the existing DataList instance.
*
* @package framework * @package framework
* @subpackage model * @subpackage model
*/ */
@ -67,12 +81,109 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
} }
/** /**
* Return the internal {@link DataQuery} object for direct manipulation * Return a copy of the internal {@link DataQuery} object
* *
* todo 3.1: In 3.0 the below is not currently true for backwards compatible reasons, but code should not rely on this
* Because the returned value is a copy, modifying it won't affect this list's contents. If
* you want to alter the data query directly, use the alterDataQuery method
*
* @return DataQuery * @return DataQuery
*/ */
public function dataQuery() { public function dataQuery() {
return $this->dataQuery; // TODO 3.1: This method potentially mutates self
return /* clone */ $this->dataQuery;
}
/**
* @var bool - Indicates if we are in an alterDataQueryCall already, so alterDataQuery can be re-entrant
*/
protected $inAlterDataQueryCall = false;
/**
* Return a new DataList instance with the underlying {@link DataQuery} object altered
*
* If you want to alter the underlying dataQuery for this list, this wrapper method
* will ensure that you can do so without mutating the existing List object.
*
* It clones this list, calls the passed callback function with the dataQuery of the new
* list as it's first parameter (and the list as it's second), then returns the list
*
* Note that this function is re-entrant - it's safe to call this inside a callback passed to
* alterDataQuery
*
* @param $callback
* @return DataList
*/
public function alterDataQuery($callback) {
if ($this->inAlterDataQueryCall) {
$list = $this;
$res = $callback($list->dataQuery, $list);
if ($res) $list->dataQuery = $res;
return $list;
}
else {
$list = clone $this;
$list->inAlterDataQueryCall = true;
try {
$res = $callback($list->dataQuery, $list);
if ($res) $list->dataQuery = $res;
}
catch (Exception $e) {
$list->inAlterDataQueryCall = false;
throw $e;
}
$list->inAlterDataQueryCall = false;
return $list;
}
}
/**
* In 3.0.0 some methods in DataList mutate their list. We don't want to change that in the 3.0.x
* line, but we don't want people relying on it either. This does the same as alterDataQuery, but
* _does_ mutate the existing list.
*
* todo 3.1: All methods that call this need to call alterDataQuery instead
*/
protected function alterDataQuery_30($callback) {
Deprecation::notice('3.1', 'DataList will become immutable in 3.1');
if ($this->inAlterDataQueryCall) {
$res = $callback($this->dataQuery, $this);
if ($res) $this->dataQuery = $res;
return $this;
}
else {
$this->inAlterDataQueryCall = true;
try {
$res = $callback($this->dataQuery, $this);
if ($res) $this->dataQuery = $res;
}
catch (Exception $e) {
$this->inAlterDataQueryCall = false;
throw $e;
}
$this->inAlterDataQueryCall = false;
return $this;
}
}
/**
* Return a new DataList instance with the underlying {@link DataQuery} object changed
*
* @param DataQuery $dataQuery
* @return DataList
*/
public function setDataQuery(DataQuery $dataQuery) {
$clone = clone $this;
$clone->dataQuery = $dataQuery;
return $clone;
} }
/** /**
@ -85,14 +196,15 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
} }
/** /**
* Add a WHERE clause to the query. * Return a new DataList instance with a WHERE clause added to this list's query.
* *
* @param string $filter Escaped SQL statement * @param string $filter Escaped SQL statement
* @return DataList * @return DataList
*/ */
public function where($filter) { public function where($filter) {
$this->dataQuery->where($filter); return $this->alterDataQuery_30(function($query) use ($filter){
return $this; $query->where($filter);
});
} }
/** /**
@ -118,7 +230,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
} }
/** /**
* Add an join clause to this data list's query. * Return a new DataList instance with a join clause added to this list's query.
* *
* @param type $join Escaped SQL statement * @param type $join Escaped SQL statement
* @return DataList * @return DataList
@ -126,12 +238,13 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
*/ */
public function join($join) { public function join($join) {
Deprecation::notice('3.0', 'Use innerJoin() or leftJoin() instead.'); Deprecation::notice('3.0', 'Use innerJoin() or leftJoin() instead.');
$this->dataQuery->join($join); return $this->alterDataQuery_30(function($query) use ($join){
return $this; $query->join($join);
});
} }
/** /**
* Restrict the records returned in this query by a limit clause * Return a new DataList instance with the records returned in this query restricted by a limit clause
* *
* @param int $limit * @param int $limit
* @param int $offset * @param int $offset
@ -143,76 +256,91 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
if($limit && !is_numeric($limit)) { if($limit && !is_numeric($limit)) {
Deprecation::notice('3.0', 'Please pass limits as 2 arguments, rather than an array or SQL fragment.', Deprecation::SCOPE_GLOBAL); Deprecation::notice('3.0', 'Please pass limits as 2 arguments, rather than an array or SQL fragment.', Deprecation::SCOPE_GLOBAL);
} }
$this->dataQuery->limit($limit, $offset); return $this->alterDataQuery_30(function($query) use ($limit, $offset){
return $this; $query->limit($limit, $offset);
});
} }
/** /**
* Set the sort order of this data list * Return a new DataList instance as a copy of this data list with the sort order set
* *
* @see SS_List::sort() * @see SS_List::sort()
* @see SQLQuery::orderby * @see SQLQuery::orderby
* @example $list->sort('Name'); // default ASC sorting * @example $list = $list->sort('Name'); // default ASC sorting
* @example $list->sort('Name DESC'); // DESC sorting * @example $list = $list->sort('Name DESC'); // DESC sorting
* @example $list->sort('Name', 'ASC'); * @example $list = $list->sort('Name', 'ASC');
* @example $list->sort(array('Name'=>'ASC,'Age'=>'DESC')); * @example $list = $list->sort(array('Name'=>'ASC,'Age'=>'DESC'));
* *
* @param String|array Escaped SQL statement. If passed as array, all keys and values are assumed to be escaped. * @param String|array Escaped SQL statement. If passed as array, all keys and values are assumed to be escaped.
* @return DataList * @return DataList
*/ */
public function sort() { public function sort() {
if(count(func_get_args()) == 0) { $count = func_num_args();
if($count == 0) {
return $this; return $this;
} }
if(count(func_get_args()) > 2) { if($count > 2) {
throw new InvalidArgumentException('This method takes zero, one or two arguments'); throw new InvalidArgumentException('This method takes zero, one or two arguments');
} }
if(count(func_get_args()) == 2) { $sort = $col = $dir = null;
// sort('Name','Desc')
if(!in_array(strtolower(func_get_arg(1)),array('desc','asc'))){ if ($count == 2) {
user_error('Second argument to sort must be either ASC or DESC'); list($col, $dir) = func_get_args();
}
$this->dataQuery->sort(func_get_arg(0), func_get_arg(1));
} }
else if(is_string(func_get_arg(0)) && func_get_arg(0)){ else {
// sort('Name ASC') $sort = func_get_arg(0);
if(stristr(func_get_arg(0), ' asc') || stristr(func_get_arg(0), ' desc')) {
$this->dataQuery->sort(func_get_arg(0));
} else {
$this->dataQuery->sort(func_get_arg(0), 'ASC');
}
} }
else if(is_array(func_get_arg(0))) {
// sort(array('Name'=>'desc')); return $this->alterDataQuery_30(function($query, $list) use ($sort, $col, $dir){
$this->dataQuery->sort(null, null); // wipe the sort
if ($col) {
foreach(func_get_arg(0) as $col => $dir) { // sort('Name','Desc')
// Convert column expressions to SQL fragment, while still allowing the passing of raw SQL fragments. if(!in_array(strtolower($dir),array('desc','asc'))){
try { user_error('Second argument to sort must be either ASC or DESC');
$relCol = $this->getRelationName($col);
} catch(InvalidArgumentException $e) {
$relCol = $col;
} }
$this->dataQuery->sort($relCol, $dir, false);
$query->sort($col, $dir);
} }
}
else if(is_string($sort) && $sort){
return $this; // sort('Name ASC')
if(stristr($sort, ' asc') || stristr($sort, ' desc')) {
$query->sort($sort);
} else {
$query->sort($sort, 'ASC');
}
}
else if(is_array($sort)) {
// sort(array('Name'=>'desc'));
$query->sort(null, null); // wipe the sort
foreach($sort as $col => $dir) {
// Convert column expressions to SQL fragment, while still allowing the passing of raw SQL fragments.
try {
$relCol = $list->getRelationName($col);
} catch(InvalidArgumentException $e) {
$relCol = $col;
}
$query->sort($relCol, $dir, false);
}
}
});
} }
/** /**
* Filter the list to include items with these charactaristics * Return a copy of this list which only includes items with these charactaristics
* *
* @see SS_List::filter() * @see SS_List::filter()
* *
* @example $list->filter('Name', 'bob'); // only bob in the list * @example $list = $list->filter('Name', 'bob'); // only bob in the list
* @example $list->filter('Name', array('aziz', 'bob'); // aziz and bob in list * @example $list = $list->filter('Name', array('aziz', 'bob'); // aziz and bob in list
* @example $list->filter(array('Name'=>'bob, 'Age'=>21)); // bob with the age 21 * @example $list = $list->filter(array('Name'=>'bob, 'Age'=>21)); // bob with the age 21
* @example $list->filter(array('Name'=>'bob, 'Age'=>array(21, 43))); // bob with the Age 21 or 43 * @example $list = $list->filter(array('Name'=>'bob, 'Age'=>array(21, 43))); // bob with the Age 21 or 43
* @example $list->filter(array('Name'=>array('aziz','bob'), 'Age'=>array(21, 43))); // aziz with the age 21 or 43 and bob with the Age 21 or 43 * @example $list = $list->filter(array('Name'=>array('aziz','bob'), 'Age'=>array(21, 43))); // aziz with the age 21 or 43 and bob with the Age 21 or 43
* *
* @todo extract the sql from $customQuery into a SQLGenerator class * @todo extract the sql from $customQuery into a SQLGenerator class
* *
@ -229,13 +357,14 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
throw new InvalidArgumentException('Incorrect number of arguments passed to filter()'); throw new InvalidArgumentException('Incorrect number of arguments passed to filter()');
} }
// TODO 3.1: Once addFilter doesn't mutate self, this results in a double clone
$clone = clone $this; $clone = clone $this;
$clone->addFilter($filters); $clone->addFilter($filters);
return $clone; return $clone;
} }
/** /**
* Modify this DataList, adding a filter * Return a new instance of the list with an added filter
*/ */
public function addFilter($filterArray) { public function addFilter($filterArray) {
$SQL_Statements = array(); $SQL_Statements = array();
@ -262,12 +391,14 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
$SQL_Statements[] = $field . ' ' . $customQuery; $SQL_Statements[] = $field . ' ' . $customQuery;
} }
} }
if(count($SQL_Statements)) {
if(!count($SQL_Statements)) return $this;
return $this->alterDataQuery_30(function($query) use ($SQL_Statements){
foreach($SQL_Statements as $SQL_Statement){ foreach($SQL_Statements as $SQL_Statement){
$this->dataQuery->where($SQL_Statement); $query->where($SQL_Statement);
} }
} });
return $this;
} }
/** /**
@ -299,7 +430,11 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
if(!preg_match('/^[A-Z0-9._]+$/i', $field)) { if(!preg_match('/^[A-Z0-9._]+$/i', $field)) {
throw new InvalidArgumentException("Bad field expression $field"); throw new InvalidArgumentException("Bad field expression $field");
} }
if (!$this->inAlterDataQueryCall) {
Deprecation::notice('3.1', 'getRelationName is mutating, and must be called inside an alterDataQuery block');
}
if(strpos($field,'.') === false) { if(strpos($field,'.') === false) {
return '"'.$field.'"'; return '"'.$field.'"';
} }
@ -328,14 +463,14 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
} }
/** /**
* Exclude the list to not contain items with these characteristics * Return a copy of this list which does not contain any items with these charactaristics
* *
* @see SS_List::exclude() * @see SS_List::exclude()
* @example $list->exclude('Name', 'bob'); // exclude bob from list * @example $list = $list->exclude('Name', 'bob'); // exclude bob from list
* @example $list->exclude('Name', array('aziz', 'bob'); // exclude aziz and bob from list * @example $list = $list->exclude('Name', array('aziz', 'bob'); // exclude aziz and bob from list
* @example $list->exclude(array('Name'=>'bob, 'Age'=>21)); // exclude bob that has Age 21 * @example $list = $list->exclude(array('Name'=>'bob, 'Age'=>21)); // exclude bob that has Age 21
* @example $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob with Age 21 or 43 * @example $list = $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob with Age 21 or 43
* @example $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43))); // bob age 21 or 43, phil age 21 or 43 would be excluded * @example $list = $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43))); // bob age 21 or 43, phil age 21 or 43 would be excluded
* *
* @todo extract the sql from this method into a SQLGenerator class * @todo extract the sql from this method into a SQLGenerator class
* *
@ -368,15 +503,17 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
$SQL_Statements[] = ($fieldName . ' != \''.Convert::raw2sql($value).'\''); $SQL_Statements[] = ($fieldName . ' != \''.Convert::raw2sql($value).'\'');
} }
} }
$this->dataQuery->whereAny($SQL_Statements);
return $this; if(!count($SQL_Statements)) return $this;
return $this->alterDataQuery_30(function($query) use ($SQL_Statements){
$query->whereAny($SQL_Statements);
});
} }
/** /**
* This method returns a list does not contain any DataObjects that exists in $list * This method returns a copy of this list that does not contain any DataObjects that exists in $list
* *
* It does not return the resulting list, it only adds the constraints on the database to exclude
* objects from $list.
* The $list passed needs to contain the same dataclass as $this * The $list passed needs to contain the same dataclass as $this
* *
* @param SS_List $list * @param SS_List $list
@ -387,15 +524,14 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
if($this->dataclass() != $list->dataclass()) { if($this->dataclass() != $list->dataclass()) {
throw new InvalidArgumentException('The list passed must have the same dataclass as this class'); throw new InvalidArgumentException('The list passed must have the same dataclass as this class');
} }
$newlist = clone $this; return $this->alterDataQuery(function($query) use ($list){
$newlist->dataQuery->subtract($list->dataQuery()); $query->subtract($list->dataQuery());
});
return $newlist;
} }
/** /**
* Add an inner join clause to this data list's query. * Return a new DataList instance with an inner join clause added to this list's query.
* *
* @param string $table Table name (unquoted) * @param string $table Table name (unquoted)
* @param string $onClause Escaped SQL statement, e.g. '"Table1"."ID" = "Table2"."ID"' * @param string $onClause Escaped SQL statement, e.g. '"Table1"."ID" = "Table2"."ID"'
@ -403,13 +539,13 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
* @return DataList * @return DataList
*/ */
public function innerJoin($table, $onClause, $alias = null) { public function innerJoin($table, $onClause, $alias = null) {
$this->dataQuery->innerJoin($table, $onClause, $alias); return $this->alterDataQuery_30(function($query) use ($table, $onClause, $alias){
$query->innerJoin($table, $onClause, $alias);
return $this; });
} }
/** /**
* Add an left join clause to this data list's query. * Return a new DataList instance with a left join clause added to this list's query.
* *
* @param string $table Table name (unquoted) * @param string $table Table name (unquoted)
* @param string $onClause Escaped SQL statement, e.g. '"Table1"."ID" = "Table2"."ID"' * @param string $onClause Escaped SQL statement, e.g. '"Table1"."ID" = "Table2"."ID"'
@ -417,9 +553,9 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
* @return DataList * @return DataList
*/ */
public function leftJoin($table, $onClause, $alias = null) { public function leftJoin($table, $onClause, $alias = null) {
$this->dataQuery->leftJoin($table, $onClause, $alias); return $this->alterDataQuery_30(function($query) use ($table, $onClause, $alias){
$query->leftJoin($table, $onClause, $alias);
return $this; });
} }
/** /**
@ -611,8 +747,6 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
* @return DataObject|null * @return DataObject|null
*/ */
public function find($key, $value) { public function find($key, $value) {
$clone = clone $this;
if($key == 'ID') { if($key == 'ID') {
$baseClass = ClassInfo::baseDataClass($this->dataClass); $baseClass = ClassInfo::baseDataClass($this->dataClass);
$SQL_col = sprintf('"%s"."%s"', $baseClass, Convert::raw2sql($key)); $SQL_col = sprintf('"%s"."%s"', $baseClass, Convert::raw2sql($key));
@ -620,6 +754,8 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
$SQL_col = sprintf('"%s"', Convert::raw2sql($key)); $SQL_col = sprintf('"%s"', Convert::raw2sql($key));
} }
// todo 3.1: In 3.1 where won't be mutating, so this can be on $this directly
$clone = clone $this;
return $clone->where("$SQL_col = '" . Convert::raw2sql($value) . "'")->First(); return $clone->where("$SQL_col = '" . Convert::raw2sql($value) . "'")->First();
} }
@ -630,9 +766,9 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
* @return DataList * @return DataList
*/ */
public function setQueriedColumns($queriedColumns) { public function setQueriedColumns($queriedColumns) {
$clone = clone $this; return $this->alterDataQuery(function($query) use ($queriedColumns){
$clone->dataQuery->setQueriedColumns($queriedColumns); $query->setQueriedColumns($queriedColumns);
return $clone; });
} }
/** /**
@ -657,8 +793,9 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
*/ */
public function byID($id) { public function byID($id) {
$baseClass = ClassInfo::baseDataClass($this->dataClass); $baseClass = ClassInfo::baseDataClass($this->dataClass);
// todo 3.1: In 3.1 where won't be mutating, so this can be on $this directly
$clone = clone $this; $clone = clone $this;
return $clone->where("\"$baseClass\".\"ID\" = " . (int)$id)->First(); return $clone->where("\"$baseClass\".\"ID\" = " . (int)$id)->First();
} }
@ -835,9 +972,9 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
* @return DataList * @return DataList
*/ */
public function reverse() { public function reverse() {
$this->dataQuery->reverseSort(); return $this->alterDataQuery_30(function($query){
$query->reverseSort();
return $this; });
} }
/** /**

View File

@ -679,13 +679,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return array The data as a map. * @return array The data as a map.
*/ */
public function toMap() { public function toMap() {
foreach ($this->record as $key => $value) { $this->loadLazyFields();
if (strlen($key) > 5 && substr($key, -5) == '_Lazy') {
$this->loadLazyFields($value);
break;
}
}
return $this->record; return $this->record;
} }
@ -847,13 +841,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* if they are not already marked as changed. * if they are not already marked as changed.
*/ */
public function forceChange() { public function forceChange() {
// Ensure lazy fields loaded
$this->loadLazyFields();
// $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well // $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
$fieldNames = array_unique(array_merge(array_keys($this->record), array_keys($this->inheritedDatabaseFields()))); $fieldNames = array_unique(array_merge(array_keys($this->record), array_keys($this->inheritedDatabaseFields())));
foreach($fieldNames as $fieldName) { foreach($fieldNames as $fieldName) {
if(!isset($this->changed[$fieldName])) $this->changed[$fieldName] = 1; if(!isset($this->changed[$fieldName])) $this->changed[$fieldName] = 1;
// Populate the null values in record so that they actually get written // Populate the null values in record so that they actually get written
if(!$this->$fieldName) $this->record[$fieldName] = null; if(!isset($this->record[$fieldName])) $this->record[$fieldName] = null;
} }
// @todo Find better way to allow versioned to write a new version after forceChange // @todo Find better way to allow versioned to write a new version after forceChange
@ -1288,7 +1285,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$result = new HasManyList($componentClass, $joinField); $result = new HasManyList($componentClass, $joinField);
if($this->model) $result->setDataModel($this->model); if($this->model) $result->setDataModel($this->model);
$result->setForeignID($this->ID); $result = $result->forForeignID($this->ID);
$result = $result->where($filter)->limit($limit)->sort($sort); $result = $result->where($filter)->limit($limit)->sort($sort);
if($join) $result = $result->join($join); if($join) $result = $result->join($join);
@ -1412,7 +1409,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// If this is called on a singleton, then we return an 'orphaned relation' that can have the // If this is called on a singleton, then we return an 'orphaned relation' that can have the
// foreignID set elsewhere. // foreignID set elsewhere.
$result->setForeignID($this->ID); $result = $result->forForeignID($this->ID);
return $result->where($filter)->sort($sort)->limit($limit); return $result->where($filter)->sort($sort)->limit($limit);
} }
@ -1920,7 +1917,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if(self::is_composite_field($this->class, $field)) { if(self::is_composite_field($this->class, $field)) {
$helper = $this->castingHelper($field); $helper = $this->castingHelper($field);
$fieldObj = Object::create_from_string($helper, $field); $fieldObj = Object::create_from_string($helper, $field);
$compositeFields = $fieldObj->compositeDatabaseFields();
foreach ($compositeFields as $compositeName => $compositeType) {
if(isset($this->record[$field.$compositeName.'_Lazy'])) {
$tableClass = $this->record[$field.$compositeName.'_Lazy'];
$this->loadLazyFields($tableClass);
}
}
// write value only if either the field value exists, // write value only if either the field value exists,
// or a valid record has been loaded from the database // or a valid record has been loaded from the database
$value = (isset($this->record[$field])) ? $this->record[$field] : null; $value = (isset($this->record[$field])) ? $this->record[$field] : null;
@ -1946,13 +1951,24 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} }
/** /**
* Loads all the stub fields than an initial lazy load didn't load fully. * Loads all the stub fields that an initial lazy load didn't load fully.
* *
* @param tableClass Base table to load the values from. Others are joined as required. * @param tableClass Base table to load the values from. Others are joined as required.
* Not specifying a tableClass will load all lazy fields from all tables.
*/ */
protected function loadLazyFields($tableClass = null) { protected function loadLazyFields($tableClass = null) {
// Smarter way to work out the tableClass? Should the functionality in toMap and getField be moved into here? if (!$tableClass) {
if (!$tableClass) $tableClass = $this->ClassName; $loaded = array();
foreach ($this->record as $key => $value) {
if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
$this->loadLazyFields($value);
$loaded[$value] = $value;
}
}
return;
}
$dataQuery = new DataQuery($tableClass); $dataQuery = new DataQuery($tableClass);
@ -2092,9 +2108,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// If we've just lazy-loaded the column, then we need to populate the $original array by // If we've just lazy-loaded the column, then we need to populate the $original array by
// called getField(). Too much overhead? Could this be done by a quicker method? Maybe only // called getField(). Too much overhead? Could this be done by a quicker method? Maybe only
// on a call to getChanged()? // on a call to getChanged()?
if (isset($this->record[$fieldName.'_Lazy'])) { $this->getField($fieldName);
$this->getField($fieldName);
}
$this->record[$fieldName] = $val; $this->record[$fieldName] = $val;
// Situation 2: Passing a literal or non-DBField object // Situation 2: Passing a literal or non-DBField object
@ -2120,9 +2134,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// If we've just lazy-loaded the column, then we need to populate the $original array by // If we've just lazy-loaded the column, then we need to populate the $original array by
// called getField(). Too much overhead? Could this be done by a quicker method? Maybe only // called getField(). Too much overhead? Could this be done by a quicker method? Maybe only
// on a call to getChanged()? // on a call to getChanged()?
if (isset($this->record[$fieldName.'_Lazy'])) { $this->getField($fieldName);
$this->getField($fieldName);
}
// Value is always saved back when strict check succeeds. // Value is always saved back when strict check succeeds.
$this->record[$fieldName] = $val; $this->record[$fieldName] = $val;

View File

@ -2,7 +2,10 @@
/** /**
* Additional interface for {@link SS_List} classes that are filterable. * Additional interface for {@link SS_List} classes that are filterable.
* *
* All methods in this interface are immutable - they should return new instances with the filter
* applied, rather than applying the filter in place
*
* @see SS_List, SS_Sortable, SS_Limitable * @see SS_List, SS_Sortable, SS_Limitable
*/ */
interface SS_Filterable { interface SS_Filterable {
@ -18,22 +21,22 @@ interface SS_Filterable {
/** /**
* Filter the list to include items with these charactaristics * Filter the list to include items with these charactaristics
* *
* @example $list->filter('Name', 'bob'); // only bob in the list * @example $list = $list->filter('Name', 'bob'); // only bob in the list
* @example $list->filter('Name', array('aziz', 'bob'); // aziz and bob in list * @example $list = $list->filter('Name', array('aziz', 'bob'); // aziz and bob in list
* @example $list->filter(array('Name'=>'bob, 'Age'=>21)); // bob with the age 21 * @example $list = $list->filter(array('Name'=>'bob, 'Age'=>21)); // bob with the age 21
* @example $list->filter(array('Name'=>'bob, 'Age'=>array(21, 43))); // bob with the Age 21 or 43 * @example $list = $list->filter(array('Name'=>'bob, 'Age'=>array(21, 43))); // bob with the Age 21 or 43
* @example $list->filter(array('Name'=>array('aziz','bob'), 'Age'=>array(21, 43))); // aziz with the age 21 or 43 and bob with the Age 21 or 43 * @example $list = $list->filter(array('Name'=>array('aziz','bob'), 'Age'=>array(21, 43))); // aziz with the age 21 or 43 and bob with the Age 21 or 43
*/ */
public function filter(); public function filter();
/** /**
* Exclude the list to not contain items with these charactaristics * Exclude the list to not contain items with these charactaristics
* *
* @example $list->exclude('Name', 'bob'); // exclude bob from list * @example $list = $list->exclude('Name', 'bob'); // exclude bob from list
* @example $list->exclude('Name', array('aziz', 'bob'); // exclude aziz and bob from list * @example $list = $list->exclude('Name', array('aziz', 'bob'); // exclude aziz and bob from list
* @example $list->exclude(array('Name'=>'bob, 'Age'=>21)); // exclude bob that has Age 21 * @example $list = $list->exclude(array('Name'=>'bob, 'Age'=>21)); // exclude bob that has Age 21
* @example $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob with Age 21 or 43 * @example $list = $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob with Age 21 or 43
* @example $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43))); // bob age 21 or 43, phil age 21 or 43 would be excluded * @example $list = $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43))); // bob age 21 or 43, phil age 21 or 43 would be excluded
*/ */
public function exclude(); public function exclude();

View File

@ -2,7 +2,10 @@
/** /**
* Additional interface for {@link SS_List} classes that are limitable - able to have a subset of the list extracted. * Additional interface for {@link SS_List} classes that are limitable - able to have a subset of the list extracted.
* *
* All methods in this interface are immutable - they should return new instances with the limit
* applied, rather than applying the limit in place
*
* @see SS_List, SS_Sortable, SS_Filterable * @see SS_List, SS_Sortable, SS_Filterable
*/ */
interface SS_Limitable { interface SS_Limitable {

View File

@ -11,6 +11,10 @@ abstract class RelationList extends DataList {
/** /**
* Set the ID of the record that this ManyManyList is linking *from*. * Set the ID of the record that this ManyManyList is linking *from*.
*
* This is the mutatable version of this function, and will be protected only
* from 3.1. Use forForeignID instead
*
* @param $id A single ID, or an array of IDs * @param $id A single ID, or an array of IDs
*/ */
function setForeignID($id) { function setForeignID($id) {
@ -19,7 +23,8 @@ abstract class RelationList extends DataList {
$oldFilter = $this->foreignIDFilter(); $oldFilter = $this->foreignIDFilter();
try { try {
$this->dataQuery->removeFilterOn($oldFilter); $this->dataQuery->removeFilterOn($oldFilter);
} catch(InvalidArgumentException $e) {} }
catch(InvalidArgumentException $e) { /* NOP */ }
} }
// Turn a 1-element array into a simple value // Turn a 1-element array into a simple value
@ -32,12 +37,13 @@ abstract class RelationList extends DataList {
} }
/** /**
* Returns this ManyMany relationship linked to the given foreign ID. * Returns a copy of this list with the ManyMany relationship linked to the given foreign ID.
* @param $id An ID or an array of IDs. * @param $id An ID or an array of IDs.
*/ */
function forForeignID($id) { function forForeignID($id) {
$this->setForeignID($id); return $this->alterDataQuery_30(function($query, $list) use ($id){
return $this; $list->setForeignID($id);
});
} }
abstract protected function foreignIDFilter(); abstract protected function foreignIDFilter();

View File

@ -2,7 +2,10 @@
/** /**
* Additional interface for {@link SS_List} classes that are sortable. * Additional interface for {@link SS_List} classes that are sortable.
* *
* All methods in this interface are immutable - they should return new instances with the sort
* applied, rather than applying the sort in place
*
* @see SS_List, SS_Filterable, SS_Limitable * @see SS_List, SS_Filterable, SS_Limitable
*/ */
interface SS_Sortable { interface SS_Sortable {
@ -19,10 +22,10 @@ interface SS_Sortable {
* Sorts this list by one or more fields. You can either pass in a single * Sorts this list by one or more fields. You can either pass in a single
* field name and direction, or a map of field names to sort directions. * field name and direction, or a map of field names to sort directions.
* *
* @example $list->sort('Name'); // default ASC sorting * @example $list = $list->sort('Name'); // default ASC sorting
* @example $list->sort('Name DESC'); // DESC sorting * @example $list = $list->sort('Name DESC'); // DESC sorting
* @example $list->sort('Name', 'ASC'); * @example $list = $list->sort('Name', 'ASC');
* @example $list->sort(array('Name'=>'ASC,'Age'=>'DESC')); * @example $list = $list->sort(array('Name'=>'ASC,'Age'=>'DESC'));
*/ */
public function sort(); public function sort();
@ -30,7 +33,7 @@ interface SS_Sortable {
/** /**
* Reverses the list based on reversing the current sort. * Reverses the list based on reversing the current sort.
* *
* @example $list->reverse(); * @example $list = $list->reverse();
* *
* @return array * @return array
*/ */

View File

@ -935,7 +935,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
*/ */
public function Groups() { public function Groups() {
$groups = Injector::inst()->create('Member_GroupSet', 'Group', 'Group_Members', 'GroupID', 'MemberID'); $groups = Injector::inst()->create('Member_GroupSet', 'Group', 'Group_Members', 'GroupID', 'MemberID');
$groups->setForeignID($this->ID); $groups = $groups->forForeignID($this->ID);
$this->extend('updateGroups', $groups); $this->extend('updateGroups', $groups);

View File

@ -29,8 +29,7 @@ class ManyManyListTest extends SapphireTest {
$player1->flushCache(); $player1->flushCache();
$compareTeams = new ManyManyList('DataObjectTest_Team','DataObjectTest_Team_Players', 'DataObjectTest_TeamID', 'DataObjectTest_PlayerID'); $compareTeams = new ManyManyList('DataObjectTest_Team','DataObjectTest_Team_Players', 'DataObjectTest_TeamID', 'DataObjectTest_PlayerID');
$compareTeams->forForeignID($player1->ID); $compareTeams = $compareTeams->forForeignID($player1->ID);
$compareTeams->byID($team1->ID);
$this->assertEquals($player1->Teams()->column('ID'),$compareTeams->column('ID'),"Adding single record as DataObject to many_many"); $this->assertEquals($player1->Teams()->column('ID'),$compareTeams->column('ID'),"Adding single record as DataObject to many_many");
} }
@ -40,8 +39,7 @@ class ManyManyListTest extends SapphireTest {
$player1->Teams()->remove($team1); $player1->Teams()->remove($team1);
$player1->flushCache(); $player1->flushCache();
$compareTeams = new ManyManyList('DataObjectTest_Team','DataObjectTest_Team_Players', 'DataObjectTest_TeamID', 'DataObjectTest_PlayerID'); $compareTeams = new ManyManyList('DataObjectTest_Team','DataObjectTest_Team_Players', 'DataObjectTest_TeamID', 'DataObjectTest_PlayerID');
$compareTeams->forForeignID($player1->ID); $compareTeams = $compareTeams->forForeignID($player1->ID);
$compareTeams->byID($team1->ID);
$this->assertEquals($player1->Teams()->column('ID'),$compareTeams->column('ID'),"Removing single record as DataObject from many_many"); $this->assertEquals($player1->Teams()->column('ID'),$compareTeams->column('ID'),"Removing single record as DataObject from many_many");
} }
@ -51,8 +49,7 @@ class ManyManyListTest extends SapphireTest {
$player1->Teams()->add($team1->ID); $player1->Teams()->add($team1->ID);
$player1->flushCache(); $player1->flushCache();
$compareTeams = new ManyManyList('DataObjectTest_Team','DataObjectTest_Team_Players', 'DataObjectTest_TeamID', 'DataObjectTest_PlayerID'); $compareTeams = new ManyManyList('DataObjectTest_Team','DataObjectTest_Team_Players', 'DataObjectTest_TeamID', 'DataObjectTest_PlayerID');
$compareTeams->forForeignID($player1->ID); $compareTeams = $compareTeams->forForeignID($player1->ID);
$compareTeams->byID($team1->ID);
$this->assertEquals($player1->Teams()->column('ID'), $compareTeams->column('ID'), "Adding single record as ID to many_many"); $this->assertEquals($player1->Teams()->column('ID'), $compareTeams->column('ID'), "Adding single record as ID to many_many");
} }
@ -62,8 +59,7 @@ class ManyManyListTest extends SapphireTest {
$player1->Teams()->removeByID($team1->ID); $player1->Teams()->removeByID($team1->ID);
$player1->flushCache(); $player1->flushCache();
$compareTeams = new ManyManyList('DataObjectTest_Team','DataObjectTest_Team_Players', 'DataObjectTest_TeamID', 'DataObjectTest_PlayerID'); $compareTeams = new ManyManyList('DataObjectTest_Team','DataObjectTest_Team_Players', 'DataObjectTest_TeamID', 'DataObjectTest_PlayerID');
$compareTeams->forForeignID($player1->ID); $compareTeams = $compareTeams->forForeignID($player1->ID);
$compareTeams->byID($team1->ID);
$this->assertEquals($player1->Teams()->column('ID'), $compareTeams->column('ID'), "Removing single record as ID from many_many"); $this->assertEquals($player1->Teams()->column('ID'), $compareTeams->column('ID'), "Removing single record as ID from many_many");
} }
@ -85,7 +81,7 @@ class ManyManyListTest extends SapphireTest {
$team1 = $this->objFromFixture('DataObjectTest_Team', 'team1'); $team1 = $this->objFromFixture('DataObjectTest_Team', 'team1');
$team2 = $this->objFromFixture('DataObjectTest_Team', 'team2'); $team2 = $this->objFromFixture('DataObjectTest_Team', 'team2');
$playersTeam1Team2 = DataObjectTest_Team::get()->relation('Players')->setForeignID(array($team1->ID, $team2->ID)); $playersTeam1Team2 = DataObjectTest_Team::get()->relation('Players')->forForeignID(array($team1->ID, $team2->ID));
$playersTeam1Team2->add($newPlayer); $playersTeam1Team2->add($newPlayer);
$this->assertEquals( $this->assertEquals(
array($team1->ID, $team2->ID), array($team1->ID, $team2->ID),

View File

@ -392,6 +392,10 @@ after')
$this->assertEquals('AD', $this->assertEquals('AD',
$this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% end_if %>D')); $this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% end_if %>D'));
// Bare words with ending space
$this->assertEquals('ABC',
$this->render('A<% if "RawVal" == RawVal %>B<% end_if %>C'));
// Else // Else
$this->assertEquals('ADE', $this->assertEquals('ADE',
$this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% else %>D<% end_if %>E')); $this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% else %>D<% end_if %>E'));
@ -457,7 +461,7 @@ after')
$this->assertEquals( $this->assertEquals(
$this->render('<% include SSViewerTestIncludeWithArguments Arg1=A Bare String, Arg2=B Bare String %>'), $this->render('<% include SSViewerTestIncludeWithArguments Arg1=A Bare String, Arg2=B Bare String %>'),
'<p>A Bare String</p><p>B Bare String </p>' '<p>A Bare String</p><p>B Bare String</p>'
); );
$this->assertEquals( $this->assertEquals(

View File

@ -1146,7 +1146,7 @@ class SSTemplateParser extends Parser {
function Argument_FreeString(&$res, $sub) { function Argument_FreeString(&$res, $sub) {
$res['ArgumentMode'] = 'string'; $res['ArgumentMode'] = 'string';
$res['php'] = "'" . str_replace("'", "\\'", $sub['text']) . "'"; $res['php'] = "'" . str_replace("'", "\\'", trim($sub['text'])) . "'";
} }
/* ComparisonOperator: "==" | "!=" | "=" */ /* ComparisonOperator: "==" | "!=" | "=" */

View File

@ -322,7 +322,7 @@ class SSTemplateParser extends Parser {
function Argument_FreeString(&$res, $sub) { function Argument_FreeString(&$res, $sub) {
$res['ArgumentMode'] = 'string'; $res['ArgumentMode'] = 'string';
$res['php'] = "'" . str_replace("'", "\\'", $sub['text']) . "'"; $res['php'] = "'" . str_replace("'", "\\'", trim($sub['text'])) . "'";
} }
/*!* /*!*