mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge remote-tracking branch 'origin/3.1'
Conflicts: forms/gridfield/GridFieldSortableHeader.php model/DataList.php model/fieldtypes/Enum.php
This commit is contained in:
commit
6176d65bd2
@ -217,6 +217,21 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we've a history state to go back to, go back, otherwise fall back to
|
||||||
|
* submitting the form with the 'doCancel' action.
|
||||||
|
*/
|
||||||
|
$('.cms-edit-form .Actions input.action[type=submit].ss-ui-action-cancel, .cms-edit-form .Actions button.action.ss-ui-action-cancel').entwine({
|
||||||
|
onclick: function(e) {
|
||||||
|
if (History.getStateByIndex(1)) {
|
||||||
|
History.back();
|
||||||
|
} else {
|
||||||
|
this.parents('form').trigger('submit', [this]);
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hide tabs when only one is available.
|
* Hide tabs when only one is available.
|
||||||
* Special case is actiontabs - tabs between buttons, where we want to have
|
* Special case is actiontabs - tabs between buttons, where we want to have
|
||||||
|
@ -284,9 +284,10 @@ jQuery.noConflict();
|
|||||||
* - {Object} data Any additional data passed through to History.pushState()
|
* - {Object} data Any additional data passed through to History.pushState()
|
||||||
* - {boolean} forceReload Forces the replacement of the current history state, even if the URL is the same, i.e. allows reloading.
|
* - {boolean} forceReload Forces the replacement of the current history state, even if the URL is the same, i.e. allows reloading.
|
||||||
*/
|
*/
|
||||||
loadPanel: function(url, title, data, forceReload) {
|
loadPanel: function(url, title, data, forceReload, forceReferer) {
|
||||||
if(!data) data = {};
|
if(!data) data = {};
|
||||||
if(!title) title = "";
|
if(!title) title = "";
|
||||||
|
if (!forceReferer) forceReferer = History.getState().url;
|
||||||
|
|
||||||
// Check change tracking (can't use events as we need a way to cancel the current state change)
|
// Check change tracking (can't use events as we need a way to cancel the current state change)
|
||||||
var contentEls = this._findFragments(data.pjax ? data.pjax.split(',') : ['Content']);
|
var contentEls = this._findFragments(data.pjax ? data.pjax.split(',') : ['Content']);
|
||||||
@ -306,6 +307,7 @@ jQuery.noConflict();
|
|||||||
this.saveTabState();
|
this.saveTabState();
|
||||||
|
|
||||||
if(window.History.enabled) {
|
if(window.History.enabled) {
|
||||||
|
$.extend(data, {__forceReferer: forceReferer});
|
||||||
// Active menu item is set based on X-Controller ajax header,
|
// Active menu item is set based on X-Controller ajax header,
|
||||||
// which matches one class on the menu
|
// which matches one class on the menu
|
||||||
if(forceReload) {
|
if(forceReload) {
|
||||||
@ -458,6 +460,12 @@ jQuery.noConflict();
|
|||||||
// is fired, so the server might decide to change it based on its own logic.
|
// is fired, so the server might decide to change it based on its own logic.
|
||||||
headers['X-Pjax'] = fragments;
|
headers['X-Pjax'] = fragments;
|
||||||
|
|
||||||
|
// Set 'fake' referer - we call pushState() before making the AJAX request, so we have to
|
||||||
|
// set our own referer here
|
||||||
|
if (typeof state.data.__forceReferer !== 'undefined') {
|
||||||
|
headers['X-Backurl'] = state.data.__forceReferer;
|
||||||
|
}
|
||||||
|
|
||||||
contentEls.addClass('loading');
|
contentEls.addClass('loading');
|
||||||
var xhr = $.ajax({
|
var xhr = $.ajax({
|
||||||
headers: headers,
|
headers: headers,
|
||||||
|
@ -1,17 +1,21 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* RestfulService class allows you to consume various RESTful APIs.
|
* RestfulService class allows you to consume various RESTful APIs.
|
||||||
|
*
|
||||||
* Through this you could connect and aggregate data of various web services.
|
* Through this you could connect and aggregate data of various web services.
|
||||||
* For more info visit wiki documentation - http://doc.silverstripe.org/doku.php?id=restfulservice
|
*
|
||||||
|
* @see http://doc.silverstripe.org/framework/en/reference/restfulservice
|
||||||
*
|
*
|
||||||
* @package framework
|
* @package framework
|
||||||
* @subpackage integration
|
* @subpackage integration
|
||||||
*/
|
*/
|
||||||
class RestfulService extends ViewableData {
|
class RestfulService extends ViewableData {
|
||||||
|
|
||||||
protected $baseURL;
|
protected $baseURL;
|
||||||
protected $queryString;
|
protected $queryString;
|
||||||
protected $errorTag;
|
protected $errorTag;
|
||||||
protected $checkErrors;
|
protected $checkErrors;
|
||||||
|
protected $connectTimeout = 5;
|
||||||
protected $cache_expire;
|
protected $cache_expire;
|
||||||
protected $authUsername, $authPassword;
|
protected $authUsername, $authPassword;
|
||||||
protected $customHeaders = array();
|
protected $customHeaders = array();
|
||||||
@ -213,7 +217,6 @@ class RestfulService extends ViewableData {
|
|||||||
*/
|
*/
|
||||||
public function curlRequest($url, $method, $data = null, $headers = null, $curlOptions = array()) {
|
public function curlRequest($url, $method, $data = null, $headers = null, $curlOptions = array()) {
|
||||||
$ch = curl_init();
|
$ch = curl_init();
|
||||||
$timeout = 5;
|
|
||||||
$sapphireInfo = new SapphireInfo();
|
$sapphireInfo = new SapphireInfo();
|
||||||
$useragent = 'SilverStripe/' . $sapphireInfo->Version();
|
$useragent = 'SilverStripe/' . $sapphireInfo->Version();
|
||||||
$curlOptions = $curlOptions + (array)$this->config()->default_curl_options;
|
$curlOptions = $curlOptions + (array)$this->config()->default_curl_options;
|
||||||
@ -221,7 +224,7 @@ class RestfulService extends ViewableData {
|
|||||||
curl_setopt($ch, CURLOPT_URL, $url);
|
curl_setopt($ch, CURLOPT_URL, $url);
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||||
curl_setopt($ch, CURLOPT_USERAGENT, $useragent);
|
curl_setopt($ch, CURLOPT_USERAGENT, $useragent);
|
||||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->getConnectTimeout());
|
||||||
if(!ini_get('open_basedir')) curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1);
|
if(!ini_get('open_basedir')) curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1);
|
||||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||||
|
|
||||||
@ -553,6 +556,31 @@ class RestfulService extends ViewableData {
|
|||||||
|
|
||||||
return $output;
|
return $output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the connection timeout for the curl request in seconds.
|
||||||
|
*
|
||||||
|
* @see http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTCONNECTTIMEOUT
|
||||||
|
*
|
||||||
|
* @param int
|
||||||
|
*
|
||||||
|
* @return RestfulService
|
||||||
|
*/
|
||||||
|
public function setConnectTimeout($timeout) {
|
||||||
|
$this->connectTimeout = $timeout;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the connection timeout value.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getConnectTimeout() {
|
||||||
|
return $this->connectTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -488,6 +488,8 @@ class Controller extends RequestHandler implements TemplateGlobalProvider {
|
|||||||
if($this->request) {
|
if($this->request) {
|
||||||
if($this->request->requestVar('BackURL')) {
|
if($this->request->requestVar('BackURL')) {
|
||||||
$url = $this->request->requestVar('BackURL');
|
$url = $this->request->requestVar('BackURL');
|
||||||
|
} else if($this->request->isAjax() && $this->request->getHeader('X-Backurl')) {
|
||||||
|
$url = $this->request->getHeader('X-Backurl');
|
||||||
} else if($this->request->getHeader('Referer')) {
|
} else if($this->request->getHeader('Referer')) {
|
||||||
$url = $this->request->getHeader('Referer');
|
$url = $this->request->getHeader('Referer');
|
||||||
}
|
}
|
||||||
|
@ -347,6 +347,7 @@ class Debug {
|
|||||||
);
|
);
|
||||||
if(file_exists($errorFilePath)) {
|
if(file_exists($errorFilePath)) {
|
||||||
$content = file_get_contents(ASSETS_PATH . "/error-$statusCode.html");
|
$content = file_get_contents(ASSETS_PATH . "/error-$statusCode.html");
|
||||||
|
if(!headers_sent()) header('Content-Type: text/html');
|
||||||
// $BaseURL is left dynamic in error-###.html, so that multi-domain sites don't get broken
|
// $BaseURL is left dynamic in error-###.html, so that multi-domain sites don't get broken
|
||||||
echo str_replace('$BaseURL', Director::absoluteBaseURL(), $content);
|
echo str_replace('$BaseURL', Director::absoluteBaseURL(), $content);
|
||||||
}
|
}
|
||||||
|
@ -371,15 +371,14 @@ class InstallRequirements {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the web server is IIS.
|
* Check if the web server is IIS and version greater than the given version.
|
||||||
* @return boolean
|
* @return boolean
|
||||||
*/
|
*/
|
||||||
function isIIS($version = 7) {
|
function isIIS($fromVersion = 7) {
|
||||||
if(strpos($this->findWebserver(), 'IIS/' . $version) !== false) {
|
if(strpos($this->findWebserver(), 'IIS/') === false) {
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
return substr(strstr($this->findWebserver(), '/'), -3, 1) >= $fromVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isApache() {
|
function isApache() {
|
||||||
@ -409,7 +408,7 @@ class InstallRequirements {
|
|||||||
function check() {
|
function check() {
|
||||||
$this->errors = null;
|
$this->errors = null;
|
||||||
$isApache = $this->isApache();
|
$isApache = $this->isApache();
|
||||||
$isIIS = $this->isIIS(7);
|
$isIIS = $this->isIIS();
|
||||||
$webserver = $this->findWebserver();
|
$webserver = $this->findWebserver();
|
||||||
|
|
||||||
$this->requirePHPVersion('5.3.4', '5.3.3', array(
|
$this->requirePHPVersion('5.3.4', '5.3.3', array(
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# 3.1.2 (unreleased)
|
# 3.1.2
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ Our web-based [PHP installer](/installation) can check if you meet the requireme
|
|||||||
|
|
||||||
Hardware requirements vary widely depending on the traffic to your website, the complexity of its logic (i.e., PHP), and its size (i.e., database.) By default, all pages are dynamic, and thus access both the database and execute PHP code to generate. SilverStripe can cache full pages and segments of templates to dramatically increase performance.
|
Hardware requirements vary widely depending on the traffic to your website, the complexity of its logic (i.e., PHP), and its size (i.e., database.) By default, all pages are dynamic, and thus access both the database and execute PHP code to generate. SilverStripe can cache full pages and segments of templates to dramatically increase performance.
|
||||||
|
|
||||||
A typical website page on a conservative single CPU machine (e.g., Intel 2Ghz) takes roughly 300ms to generate. This comfortably allows over a million page views per month. Caching and other optimisations can improve this by a factor of ten or even one hundred times. SilverStripe CMS can be used in multiple-server architectures to improve scalability and redunancy.
|
A typical website page on a conservative single CPU machine (e.g., Intel 2Ghz) takes roughly 300ms to generate. This comfortably allows over a million page views per month. Caching and other optimisations can improve this by a factor of ten or even one hundred times. SilverStripe CMS can be used in multiple-server architectures to improve scalability and redundancy.
|
||||||
|
|
||||||
## Client side (CMS) requirements
|
## Client side (CMS) requirements
|
||||||
|
|
||||||
|
@ -61,13 +61,16 @@ For example, if we have a menu, we want that menu to update whenever _any_ page
|
|||||||
otherwise. By using aggregates, we can do that like this:
|
otherwise. By using aggregates, we can do that like this:
|
||||||
|
|
||||||
:::ss
|
:::ss
|
||||||
<% cached 'navigation', List(SiteTree).max(LastEdited) %>
|
<% cached 'navigation', List(SiteTree).max(LastEdited), List(SiteTree).count() %>
|
||||||
|
|
||||||
If we have a block that shows a list of categories, we can make sure the cache updates every time a category is added or
|
If we have a block that shows a list of categories, we can make sure the cache updates every time a category is added or
|
||||||
edited
|
edited
|
||||||
|
|
||||||
:::ss
|
:::ss
|
||||||
<% cached 'categorylist', List(Category).max(LastEdited) %>
|
<% cached 'categorylist', List(Category).max(LastEdited), List(Category).count() %>
|
||||||
|
|
||||||
|
Note the use of both .max(LastEdited) and .count() - this takes care of both the case where an object has been edited
|
||||||
|
since the cache was last built, and also when an object has been deleted/un-linked since the cache was last built.
|
||||||
|
|
||||||
We can also calculate aggregates on relationships. A block that shows the current member's favourites needs to update
|
We can also calculate aggregates on relationships. A block that shows the current member's favourites needs to update
|
||||||
whenever the relationship Member::$has_many = array('Favourites' => Favourite') changes.
|
whenever the relationship Member::$has_many = array('Favourites' => Favourite') changes.
|
||||||
|
@ -112,6 +112,24 @@ You can also clear specific Requirements:
|
|||||||
|
|
||||||
Caution: Depending on where you call this command, a Requirement might be *re-included* afterwards.
|
Caution: Depending on where you call this command, a Requirement might be *re-included* afterwards.
|
||||||
|
|
||||||
|
## Blocking
|
||||||
|
|
||||||
|
Requirements can also be explicitly blocked from inclusion,
|
||||||
|
which is useful to avoid conflicting JavaScript logic or CSS rules.
|
||||||
|
These blocking rules are independent of where the `block()` call is made:
|
||||||
|
It applies both for already included requirements, and ones
|
||||||
|
included after the `block()` call.
|
||||||
|
|
||||||
|
One common example is to block the core `jquery.js` include
|
||||||
|
added by various form fields and core controllers,
|
||||||
|
and use a newer version in a custom location.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
Requirements::block(THIRDPARTY_DIR . '/jquery/jquery.js');
|
||||||
|
|
||||||
|
Caution: The CMS also uses the `Requirements` system, and its operation can be
|
||||||
|
affected by `block()` calls. Avoid this by limiting the scope of
|
||||||
|
your blocking operations, e.g. in `init()` of your controller.
|
||||||
|
|
||||||
|
|
||||||
## Inclusion Order
|
## Inclusion Order
|
||||||
|
@ -211,6 +211,25 @@ You can also combine both conjunctive ("AND") and disjunctive ("OR") statements.
|
|||||||
));
|
));
|
||||||
// WHERE ("LastName" = 'Minnée' AND ("FirstName" = 'Sam' OR "Age" = '17'))
|
// WHERE ("LastName" = 'Minnée' AND ("FirstName" = 'Sam' OR "Age" = '17'))
|
||||||
|
|
||||||
|
### Filter with PHP / filterByCallback
|
||||||
|
|
||||||
|
It is also possible to filter by a PHP callback, however this will force the
|
||||||
|
data model to fetch all records and loop them in PHP, thus `filter()` or `filterAny()`
|
||||||
|
are to be preferred over `filterByCallback()`.
|
||||||
|
Please note that because `filterByCallback()` has to run in PHP, it will always return
|
||||||
|
an `ArrayList` (even if called on a `DataList`, this however might change in future).
|
||||||
|
The first parameter to the callback is the item, the second parameter is the list itself.
|
||||||
|
The callback will run once for each record, if the callback returns true, this record
|
||||||
|
will be added to the list of returned items.
|
||||||
|
The below example will get all Members that have an expired or not encrypted password.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$membersWithBadPassword = Member::get()->filterByCallback(function($item, $list) {
|
||||||
|
if ($item->isPasswordExpired() || $item->PasswordEncryption = 'none') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
### Exclude
|
### Exclude
|
||||||
|
|
||||||
The `exclude()` method is the opposite to the filter in that it removes entries
|
The `exclude()` method is the opposite to the filter in that it removes entries
|
||||||
|
@ -95,7 +95,7 @@ Creating a form is a matter of defining a method to represent that form. This
|
|||||||
method should return a form object. The constructor takes the following
|
method should return a form object. The constructor takes the following
|
||||||
arguments:
|
arguments:
|
||||||
|
|
||||||
* `$controller`: This must be and instance of the controller that contains the form, often `$this`.
|
* `$controller`: This must be an instance of the controller that contains the form, often `$this`.
|
||||||
* `$name`: This must be the name of the method on that controller that is called to return the form. The first two
|
* `$name`: This must be the name of the method on that controller that is called to return the form. The first two
|
||||||
fields allow the form object to be re-created after submission. **It's vital that they are properly set - if you ever
|
fields allow the form object to be re-created after submission. **It's vital that they are properly set - if you ever
|
||||||
have problems with form action handler not working, check that these values are correct.**
|
have problems with form action handler not working, check that these values are correct.**
|
||||||
|
@ -42,7 +42,7 @@ This is explained in a more in-depth topic at [Page Type Templates](/topics/page
|
|||||||
## Adding Database Fields
|
## Adding Database Fields
|
||||||
|
|
||||||
Adding database fields is a simple process. You define them in an array of the static variable `$db`, this array is
|
Adding database fields is a simple process. You define them in an array of the static variable `$db`, this array is
|
||||||
added on the object class. For example, Page or StaffPage. Every time you run db/build to recompile the manifest, it
|
added on the object class. For example, Page or StaffPage. Every time you run dev/build to recompile the manifest, it
|
||||||
checks if any new entries are added to the `$db` array and adds any fields to the database that are missing.
|
checks if any new entries are added to the `$db` array and adds any fields to the database that are missing.
|
||||||
|
|
||||||
For example, you may want an additional field on a `StaffPage` class which extends `Page`, called `Author`. `Author` is a
|
For example, you may want an additional field on a `StaffPage` class which extends `Page`, called `Author`. `Author` is a
|
||||||
|
@ -413,7 +413,7 @@ you need to serve directly.
|
|||||||
|
|
||||||
See [Apache](/installation/webserver) and [Nginx](/installation/nginx) installation documentation for details
|
See [Apache](/installation/webserver) and [Nginx](/installation/nginx) installation documentation for details
|
||||||
specific to your web server
|
specific to your web server
|
||||||
See [Apache](/installation/webserver) and [Nginx](/installation/nginx) installation documentation for details specific to your web server
|
|
||||||
|
|
||||||
## Passwords
|
## Passwords
|
||||||
|
|
||||||
|
@ -385,10 +385,15 @@ The controller for a page is only created when page is actually visited, while t
|
|||||||
|
|
||||||
## Creating a RSS feed
|
## Creating a RSS feed
|
||||||
|
|
||||||
An RSS feed is something that no news section should be without. SilverStripe makes it easy to create RSS feeds by providing an `[api:RSSFeed]` class to do all the hard work for us. Create the following function in the
|
An RSS feed is something that no news section should be without. SilverStripe makes it easy to create RSS feeds by providing an `[api:RSSFeed]` class to do all the hard work for us. Add the following in the *ArticleHolder_Controller* class:
|
||||||
*ArticleHolder_Controller*:
|
|
||||||
|
**mysite/code/ArticlePage.php**
|
||||||
|
|
||||||
:::php
|
:::php
|
||||||
|
private static $allowed_actions = array(
|
||||||
|
'rss'
|
||||||
|
);
|
||||||
|
|
||||||
public function rss() {
|
public function rss() {
|
||||||
$rss = new RSSFeed($this->Children(), $this->Link(), "The coolest news around");
|
$rss = new RSSFeed($this->Children(), $this->Link(), "The coolest news around");
|
||||||
return $rss->outputToBrowser();
|
return $rss->outputToBrowser();
|
||||||
@ -403,6 +408,8 @@ Depending on your browser, you should see something like the picture below. If y
|
|||||||
|
|
||||||
Now all we need is to let the user know that our RSS feed exists. Add this function to *ArticleHolder_Controller*:
|
Now all we need is to let the user know that our RSS feed exists. Add this function to *ArticleHolder_Controller*:
|
||||||
|
|
||||||
|
**mysite/code/ArticlePage.php**
|
||||||
|
|
||||||
:::php
|
:::php
|
||||||
public function init() {
|
public function init() {
|
||||||
RSSFeed::linkToFeed($this->Link() . "rss");
|
RSSFeed::linkToFeed($this->Link() . "rss");
|
||||||
|
@ -304,7 +304,7 @@ a named list of object.
|
|||||||
<% end_loop %>
|
<% end_loop %>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<% loop $Mentor %>
|
<% loop $Mentors %>
|
||||||
$Name<% if $Last !=1 %>,<% end_if %>
|
$Name<% if $Last !=1 %>,<% end_if %>
|
||||||
<% end_loop %>
|
<% end_loop %>
|
||||||
</td>
|
</td>
|
||||||
|
@ -684,7 +684,7 @@ class FormField extends RequestHandler {
|
|||||||
*/
|
*/
|
||||||
public function Field($properties = array()) {
|
public function Field($properties = array()) {
|
||||||
$obj = ($properties) ? $this->customise($properties) : $this;
|
$obj = ($properties) ? $this->customise($properties) : $this;
|
||||||
|
$this->extend('onBeforeRender', $this);
|
||||||
return $obj->renderWith($this->getTemplates());
|
return $obj->renderWith($this->getTemplates());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,9 +215,12 @@ class GridFieldAddExistingAutocompleter
|
|||||||
->limit($this->getResultsLimit());
|
->limit($this->getResultsLimit());
|
||||||
|
|
||||||
$json = array();
|
$json = array();
|
||||||
|
$originalSourceFileComments = Config::inst()->get('SSViewer', 'source_file_comments');
|
||||||
|
Config::inst()->update('SSViewer', 'source_file_comments', false);
|
||||||
foreach($results as $result) {
|
foreach($results as $result) {
|
||||||
$json[$result->ID] = SSViewer::fromString($this->resultsFormat)->process($result);
|
$json[$result->ID] = html_entity_decode(SSViewer::fromString($this->resultsFormat)->process($result));
|
||||||
}
|
}
|
||||||
|
Config::inst()->update('SSViewer', 'source_file_comments', $originalSourceFileComments);
|
||||||
return Convert::array2json($json);
|
return Convert::array2json($json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -372,8 +372,26 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
|
|||||||
$actions->push(new LiteralField('cancelbutton', $text));
|
$actions->push(new LiteralField('cancelbutton', $text));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$fields = $this->component->getFields();
|
$fields = $this->component->getFields();
|
||||||
if(!$fields) $fields = $this->record->getCMSFields();
|
if(!$fields) $fields = $this->record->getCMSFields();
|
||||||
|
|
||||||
|
// If we are creating a new record in a has-many list, then
|
||||||
|
// pre-populate the record's foreign key. Also disable the form field as
|
||||||
|
// it has no effect.
|
||||||
|
if($list instanceof HasManyList) {
|
||||||
|
$key = $list->getForeignKey();
|
||||||
|
$id = $list->getForeignID();
|
||||||
|
|
||||||
|
if(!$this->record->isInDB()) {
|
||||||
|
$this->record->$key = $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($field = $fields->dataFieldByName($key)) {
|
||||||
|
$fields->makeFieldReadonly($field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$form = new Form(
|
$form = new Form(
|
||||||
$this,
|
$this,
|
||||||
'ItemEditForm',
|
'ItemEditForm',
|
||||||
|
@ -87,6 +87,7 @@ class GridFieldSortableHeader implements GridField_HTMLProvider, GridField_DataM
|
|||||||
$state = $gridField->State->GridFieldSortableHeader;
|
$state = $gridField->State->GridFieldSortableHeader;
|
||||||
$columns = $gridField->getColumns();
|
$columns = $gridField->getColumns();
|
||||||
$currentColumn = 0;
|
$currentColumn = 0;
|
||||||
|
$list = $gridField->getList();
|
||||||
|
|
||||||
foreach($columns as $columnField) {
|
foreach($columns as $columnField) {
|
||||||
$currentColumn++;
|
$currentColumn++;
|
||||||
@ -96,7 +97,35 @@ class GridFieldSortableHeader implements GridField_HTMLProvider, GridField_DataM
|
|||||||
if(isset($this->fieldSorting[$columnField]) && $this->fieldSorting[$columnField]) {
|
if(isset($this->fieldSorting[$columnField]) && $this->fieldSorting[$columnField]) {
|
||||||
$columnField = $this->fieldSorting[$columnField];
|
$columnField = $this->fieldSorting[$columnField];
|
||||||
}
|
}
|
||||||
if($title && $gridField->getList()->canSortBy($columnField)) {
|
|
||||||
|
$allowSort = ($title && $list->canSortBy($columnField));
|
||||||
|
|
||||||
|
if(!$allowSort && strpos($columnField, '.') !== false) {
|
||||||
|
// we have a relation column with dot notation
|
||||||
|
// @see DataObject::relField for approximation
|
||||||
|
$parts = explode('.', $columnField);
|
||||||
|
$tmpItem = singleton($list->dataClass());
|
||||||
|
for($idx = 0; $idx < sizeof($parts); $idx++) {
|
||||||
|
$methodName = $parts[$idx];
|
||||||
|
if($tmpItem instanceof SS_List) {
|
||||||
|
// It's impossible to sort on a HasManyList/ManyManyList
|
||||||
|
break;
|
||||||
|
} elseif($tmpItem->hasMethod($methodName)) {
|
||||||
|
// The part is a relation name, so get the object/list from it
|
||||||
|
$tmpItem = $tmpItem->$methodName();
|
||||||
|
} elseif($tmpItem instanceof DataObject && $tmpItem->hasField($methodName)) {
|
||||||
|
// Else, if we've found a field at the end of the chain, we can sort on it.
|
||||||
|
// If a method is applied further to this field (E.g. 'Cost.Currency') then don't try to sort.
|
||||||
|
$allowSort = $idx === sizeof($parts) - 1;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
// If neither method nor field, then unable to sort
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($allowSort) {
|
||||||
$dir = 'asc';
|
$dir = 'asc';
|
||||||
if($state->SortColumn(null) == $columnField && $state->SortDirection('asc') == 'asc') {
|
if($state->SortColumn(null) == $columnField && $state->SortDirection('asc') == 'asc') {
|
||||||
$dir = 'desc';
|
$dir = 'desc';
|
||||||
@ -161,6 +190,14 @@ class GridFieldSortableHeader implements GridField_HTMLProvider, GridField_DataM
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the manipulated (sorted) DataList. Field names will simply add an 'ORDER BY'
|
||||||
|
* clause, relation names will add appropriate joins to the DataQuery first.
|
||||||
|
*
|
||||||
|
* @param GridField
|
||||||
|
* @param SS_List
|
||||||
|
* @return SS_List
|
||||||
|
*/
|
||||||
public function getManipulatedData(GridField $gridField, SS_List $dataList) {
|
public function getManipulatedData(GridField $gridField, SS_List $dataList) {
|
||||||
if(!$this->checkDataType($dataList)) return $dataList;
|
if(!$this->checkDataType($dataList)) return $dataList;
|
||||||
|
|
||||||
@ -169,6 +206,45 @@ class GridFieldSortableHeader implements GridField_HTMLProvider, GridField_DataM
|
|||||||
if (empty($sortcolumn)) {
|
if (empty($sortcolumn)) {
|
||||||
return $dataList;
|
return $dataList;
|
||||||
}
|
}
|
||||||
return $dataList->sort($sortColumn, $state->SortDirection('asc'));
|
|
||||||
|
$column = $state->SortColumn;
|
||||||
|
|
||||||
|
// if we have a relation column with dot notation
|
||||||
|
if(strpos($column, '.') !== false) {
|
||||||
|
$lastAlias = $dataList->dataClass();
|
||||||
|
$tmpItem = singleton($lastAlias);
|
||||||
|
$parts = explode('.', $state->SortColumn);
|
||||||
|
|
||||||
|
for($idx = 0; $idx < sizeof($parts); $idx++) {
|
||||||
|
$methodName = $parts[$idx];
|
||||||
|
|
||||||
|
// If we're not on the last item, we're looking at a relation
|
||||||
|
if($idx !== sizeof($parts) - 1) {
|
||||||
|
// Traverse to the relational list
|
||||||
|
$tmpItem = $tmpItem->$methodName();
|
||||||
|
|
||||||
|
// Add a left join to the query
|
||||||
|
$dataList = $dataList->leftJoin(
|
||||||
|
$tmpItem->class,
|
||||||
|
'"' . $methodName . '"."ID" = "' . $lastAlias . '"."' . $methodName . 'ID"',
|
||||||
|
$methodName
|
||||||
|
);
|
||||||
|
|
||||||
|
// Store the last 'alias' name as it'll be used for the next
|
||||||
|
// join, or the 'sort' column
|
||||||
|
$lastAlias = $methodName;
|
||||||
|
} else {
|
||||||
|
// Change relation.relation.fieldname to alias.fieldname
|
||||||
|
$column = $lastAlias . '.' . $methodName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to manually create our ORDER BY "Foo"."Bar" string for relations,
|
||||||
|
// as ->sort() won't do it by itself. Blame PostgreSQL for making this necessary
|
||||||
|
$pieces = explode('.', $column);
|
||||||
|
$column = '"' . implode('"."', $pieces) . '"';
|
||||||
|
|
||||||
|
return $dataList->sort($column, $state->SortDirection('asc'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,12 +67,16 @@ ss.i18n = {
|
|||||||
this.init();
|
this.init();
|
||||||
|
|
||||||
var langName = this.getLocale().replace(/_[\w]+/i, '');
|
var langName = this.getLocale().replace(/_[\w]+/i, '');
|
||||||
|
var defaultlangName = this.defaultLocale.replace(/_[\w]+/i, '');
|
||||||
|
|
||||||
if (this.lang && this.lang[this.getLocale()] && this.lang[this.getLocale()][entity]) {
|
if (this.lang && this.lang[this.getLocale()] && this.lang[this.getLocale()][entity]) {
|
||||||
return this.lang[this.getLocale()][entity];
|
return this.lang[this.getLocale()][entity];
|
||||||
} else if (this.lang && this.lang[langName] && this.lang[langName][entity]) {
|
} else if (this.lang && this.lang[langName] && this.lang[langName][entity]) {
|
||||||
return this.lang[langName][entity];
|
return this.lang[langName][entity];
|
||||||
} else if (this.lang && this.lang[this.defaultLocale] && this.lang[this.defaultLocale][entity]) {
|
} else if (this.lang && this.lang[this.defaultLocale] && this.lang[this.defaultLocale][entity]) {
|
||||||
return this.lang[this.defaultLocale][entity];
|
return this.lang[this.defaultLocale][entity];
|
||||||
|
} else if (this.lang && this.lang[defaultlangName] && this.lang[defaultlangName][entity]) {
|
||||||
|
return this.lang[defaultlangName][entity];
|
||||||
} else if(fallbackString) {
|
} else if(fallbackString) {
|
||||||
return fallbackString;
|
return fallbackString;
|
||||||
} else {
|
} else {
|
||||||
|
@ -476,6 +476,27 @@ class ArrayList extends ViewableData implements SS_List, SS_Filterable, SS_Sorta
|
|||||||
return $firstElement;
|
return $firstElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see SS_Filterable::filterByCallback()
|
||||||
|
*
|
||||||
|
* @example $list = $list->filterByCallback(function($item, $list) { return $item->Age == 9; })
|
||||||
|
* @param callable $callback
|
||||||
|
* @return ArrayList
|
||||||
|
*/
|
||||||
|
public function filterByCallback($callback) {
|
||||||
|
if(!is_callable($callback)) {
|
||||||
|
throw new LogicException(sprintf(
|
||||||
|
"SS_Filterable::filterByCallback() passed callback must be callable, '%s' given",
|
||||||
|
gettype($callback)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
$output = ArrayList::create();
|
||||||
|
foreach($this as $item) {
|
||||||
|
if(call_user_func($callback, $item, $this)) $output->push($item);
|
||||||
|
}
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exclude the list to not contain items with these charactaristics
|
* Exclude the list to not contain items with these charactaristics
|
||||||
*
|
*
|
||||||
|
@ -406,20 +406,24 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter this DataList by a callback function.
|
* Note that, in the current implementation, the filtered list will be an ArrayList, but this may change in a
|
||||||
* The function will be passed each record of the DataList in turn, and must return true for the record to be
|
* future implementation.
|
||||||
* included. Returns the filtered list.
|
* @see SS_Filterable::filterByCallback()
|
||||||
*
|
*
|
||||||
|
* @example $list = $list->filterByCallback(function($item, $list) { return $item->Age == 9; })
|
||||||
|
* @param callable $callback
|
||||||
* @return ArrayList (this may change in future implementations)
|
* @return ArrayList (this may change in future implementations)
|
||||||
*/
|
*/
|
||||||
public function filterByCallback($callback) {
|
public function filterByCallback($callback) {
|
||||||
if(!is_callable($callback)) {
|
if(!is_callable($callback)) {
|
||||||
throw new LogicException("DataList::filterByCallback() must be passed something callable.");
|
throw new LogicException(sprintf(
|
||||||
|
"SS_Filterable::filterByCallback() passed callback must be callable, '%s' given",
|
||||||
|
gettype($callback)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
$output = ArrayList::create();
|
||||||
$output = new ArrayList();
|
|
||||||
foreach($this as $item) {
|
foreach($this as $item) {
|
||||||
if($callback($item)) $output->push($item);
|
if(call_user_func($callback, $item, $this)) $output->push($item);
|
||||||
}
|
}
|
||||||
return $output;
|
return $output;
|
||||||
}
|
}
|
||||||
|
@ -1051,210 +1051,3 @@ abstract class SS_Database {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract query-result class.
|
|
||||||
* Once again, this should be subclassed by an actual database implementation. It will only
|
|
||||||
* ever be constructed by a subclass of SS_Database. The result of a database query - an iteratable object
|
|
||||||
* that's returned by DB::SS_Query
|
|
||||||
*
|
|
||||||
* Primarily, the SS_Query class takes care of the iterator plumbing, letting the subclasses focusing
|
|
||||||
* on providing the specific data-access methods that are required: {@link nextRecord()}, {@link numRecords()}
|
|
||||||
* and {@link seek()}
|
|
||||||
* @package framework
|
|
||||||
* @subpackage model
|
|
||||||
*/
|
|
||||||
abstract class SS_Query implements Iterator {
|
|
||||||
/**
|
|
||||||
* The current record in the interator.
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
private $currentRecord = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of the current row in the interator.
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
private $rowNum = -1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flag to keep track of whether iteration has begun, to prevent unnecessary seeks
|
|
||||||
*/
|
|
||||||
private $queryHasBegun = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return an array containing all the values from a specific column. If no column is set, then the first will be
|
|
||||||
* returned
|
|
||||||
*
|
|
||||||
* @param string $column
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function column($column = null) {
|
|
||||||
$result = array();
|
|
||||||
|
|
||||||
while($record = $this->next()) {
|
|
||||||
if($column) $result[] = $record[$column];
|
|
||||||
else $result[] = $record[key($record)];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return an array containing all values in the leftmost column, where the keys are the
|
|
||||||
* same as the values.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function keyedColumn() {
|
|
||||||
$column = array();
|
|
||||||
foreach($this as $record) {
|
|
||||||
$val = $record[key($record)];
|
|
||||||
$column[$val] = $val;
|
|
||||||
}
|
|
||||||
return $column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a map from the first column to the second column.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function map() {
|
|
||||||
$column = array();
|
|
||||||
foreach($this as $record) {
|
|
||||||
$key = reset($record);
|
|
||||||
$val = next($record);
|
|
||||||
$column[$key] = $val;
|
|
||||||
}
|
|
||||||
return $column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the next record in the iterator.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function record() {
|
|
||||||
return $this->next();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the first column of the first record.
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function value() {
|
|
||||||
$record = $this->next();
|
|
||||||
if($record) return $record[key($record)];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return an HTML table containing the full result-set
|
|
||||||
*/
|
|
||||||
public function table() {
|
|
||||||
$first = true;
|
|
||||||
$result = "<table>\n";
|
|
||||||
|
|
||||||
foreach($this as $record) {
|
|
||||||
if($first) {
|
|
||||||
$result .= "<tr>";
|
|
||||||
foreach($record as $k => $v) {
|
|
||||||
$result .= "<th>" . Convert::raw2xml($k) . "</th> ";
|
|
||||||
}
|
|
||||||
$result .= "</tr> \n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$result .= "<tr>";
|
|
||||||
foreach($record as $k => $v) {
|
|
||||||
$result .= "<td>" . Convert::raw2xml($v) . "</td> ";
|
|
||||||
}
|
|
||||||
$result .= "</tr> \n";
|
|
||||||
|
|
||||||
$first = false;
|
|
||||||
}
|
|
||||||
$result .= "</table>\n";
|
|
||||||
|
|
||||||
if($first) return "No records found";
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterator function implementation. Rewind the iterator to the first item and return it.
|
|
||||||
* Makes use of {@link seek()} and {@link numRecords()}, takes care of the plumbing.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function rewind() {
|
|
||||||
if($this->queryHasBegun && $this->numRecords() > 0) {
|
|
||||||
$this->queryHasBegun = false;
|
|
||||||
return $this->seek(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterator function implementation. Return the current item of the iterator.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function current() {
|
|
||||||
if(!$this->currentRecord) {
|
|
||||||
return $this->next();
|
|
||||||
} else {
|
|
||||||
return $this->currentRecord;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterator function implementation. Return the first item of this iterator.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function first() {
|
|
||||||
$this->rewind();
|
|
||||||
return $this->current();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterator function implementation. Return the row number of the current item.
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function key() {
|
|
||||||
return $this->rowNum;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterator function implementation. Return the next record in the iterator.
|
|
||||||
* Makes use of {@link nextRecord()}, takes care of the plumbing.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function next() {
|
|
||||||
$this->queryHasBegun = true;
|
|
||||||
$this->currentRecord = $this->nextRecord();
|
|
||||||
$this->rowNum++;
|
|
||||||
return $this->currentRecord;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterator function implementation. Check if the iterator is pointing to a valid item.
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
public function valid() {
|
|
||||||
if(!$this->queryHasBegun) $this->next();
|
|
||||||
return $this->currentRecord !== false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the next record in the query result.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
abstract public function nextRecord();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the total number of items in the query result.
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
abstract public function numRecords();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Go to a specific row number in the query result and return the record.
|
|
||||||
* @param int $rowNum Tow number to go to.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
abstract public function seek($rowNum);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,4 +44,13 @@ interface SS_Filterable {
|
|||||||
*/
|
*/
|
||||||
public function exclude();
|
public function exclude();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a new instance of this list that excludes any items with these charactaristics
|
||||||
|
* Filter this List by a callback function. The function will be passed each record of the List in turn,
|
||||||
|
* and must return true for the record to be included. Returns the filtered list.
|
||||||
|
*
|
||||||
|
* @example $list = $list->filterByCallback(function($item, $list) { return $item->Age == 9; })
|
||||||
|
* @return SS_Filterable
|
||||||
|
*/
|
||||||
|
public function filterByCallback($callback);
|
||||||
}
|
}
|
||||||
|
@ -147,6 +147,29 @@ abstract class SS_ListDecorator extends ViewableData implements SS_List, SS_Sort
|
|||||||
return call_user_func_array(array($this->list, 'filter'), $args);
|
return call_user_func_array(array($this->list, 'filter'), $args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note that, in the current implementation, the filtered list will be an ArrayList, but this may change in a
|
||||||
|
* future implementation.
|
||||||
|
* @see SS_Filterable::filterByCallback()
|
||||||
|
*
|
||||||
|
* @example $list = $list->filterByCallback(function($item, $list) { return $item->Age == 9; })
|
||||||
|
* @param callable $callback
|
||||||
|
* @return ArrayList (this may change in future implementations)
|
||||||
|
*/
|
||||||
|
public function filterByCallback($callback) {
|
||||||
|
if(!is_callable($callback)) {
|
||||||
|
throw new LogicException(sprintf(
|
||||||
|
"SS_Filterable::filterByCallback() passed callback must be callable, '%s' given",
|
||||||
|
gettype($callback)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
$output = ArrayList::create();
|
||||||
|
foreach($this->list as $item) {
|
||||||
|
if(call_user_func($callback, $item, $this->list)) $output->push($item);
|
||||||
|
}
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
public function limit($limit, $offset = 0) {
|
public function limit($limit, $offset = 0) {
|
||||||
return $this->list->limit($limit, $offset);
|
return $this->list->limit($limit, $offset);
|
||||||
}
|
}
|
||||||
|
328
model/Map.php
328
model/Map.php
@ -7,12 +7,26 @@
|
|||||||
* @subpackage model
|
* @subpackage model
|
||||||
*/
|
*/
|
||||||
class SS_Map implements ArrayAccess, Countable, IteratorAggregate {
|
class SS_Map implements ArrayAccess, Countable, IteratorAggregate {
|
||||||
|
|
||||||
protected $list, $keyField, $valueField;
|
protected $list, $keyField, $valueField;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see SS_Map::unshift()
|
||||||
|
*
|
||||||
|
* @var array $firstItems
|
||||||
|
*/
|
||||||
protected $firstItems = array();
|
protected $firstItems = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see SS_Map::push()
|
||||||
|
*
|
||||||
|
* @var array $lastItems
|
||||||
|
*/
|
||||||
|
protected $lastItems = array();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a new map around an SS_list.
|
* Construct a new map around an SS_list.
|
||||||
|
*
|
||||||
* @param $list The list to build a map from
|
* @param $list The list to build a map from
|
||||||
* @param $keyField The field to use as the key of each map entry
|
* @param $keyField The field to use as the key of each map entry
|
||||||
* @param $valueField The field to use as the value of each map entry
|
* @param $valueField The field to use as the value of each map entry
|
||||||
@ -24,182 +38,355 @@ class SS_Map implements ArrayAccess, Countable, IteratorAggregate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the key field for this map
|
* Set the key field for this map.
|
||||||
|
*
|
||||||
|
* @var string $keyField
|
||||||
*/
|
*/
|
||||||
public function setKeyField($keyField) {
|
public function setKeyField($keyField) {
|
||||||
$this->keyField = $keyField;
|
$this->keyField = $keyField;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the value field for this map
|
* Set the value field for this map.
|
||||||
|
*
|
||||||
|
* @var string $valueField
|
||||||
*/
|
*/
|
||||||
public function setValueField($valueField) {
|
public function setValueField($valueField) {
|
||||||
$this->valueField = $valueField;
|
$this->valueField = $valueField;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an array equivalent to this map
|
* Return an array equivalent to this map.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function toArray() {
|
public function toArray() {
|
||||||
$array = array();
|
$array = array();
|
||||||
|
|
||||||
foreach($this as $k => $v) {
|
foreach($this as $k => $v) {
|
||||||
$array[$k] = $v;
|
$array[$k] = $v;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $array;
|
return $array;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return all the keys of this map
|
* Return all the keys of this map.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function keys() {
|
public function keys() {
|
||||||
$output = array();
|
return array_keys($this->toArray());
|
||||||
foreach($this as $k => $v) {
|
|
||||||
$output[] = $k;
|
|
||||||
}
|
|
||||||
return $output;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return all the values of this map
|
* Return all the values of this map.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function values() {
|
public function values() {
|
||||||
$output = array();
|
return array_values($this->toArray());
|
||||||
foreach($this as $k => $v) {
|
|
||||||
$output[] = $v;
|
|
||||||
}
|
|
||||||
return $output;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unshift an item onto the start of the map
|
* Unshift an item onto the start of the map.
|
||||||
|
*
|
||||||
|
* Stores the value in addition to the {@link DataQuery} for the map.
|
||||||
|
*
|
||||||
|
* @var string $key
|
||||||
|
* @var mixed $value
|
||||||
*/
|
*/
|
||||||
public function unshift($key, $value) {
|
public function unshift($key, $value) {
|
||||||
$oldItems = $this->firstItems;
|
$oldItems = $this->firstItems;
|
||||||
$this->firstItems = array($key => $value);
|
$this->firstItems = array(
|
||||||
if($oldItems) $this->firstItems = $this->firstItems + $oldItems;
|
$key => $value
|
||||||
|
);
|
||||||
|
|
||||||
|
if($oldItems) {
|
||||||
|
$this->firstItems = $this->firstItems + $oldItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pushes an item onto the end of the map.
|
||||||
|
*
|
||||||
|
* @var string $key
|
||||||
|
* @var mixed $value
|
||||||
|
*/
|
||||||
|
public function push($key, $value) {
|
||||||
|
$oldItems = $this->lastItems;
|
||||||
|
|
||||||
|
$this->lastItems = array(
|
||||||
|
$key => $value
|
||||||
|
);
|
||||||
|
|
||||||
|
if($oldItems) {
|
||||||
|
$this->lastItems = $this->lastItems + $oldItems;
|
||||||
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ArrayAccess
|
// ArrayAccess
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string $key
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
public function offsetExists($key) {
|
public function offsetExists($key) {
|
||||||
if(isset($this->firstItems[$key])) return true;
|
if(isset($this->firstItems[$key])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isset($this->lastItems[$key])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
$record = $this->list->find($this->keyField, $key);
|
$record = $this->list->find($this->keyField, $key);
|
||||||
|
|
||||||
return $record != null;
|
return $record != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string $key
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
public function offsetGet($key) {
|
public function offsetGet($key) {
|
||||||
if(isset($this->firstItems[$key])) return $this->firstItems[$key];
|
if(isset($this->firstItems[$key])) {
|
||||||
|
return $this->firstItems[$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isset($this->lastItems[$key])) {
|
||||||
|
return $this->lastItems[$key];
|
||||||
|
}
|
||||||
|
|
||||||
$record = $this->list->find($this->keyField, $key);
|
$record = $this->list->find($this->keyField, $key);
|
||||||
|
|
||||||
if($record) {
|
if($record) {
|
||||||
$col = $this->valueField;
|
$col = $this->valueField;
|
||||||
return $record->$col;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public function offsetSet($key, $value) {
|
|
||||||
if(isset($this->firstItems[$key])) return $this->firstItems[$key] = $value;
|
|
||||||
|
|
||||||
user_error("SS_Map is read-only", E_USER_ERROR);
|
return $record->$col;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a value in the map by a given key that has been set via
|
||||||
|
* {@link SS_Map::push()} or {@link SS_Map::unshift()}
|
||||||
|
*
|
||||||
|
* Keys in the map cannot be set since these values are derived from a
|
||||||
|
* {@link DataQuery} instance. In this case, use {@link SS_Map::toArray()}
|
||||||
|
* and manipulate the resulting array.
|
||||||
|
*
|
||||||
|
* @var string $key
|
||||||
|
* @var mixed $value
|
||||||
|
*/
|
||||||
|
public function offsetSet($key, $value) {
|
||||||
|
if(isset($this->firstItems[$key])) {
|
||||||
|
return $this->firstItems[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isset($this->lastItems[$key])) {
|
||||||
|
return $this->lastItems[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
user_error(
|
||||||
|
"SS_Map is read-only. Please use $map->push($key, $value) to append values",
|
||||||
|
E_USER_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a value in the map by a given key which has been added to the map
|
||||||
|
* via {@link SS_Map::push()} or {@link SS_Map::unshift()}
|
||||||
|
*
|
||||||
|
* Keys in the map cannot be unset since these values are derived from a
|
||||||
|
* {@link DataQuery} instance. In this case, use {@link SS_Map::toArray()}
|
||||||
|
* and manipulate the resulting array.
|
||||||
|
*
|
||||||
|
* @var string $key
|
||||||
|
* @var mixed $value
|
||||||
|
*/
|
||||||
public function offsetUnset($key) {
|
public function offsetUnset($key) {
|
||||||
if(isset($this->firstItems[$key])) {
|
if(isset($this->firstItems[$key])) {
|
||||||
unset($this->firstItems[$key]);
|
unset($this->firstItems[$key]);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
user_error("SS_Map is read-only", E_USER_ERROR);
|
if(isset($this->lastItems[$key])) {
|
||||||
|
unset($this->lastItems[$key]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
user_error(
|
||||||
|
"SS_Map is read-only. Unset cannot be called on keys derived from the DataQuery",
|
||||||
|
E_USER_ERROR
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// IteratorAggreagte
|
/**
|
||||||
|
* Returns an SS_Map_Iterator instance for iterating over the complete set
|
||||||
|
* of items in the map.
|
||||||
|
*
|
||||||
|
* Satisfies the IteratorAggreagte interface.
|
||||||
|
*
|
||||||
|
* @return SS_Map_Iterator
|
||||||
|
*/
|
||||||
public function getIterator() {
|
public function getIterator() {
|
||||||
return new SS_Map_Iterator($this->list->getIterator(), $this->keyField, $this->valueField, $this->firstItems);
|
return new SS_Map_Iterator(
|
||||||
|
$this->list->getIterator(),
|
||||||
|
$this->keyField,
|
||||||
|
$this->valueField,
|
||||||
|
$this->firstItems,
|
||||||
|
$this->lastItems
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Countable
|
/**
|
||||||
|
* Returns the count of items in the list including the additional items set
|
||||||
|
* through {@link SS_Map::push()} and {@link SS_Map::unshift}.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
public function count() {
|
public function count() {
|
||||||
return $this->list->count();
|
return $this->list->count() +
|
||||||
|
count($this->firstItems) +
|
||||||
|
count($this->lastItems);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a map iterator around an Iterator. Called by SS_Map
|
* Builds a map iterator around an Iterator. Called by SS_Map
|
||||||
|
*
|
||||||
* @package framework
|
* @package framework
|
||||||
* @subpackage model
|
* @subpackage model
|
||||||
*/
|
*/
|
||||||
class SS_Map_Iterator implements Iterator {
|
class SS_Map_Iterator implements Iterator {
|
||||||
|
|
||||||
protected $items;
|
protected $items;
|
||||||
protected $keyField, $titleField;
|
protected $keyField, $titleField;
|
||||||
|
|
||||||
protected $firstItemIdx = 0;
|
protected $firstItemIdx = 0;
|
||||||
|
|
||||||
|
protected $endItemIdx;
|
||||||
|
|
||||||
protected $firstItems = array();
|
protected $firstItems = array();
|
||||||
|
protected $lastItems = array();
|
||||||
|
|
||||||
protected $excludedItems = array();
|
protected $excludedItems = array();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $items The iterator to build this map from
|
* @param Iterator $items The iterator to build this map from
|
||||||
* @param $keyField The field to use for the keys
|
* @param string $keyField The field to use for the keys
|
||||||
* @param $titleField The field to use for the values
|
* @param string $titleField The field to use for the values
|
||||||
* @param $fistItems An optional map of items to show first
|
* @param array $fristItems An optional map of items to show first
|
||||||
|
* @param array $lastItems An optional map of items to show last
|
||||||
*/
|
*/
|
||||||
public function __construct(Iterator $items, $keyField, $titleField, $firstItems = null) {
|
public function __construct(Iterator $items, $keyField, $titleField, $firstItems = null, $lastItems = null) {
|
||||||
$this->items = $items;
|
$this->items = $items;
|
||||||
$this->keyField = $keyField;
|
$this->keyField = $keyField;
|
||||||
$this->titleField = $titleField;
|
$this->titleField = $titleField;
|
||||||
|
$this->endItemIdx = null;
|
||||||
|
|
||||||
foreach($firstItems as $k => $v) {
|
if($firstItems) {
|
||||||
$this->firstItems[] = array($k,$v);
|
foreach($firstItems as $k => $v) {
|
||||||
$this->excludedItems[] = $k;
|
$this->firstItems[] = array($k,$v);
|
||||||
|
$this->excludedItems[] = $k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($lastItems) {
|
||||||
|
foreach($lastItems as $k => $v) {
|
||||||
|
$this->lastItems[] = array($k, $v);
|
||||||
|
$this->excludedItems[] = $k;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterator functions
|
/**
|
||||||
|
* Rewind the Iterator to the first element.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
public function rewind() {
|
public function rewind() {
|
||||||
$this->firstItemIdx = 0;
|
$this->firstItemIdx = 0;
|
||||||
|
$this->endItemIdx = null;
|
||||||
|
|
||||||
$rewoundItem = $this->items->rewind();
|
$rewoundItem = $this->items->rewind();
|
||||||
|
|
||||||
if(isset($this->firstItems[$this->firstItemIdx])) {
|
if(isset($this->firstItems[$this->firstItemIdx])) {
|
||||||
return $this->firstItems[$this->firstItemIdx][1];
|
return $this->firstItems[$this->firstItemIdx][1];
|
||||||
} else {
|
} else {
|
||||||
if($rewoundItem) return ($rewoundItem->hasMethod($this->titleField))
|
if($rewoundItem) {
|
||||||
? $rewoundItem->{$this->titleField}()
|
if($rewoundItem->hasMethod($this->titleField)) {
|
||||||
: $rewoundItem->{$this->titleField};
|
return $rewoundItem->{$this->titleField}();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $rewoundItem->{$this->titleField};
|
||||||
|
} else if(!$this->items->valid() && $this->lastItems) {
|
||||||
|
$this->endItemIdx = 0;
|
||||||
|
|
||||||
|
return $this->lastItems[0][1];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the current element.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
public function current() {
|
public function current() {
|
||||||
if(isset($this->firstItems[$this->firstItemIdx])) {
|
if(($this->endItemIdx !== null) && isset($this->lastItems[$this->endItemIdx])) {
|
||||||
|
return $this->lastItems[$this->endItemIdx][1];
|
||||||
|
} else if(isset($this->firstItems[$this->firstItemIdx])) {
|
||||||
return $this->firstItems[$this->firstItemIdx][1];
|
return $this->firstItems[$this->firstItemIdx][1];
|
||||||
} else {
|
} else {
|
||||||
return ($this->items->current()->hasMethod($this->titleField))
|
if($this->items->current()->hasMethod($this->titleField)) {
|
||||||
? $this->items->current()->{$this->titleField}()
|
return $this->items->current()->{$this->titleField}();
|
||||||
: $this->items->current()->{$this->titleField};
|
}
|
||||||
|
|
||||||
|
return $this->items->current()->{$this->titleField};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the key of the current element.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
public function key() {
|
public function key() {
|
||||||
if(isset($this->firstItems[$this->firstItemIdx])) {
|
if(($this->endItemIdx !== null) && isset($this->lastItems[$this->endItemIdx])) {
|
||||||
|
return $this->lastItems[$this->endItemIdx][0];
|
||||||
|
} else if(isset($this->firstItems[$this->firstItemIdx])) {
|
||||||
return $this->firstItems[$this->firstItemIdx][0];
|
return $this->firstItems[$this->firstItemIdx][0];
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return $this->items->current()->{$this->keyField};
|
return $this->items->current()->{$this->keyField};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move forward to next element.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
public function next() {
|
public function next() {
|
||||||
$this->firstItemIdx++;
|
$this->firstItemIdx++;
|
||||||
|
|
||||||
if(isset($this->firstItems[$this->firstItemIdx])) {
|
if(isset($this->firstItems[$this->firstItemIdx])) {
|
||||||
return $this->firstItems[$this->firstItemIdx][1];
|
return $this->firstItems[$this->firstItemIdx][1];
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if(!isset($this->firstItems[$this->firstItemIdx-1])) $this->items->next();
|
if(!isset($this->firstItems[$this->firstItemIdx-1])) {
|
||||||
|
$this->items->next();
|
||||||
|
}
|
||||||
|
|
||||||
if($this->excludedItems) {
|
if($this->excludedItems) {
|
||||||
while(($c = $this->items->current()) && in_array($c->{$this->keyField}, $this->excludedItems, true)) {
|
while(($c = $this->items->current()) && in_array($c->{$this->keyField}, $this->excludedItems, true)) {
|
||||||
@ -207,9 +394,34 @@ class SS_Map_Iterator implements Iterator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!$this->items->valid()) {
|
||||||
|
// iterator has passed the preface items, off the end of the items
|
||||||
|
// list. Track through the end items to go through to the next
|
||||||
|
if($this->endItemIdx === null) {
|
||||||
|
$this->endItemIdx = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->endItemIdx++;
|
||||||
|
|
||||||
|
if(isset($this->lastItems[$this->endItemIdx])) {
|
||||||
|
return $this->lastItems[$this->endItemIdx];
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if current position is valid.
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
public function valid() {
|
public function valid() {
|
||||||
return $this->items->valid();
|
return (
|
||||||
|
(isset($this->firstItems[$this->firstItemIdx])) ||
|
||||||
|
(($this->endItemIdx !== null) && isset($this->lastItems[$this->endItemIdx])) ||
|
||||||
|
$this->items->valid()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1210,52 +1210,3 @@ class MySQLDatabase extends SS_Database {
|
|||||||
return Convert::raw2sql($this->database . '_' . Convert::raw2sql($name));
|
return Convert::raw2sql($this->database . '_' . Convert::raw2sql($name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A result-set from a MySQL database.
|
|
||||||
* @package framework
|
|
||||||
* @subpackage model
|
|
||||||
*/
|
|
||||||
class MySQLQuery extends SS_Query {
|
|
||||||
/**
|
|
||||||
* The MySQLDatabase object that created this result set.
|
|
||||||
* @var MySQLDatabase
|
|
||||||
*/
|
|
||||||
protected $database;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The internal MySQL handle that points to the result set.
|
|
||||||
* @var resource
|
|
||||||
*/
|
|
||||||
protected $handle;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook the result-set given into a Query class, suitable for use by SilverStripe.
|
|
||||||
* @param database The database object that created this query.
|
|
||||||
* @param handle the internal mysql handle that is points to the resultset.
|
|
||||||
*/
|
|
||||||
public function __construct(MySQLDatabase $database, $handle) {
|
|
||||||
$this->database = $database;
|
|
||||||
$this->handle = $handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __destruct() {
|
|
||||||
if(is_object($this->handle)) $this->handle->free();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function seek($row) {
|
|
||||||
if(is_object($this->handle)) return $this->handle->data_seek($row);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function numRecords() {
|
|
||||||
if(is_object($this->handle)) return $this->handle->num_rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function nextRecord() {
|
|
||||||
if(is_object($this->handle) && ($data = $this->handle->fetch_assoc())) {
|
|
||||||
return $data;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
69
model/MySQLQuery.php
Normal file
69
model/MySQLQuery.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A result-set from a MySQL database.
|
||||||
|
*
|
||||||
|
* @package framework
|
||||||
|
* @subpackage model
|
||||||
|
*/
|
||||||
|
class MySQLQuery extends SS_Query {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The MySQLDatabase object that created this result set.
|
||||||
|
* @var MySQLDatabase
|
||||||
|
*/
|
||||||
|
protected $database;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The internal MySQL handle that points to the result set.
|
||||||
|
* @var resource
|
||||||
|
*/
|
||||||
|
protected $handle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook the result-set given into a Query class, suitable for use by
|
||||||
|
* SilverStripe.
|
||||||
|
*
|
||||||
|
* @param database $database The database object that created this query.
|
||||||
|
* @param handle $handle the internal mysql handle that is points to the resultset.
|
||||||
|
*/
|
||||||
|
public function __construct(MySQLDatabase $database, $handle) {
|
||||||
|
$this->database = $database;
|
||||||
|
$this->handle = $handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __destruct() {
|
||||||
|
if(is_object($this->handle)) {
|
||||||
|
$this->handle->free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function seek($row) {
|
||||||
|
if(is_object($this->handle)) {
|
||||||
|
return $this->handle->data_seek($row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function numRecords() {
|
||||||
|
if(is_object($this->handle)) {
|
||||||
|
return $this->handle->num_rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function nextRecord() {
|
||||||
|
if(is_object($this->handle) && ($data = $this->handle->fetch_assoc())) {
|
||||||
|
return $data;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
252
model/Query.php
Normal file
252
model/Query.php
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract query-result class.
|
||||||
|
*
|
||||||
|
* Once again, this should be subclassed by an actual database implementation
|
||||||
|
* such as {@link MySQLQuery}.
|
||||||
|
*
|
||||||
|
* It will only ever be constructed by a subclass of {@link SS_Database} and
|
||||||
|
* contain the result of a database query as an iteratable object.
|
||||||
|
*
|
||||||
|
* Primarily, the SS_Query class takes care of the iterator plumbing, letting
|
||||||
|
* the subclasses focusing on providing the specific data-access methods that
|
||||||
|
* are required: {@link nextRecord()}, {@link numRecords()} and {@link seek()}
|
||||||
|
*
|
||||||
|
* @package framework
|
||||||
|
* @subpackage model
|
||||||
|
*/
|
||||||
|
abstract class SS_Query implements Iterator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current record in the interator.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $currentRecord = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of the current row in the interator.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $rowNum = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to keep track of whether iteration has begun, to prevent unnecessary
|
||||||
|
* seeks.
|
||||||
|
*
|
||||||
|
* @var boolean
|
||||||
|
*/
|
||||||
|
private $queryHasBegun = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array containing all the values from a specific column. If no
|
||||||
|
* column is set, then the first will be returned.
|
||||||
|
*
|
||||||
|
* @param string $column
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function column($column = null) {
|
||||||
|
$result = array();
|
||||||
|
|
||||||
|
while($record = $this->next()) {
|
||||||
|
if($column) {
|
||||||
|
$result[] = $record[$column];
|
||||||
|
} else {
|
||||||
|
$result[] = $record[key($record)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array containing all values in the leftmost column, where the
|
||||||
|
* keys are the same as the values.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function keyedColumn() {
|
||||||
|
$column = array();
|
||||||
|
|
||||||
|
foreach($this as $record) {
|
||||||
|
$val = $record[key($record)];
|
||||||
|
$column[$val] = $val;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a map from the first column to the second column.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function map() {
|
||||||
|
$column = array();
|
||||||
|
|
||||||
|
foreach($this as $record) {
|
||||||
|
$key = reset($record);
|
||||||
|
$val = next($record);
|
||||||
|
$column[$key] = $val;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next record in the iterator.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function record() {
|
||||||
|
return $this->next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first column of the first record.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function value() {
|
||||||
|
$record = $this->next();
|
||||||
|
|
||||||
|
if($record) {
|
||||||
|
return $record[key($record)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an HTML table containing the full result-set.
|
||||||
|
*/
|
||||||
|
public function table() {
|
||||||
|
$first = true;
|
||||||
|
$result = "<table>\n";
|
||||||
|
|
||||||
|
foreach($this as $record) {
|
||||||
|
if($first) {
|
||||||
|
$result .= "<tr>";
|
||||||
|
foreach($record as $k => $v) {
|
||||||
|
$result .= "<th>" . Convert::raw2xml($k) . "</th> ";
|
||||||
|
}
|
||||||
|
$result .= "</tr> \n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$result .= "<tr>";
|
||||||
|
foreach($record as $k => $v) {
|
||||||
|
$result .= "<td>" . Convert::raw2xml($v) . "</td> ";
|
||||||
|
}
|
||||||
|
$result .= "</tr> \n";
|
||||||
|
|
||||||
|
$first = false;
|
||||||
|
}
|
||||||
|
$result .= "</table>\n";
|
||||||
|
|
||||||
|
if($first) return "No records found";
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterator function implementation. Rewind the iterator to the first item
|
||||||
|
* and return it.
|
||||||
|
*
|
||||||
|
* Makes use of {@link seek()} and {@link numRecords()}, takes care of the
|
||||||
|
* plumbing.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function rewind() {
|
||||||
|
if($this->queryHasBegun && $this->numRecords() > 0) {
|
||||||
|
$this->queryHasBegun = false;
|
||||||
|
|
||||||
|
return $this->seek(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterator function implementation. Return the current item of the
|
||||||
|
* iterator.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function current() {
|
||||||
|
if(!$this->currentRecord) {
|
||||||
|
return $this->next();
|
||||||
|
} else {
|
||||||
|
return $this->currentRecord;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterator function implementation. Return the first item of this iterator.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function first() {
|
||||||
|
$this->rewind();
|
||||||
|
|
||||||
|
return $this->current();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterator function implementation. Return the row number of the current
|
||||||
|
* item.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function key() {
|
||||||
|
return $this->rowNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterator function implementation. Return the next record in the iterator.
|
||||||
|
*
|
||||||
|
* Makes use of {@link nextRecord()}, takes care of the plumbing.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function next() {
|
||||||
|
$this->queryHasBegun = true;
|
||||||
|
$this->currentRecord = $this->nextRecord();
|
||||||
|
$this->rowNum++;
|
||||||
|
return $this->currentRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterator function implementation. Check if the iterator is pointing to a
|
||||||
|
* valid item.
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function valid() {
|
||||||
|
if(!$this->queryHasBegun) {
|
||||||
|
$this->next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->currentRecord !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the next record in the query result.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
abstract public function nextRecord();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the total number of items in the query result.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
abstract public function numRecords();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go to a specific row number in the query result and return the record.
|
||||||
|
*
|
||||||
|
* @param int $rowNum Tow number to go to.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
abstract public function seek($rowNum);
|
||||||
|
}
|
@ -20,8 +20,7 @@ require_once 'PHPUnit/Framework/Assert/Functions.php';
|
|||||||
* Context automatically loaded by Behat.
|
* Context automatically loaded by Behat.
|
||||||
* Uses subcontexts to extend functionality.
|
* Uses subcontexts to extend functionality.
|
||||||
*/
|
*/
|
||||||
class FeatureContext extends SilverStripeContext
|
class FeatureContext extends SilverStripeContext {
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var FixtureFactory
|
* @var FixtureFactory
|
||||||
@ -34,8 +33,7 @@ class FeatureContext extends SilverStripeContext
|
|||||||
*
|
*
|
||||||
* @param array $parameters context parameters (set them up through behat.yml)
|
* @param array $parameters context parameters (set them up through behat.yml)
|
||||||
*/
|
*/
|
||||||
public function __construct(array $parameters)
|
public function __construct(array $parameters) {
|
||||||
{
|
|
||||||
parent::__construct($parameters);
|
parent::__construct($parameters);
|
||||||
|
|
||||||
$this->useContext('BasicContext', new BasicContext($parameters));
|
$this->useContext('BasicContext', new BasicContext($parameters));
|
||||||
@ -57,8 +55,7 @@ class FeatureContext extends SilverStripeContext
|
|||||||
$factory->define('Member', $blueprint);
|
$factory->define('Member', $blueprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setMinkParameters(array $parameters)
|
public function setMinkParameters(array $parameters) {
|
||||||
{
|
|
||||||
parent::setMinkParameters($parameters);
|
parent::setMinkParameters($parameters);
|
||||||
|
|
||||||
if(isset($parameters['files_path'])) {
|
if(isset($parameters['files_path'])) {
|
||||||
|
@ -22,8 +22,7 @@ require_once 'PHPUnit/Framework/Assert/Functions.php';
|
|||||||
*
|
*
|
||||||
* Context used to define steps related to forms inside CMS.
|
* Context used to define steps related to forms inside CMS.
|
||||||
*/
|
*/
|
||||||
class CmsFormsContext extends BehatContext
|
class CmsFormsContext extends BehatContext {
|
||||||
{
|
|
||||||
protected $context;
|
protected $context;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,8 +31,7 @@ class CmsFormsContext extends BehatContext
|
|||||||
*
|
*
|
||||||
* @param array $parameters context parameters (set them up through behat.yml)
|
* @param array $parameters context parameters (set them up through behat.yml)
|
||||||
*/
|
*/
|
||||||
public function __construct(array $parameters)
|
public function __construct(array $parameters) {
|
||||||
{
|
|
||||||
// Initialize your context here
|
// Initialize your context here
|
||||||
$this->context = $parameters;
|
$this->context = $parameters;
|
||||||
}
|
}
|
||||||
@ -41,28 +39,29 @@ class CmsFormsContext extends BehatContext
|
|||||||
/**
|
/**
|
||||||
* Get Mink session from MinkContext
|
* Get Mink session from MinkContext
|
||||||
*/
|
*/
|
||||||
public function getSession($name = null)
|
public function getSession($name = null) {
|
||||||
{
|
|
||||||
return $this->getMainContext()->getSession($name);
|
return $this->getMainContext()->getSession($name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Then /^I should see an edit page form$/
|
* @Then /^I should( not? |\s*)see an edit page form$/
|
||||||
*/
|
*/
|
||||||
public function stepIShouldSeeAnEditPageForm()
|
public function stepIShouldSeeAnEditPageForm($negative) {
|
||||||
{
|
|
||||||
$page = $this->getSession()->getPage();
|
$page = $this->getSession()->getPage();
|
||||||
|
|
||||||
$form = $page->find('css', '#Form_EditForm');
|
$form = $page->find('css', '#Form_EditForm');
|
||||||
assertNotNull($form, 'I should see an edit page form');
|
if(trim($negative)) {
|
||||||
|
assertNull($form, 'I should not see an edit page form');
|
||||||
|
} else {
|
||||||
|
assertNotNull($form, 'I should see an edit page form');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @When /^I fill in the "(?P<field>([^"]*))" HTML field with "(?P<value>([^"]*))"$/
|
* @When /^I fill in the "(?P<field>([^"]*))" HTML field with "(?P<value>([^"]*))"$/
|
||||||
* @When /^I fill in "(?P<value>([^"]*))" for the "(?P<field>([^"]*))" HTML field$/
|
* @When /^I fill in "(?P<value>([^"]*))" for the "(?P<field>([^"]*))" HTML field$/
|
||||||
*/
|
*/
|
||||||
public function stepIFillInTheHtmlFieldWith($field, $value)
|
public function stepIFillInTheHtmlFieldWith($field, $value) {
|
||||||
{
|
|
||||||
$page = $this->getSession()->getPage();
|
$page = $this->getSession()->getPage();
|
||||||
$inputField = $page->findField($field);
|
$inputField = $page->findField($field);
|
||||||
assertNotNull($inputField, sprintf('HTML field "%s" not found', $field));
|
assertNotNull($inputField, sprintf('HTML field "%s" not found', $field));
|
||||||
@ -77,8 +76,7 @@ class CmsFormsContext extends BehatContext
|
|||||||
/**
|
/**
|
||||||
* @When /^I append "(?P<value>([^"]*))" to the "(?P<field>([^"]*))" HTML field$/
|
* @When /^I append "(?P<value>([^"]*))" to the "(?P<field>([^"]*))" HTML field$/
|
||||||
*/
|
*/
|
||||||
public function stepIAppendTotheHtmlField($field, $value)
|
public function stepIAppendTotheHtmlField($field, $value) {
|
||||||
{
|
|
||||||
$page = $this->getSession()->getPage();
|
$page = $this->getSession()->getPage();
|
||||||
$inputField = $page->findField($field);
|
$inputField = $page->findField($field);
|
||||||
assertNotNull($inputField, sprintf('HTML field "%s" not found', $field));
|
assertNotNull($inputField, sprintf('HTML field "%s" not found', $field));
|
||||||
@ -93,13 +91,12 @@ class CmsFormsContext extends BehatContext
|
|||||||
/**
|
/**
|
||||||
* @Then /^the "(?P<locator>([^"]*))" HTML field should contain "(?P<html>.*)"$/
|
* @Then /^the "(?P<locator>([^"]*))" HTML field should contain "(?P<html>.*)"$/
|
||||||
*/
|
*/
|
||||||
public function theHtmlFieldShouldContain($locator, $html)
|
public function theHtmlFieldShouldContain($locator, $html) {
|
||||||
{
|
|
||||||
$page = $this->getSession()->getPage();
|
$page = $this->getSession()->getPage();
|
||||||
$element = $page->findField($locator);
|
$element = $page->findField($locator);
|
||||||
assertNotNull($element, sprintf('HTML field "%s" not found', $locator));
|
assertNotNull($element, sprintf('HTML field "%s" not found', $locator));
|
||||||
|
|
||||||
$actual = $element->getAttribute('value');
|
$actual = $element->getValue();
|
||||||
$regex = '/'.preg_quote($html, '/').'/ui';
|
$regex = '/'.preg_quote($html, '/').'/ui';
|
||||||
if (!preg_match($regex, $actual)) {
|
if (!preg_match($regex, $actual)) {
|
||||||
$message = sprintf(
|
$message = sprintf(
|
||||||
@ -160,8 +157,7 @@ class CmsFormsContext extends BehatContext
|
|||||||
*
|
*
|
||||||
* @When /^I select "(?P<text>([^"]*))" in the "(?P<field>([^"]*))" HTML field$/
|
* @When /^I select "(?P<text>([^"]*))" in the "(?P<field>([^"]*))" HTML field$/
|
||||||
*/
|
*/
|
||||||
public function stepIHighlightTextInHtmlField($text, $field)
|
public function stepIHighlightTextInHtmlField($text, $field) {
|
||||||
{
|
|
||||||
$page = $this->getSession()->getPage();
|
$page = $this->getSession()->getPage();
|
||||||
$inputField = $page->findField($field);
|
$inputField = $page->findField($field);
|
||||||
assertNotNull($inputField, sprintf('HTML field "%s" not found', $field));
|
assertNotNull($inputField, sprintf('HTML field "%s" not found', $field));
|
||||||
@ -190,31 +186,42 @@ JS;
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Given /^I should see a "([^"]*)" button$/
|
* Example: I should see a "Submit" button
|
||||||
|
* Example: I should not see a "Delete" button
|
||||||
|
*
|
||||||
|
* @Given /^I should( not? |\s*)see a "([^"]*)" button$/
|
||||||
*/
|
*/
|
||||||
public function iShouldSeeAButton($text)
|
public function iShouldSeeAButton($negative, $text) {
|
||||||
{
|
|
||||||
$page = $this->getSession()->getPage();
|
$page = $this->getSession()->getPage();
|
||||||
$els = $page->findAll('named', array('link_or_button', "'$text'"));
|
$els = $page->findAll('named', array('link_or_button', "'$text'"));
|
||||||
$matchedEl = null;
|
$matchedEl = null;
|
||||||
foreach($els as $el) {
|
foreach($els as $el) {
|
||||||
if($el->isVisible()) $matchedEl = $el;
|
if($el->isVisible()) $matchedEl = $el;
|
||||||
}
|
}
|
||||||
assertNotNull($matchedEl, sprintf('%s button not found', $text));
|
|
||||||
|
if(trim($negative)) {
|
||||||
|
assertNull($matchedEl, sprintf('%s button found', $text));
|
||||||
|
} else {
|
||||||
|
assertNotNull($matchedEl, sprintf('%s button not found', $text));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Given /^I should not see a "([^"]*)" button$/
|
* @Given /^I should( not? |\s*)see a "([^"]*)" field$/
|
||||||
*/
|
*/
|
||||||
public function iShouldNotSeeAButton($text)
|
public function iShouldSeeAField($negative, $text) {
|
||||||
{
|
|
||||||
$page = $this->getSession()->getPage();
|
$page = $this->getSession()->getPage();
|
||||||
$els = $page->findAll('named', array('link_or_button', "'$text'"));
|
$els = $page->findAll('named', array('field', "'$text'"));
|
||||||
$matchedEl = null;
|
$matchedEl = null;
|
||||||
foreach($els as $el) {
|
foreach($els as $el) {
|
||||||
if($el->isVisible()) $matchedEl = $el;
|
if($el->isVisible()) $matchedEl = $el;
|
||||||
}
|
}
|
||||||
assertNull($matchedEl, sprintf('%s button found', $text));
|
|
||||||
|
if(trim($negative)) {
|
||||||
|
assertNull($matchedEl);
|
||||||
|
} else {
|
||||||
|
assertNotNull($matchedEl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,7 @@ require_once 'PHPUnit/Framework/Assert/Functions.php';
|
|||||||
*
|
*
|
||||||
* Context used to define steps related to SilverStripe CMS UI like Tree or Panel.
|
* Context used to define steps related to SilverStripe CMS UI like Tree or Panel.
|
||||||
*/
|
*/
|
||||||
class CmsUiContext extends BehatContext
|
class CmsUiContext extends BehatContext {
|
||||||
{
|
|
||||||
protected $context;
|
protected $context;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,8 +31,7 @@ class CmsUiContext extends BehatContext
|
|||||||
*
|
*
|
||||||
* @param array $parameters context parameters (set them up through behat.yml)
|
* @param array $parameters context parameters (set them up through behat.yml)
|
||||||
*/
|
*/
|
||||||
public function __construct(array $parameters)
|
public function __construct(array $parameters) {
|
||||||
{
|
|
||||||
// Initialize your context here
|
// Initialize your context here
|
||||||
$this->context = $parameters;
|
$this->context = $parameters;
|
||||||
}
|
}
|
||||||
@ -41,16 +39,14 @@ class CmsUiContext extends BehatContext
|
|||||||
/**
|
/**
|
||||||
* Get Mink session from MinkContext
|
* Get Mink session from MinkContext
|
||||||
*/
|
*/
|
||||||
public function getSession($name = null)
|
public function getSession($name = null) {
|
||||||
{
|
|
||||||
return $this->getMainContext()->getSession($name);
|
return $this->getMainContext()->getSession($name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Then /^I should see the CMS$/
|
* @Then /^I should see the CMS$/
|
||||||
*/
|
*/
|
||||||
public function iShouldSeeTheCms()
|
public function iShouldSeeTheCms() {
|
||||||
{
|
|
||||||
$page = $this->getSession()->getPage();
|
$page = $this->getSession()->getPage();
|
||||||
$cms_element = $page->find('css', '.cms');
|
$cms_element = $page->find('css', '.cms');
|
||||||
assertNotNull($cms_element, 'CMS not found');
|
assertNotNull($cms_element, 'CMS not found');
|
||||||
@ -59,21 +55,18 @@ class CmsUiContext extends BehatContext
|
|||||||
/**
|
/**
|
||||||
* @Then /^I should see a "([^"]*)" notice$/
|
* @Then /^I should see a "([^"]*)" notice$/
|
||||||
*/
|
*/
|
||||||
public function iShouldSeeANotice($notice)
|
public function iShouldSeeANotice($notice) {
|
||||||
{
|
|
||||||
$this->getMainContext()->assertElementContains('.notice-wrap', $notice);
|
$this->getMainContext()->assertElementContains('.notice-wrap', $notice);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Then /^I should see a "([^"]*)" message$/
|
* @Then /^I should see a "([^"]*)" message$/
|
||||||
*/
|
*/
|
||||||
public function iShouldSeeAMessage($message)
|
public function iShouldSeeAMessage($message) {
|
||||||
{
|
|
||||||
$this->getMainContext()->assertElementContains('.message', $message);
|
$this->getMainContext()->assertElementContains('.message', $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getCmsTabsElement()
|
protected function getCmsTabsElement() {
|
||||||
{
|
|
||||||
$this->getSession()->wait(
|
$this->getSession()->wait(
|
||||||
5000,
|
5000,
|
||||||
"window.jQuery && window.jQuery('.cms-content-header-tabs').size() > 0"
|
"window.jQuery && window.jQuery('.cms-content-header-tabs').size() > 0"
|
||||||
@ -86,8 +79,7 @@ class CmsUiContext extends BehatContext
|
|||||||
return $cms_content_header_tabs;
|
return $cms_content_header_tabs;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getCmsContentToolbarElement()
|
protected function getCmsContentToolbarElement() {
|
||||||
{
|
|
||||||
$this->getSession()->wait(
|
$this->getSession()->wait(
|
||||||
5000,
|
5000,
|
||||||
"window.jQuery && window.jQuery('.cms-content-toolbar').size() > 0 "
|
"window.jQuery && window.jQuery('.cms-content-toolbar').size() > 0 "
|
||||||
@ -101,8 +93,7 @@ class CmsUiContext extends BehatContext
|
|||||||
return $cms_content_toolbar_element;
|
return $cms_content_toolbar_element;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getCmsTreeElement()
|
protected function getCmsTreeElement() {
|
||||||
{
|
|
||||||
$this->getSession()->wait(
|
$this->getSession()->wait(
|
||||||
5000,
|
5000,
|
||||||
"window.jQuery && window.jQuery('.cms-tree').size() > 0"
|
"window.jQuery && window.jQuery('.cms-tree').size() > 0"
|
||||||
@ -115,8 +106,7 @@ class CmsUiContext extends BehatContext
|
|||||||
return $cms_tree_element;
|
return $cms_tree_element;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getGridfieldTable($title)
|
protected function getGridfieldTable($title) {
|
||||||
{
|
|
||||||
$page = $this->getSession()->getPage();
|
$page = $this->getSession()->getPage();
|
||||||
$table_elements = $page->findAll('css', '.ss-gridfield-table');
|
$table_elements = $page->findAll('css', '.ss-gridfield-table');
|
||||||
assertNotNull($table_elements, 'Table elements not found');
|
assertNotNull($table_elements, 'Table elements not found');
|
||||||
@ -137,8 +127,7 @@ class CmsUiContext extends BehatContext
|
|||||||
/**
|
/**
|
||||||
* @Given /^I should see a "([^"]*)" button in CMS Content Toolbar$/
|
* @Given /^I should see a "([^"]*)" button in CMS Content Toolbar$/
|
||||||
*/
|
*/
|
||||||
public function iShouldSeeAButtonInCmsContentToolbar($text)
|
public function iShouldSeeAButtonInCmsContentToolbar($text) {
|
||||||
{
|
|
||||||
$cms_content_toolbar_element = $this->getCmsContentToolbarElement();
|
$cms_content_toolbar_element = $this->getCmsContentToolbarElement();
|
||||||
|
|
||||||
$element = $cms_content_toolbar_element->find('named', array('link_or_button', "'$text'"));
|
$element = $cms_content_toolbar_element->find('named', array('link_or_button', "'$text'"));
|
||||||
@ -148,8 +137,7 @@ class CmsUiContext extends BehatContext
|
|||||||
/**
|
/**
|
||||||
* @When /^I should see "([^"]*)" in the tree$/
|
* @When /^I should see "([^"]*)" in the tree$/
|
||||||
*/
|
*/
|
||||||
public function stepIShouldSeeInCmsTree($text)
|
public function stepIShouldSeeInCmsTree($text) {
|
||||||
{
|
|
||||||
$cms_tree_element = $this->getCmsTreeElement();
|
$cms_tree_element = $this->getCmsTreeElement();
|
||||||
|
|
||||||
$element = $cms_tree_element->find('named', array('content', "'$text'"));
|
$element = $cms_tree_element->find('named', array('content', "'$text'"));
|
||||||
@ -159,8 +147,7 @@ class CmsUiContext extends BehatContext
|
|||||||
/**
|
/**
|
||||||
* @When /^I should not see "([^"]*)" in the tree$/
|
* @When /^I should not see "([^"]*)" in the tree$/
|
||||||
*/
|
*/
|
||||||
public function stepIShouldNotSeeInCmsTree($text)
|
public function stepIShouldNotSeeInCmsTree($text) {
|
||||||
{
|
|
||||||
$cms_tree_element = $this->getCmsTreeElement();
|
$cms_tree_element = $this->getCmsTreeElement();
|
||||||
|
|
||||||
$element = $cms_tree_element->find('named', array('content', "'$text'"));
|
$element = $cms_tree_element->find('named', array('content', "'$text'"));
|
||||||
@ -170,8 +157,7 @@ class CmsUiContext extends BehatContext
|
|||||||
/**
|
/**
|
||||||
* @When /^I click on "([^"]*)" in the tree$/
|
* @When /^I click on "([^"]*)" in the tree$/
|
||||||
*/
|
*/
|
||||||
public function stepIClickOnElementInTheTree($text)
|
public function stepIClickOnElementInTheTree($text) {
|
||||||
{
|
|
||||||
$treeEl = $this->getCmsTreeElement();
|
$treeEl = $this->getCmsTreeElement();
|
||||||
$treeNode = $treeEl->findLink($text);
|
$treeNode = $treeEl->findLink($text);
|
||||||
assertNotNull($treeNode, sprintf('%s not found', $text));
|
assertNotNull($treeNode, sprintf('%s not found', $text));
|
||||||
@ -181,8 +167,7 @@ class CmsUiContext extends BehatContext
|
|||||||
/**
|
/**
|
||||||
* @When /^I expand the "([^"]*)" CMS Panel$/
|
* @When /^I expand the "([^"]*)" CMS Panel$/
|
||||||
*/
|
*/
|
||||||
public function iExpandTheCmsPanel()
|
public function iExpandTheCmsPanel() {
|
||||||
{
|
|
||||||
// TODO Make dynamic, currently hardcoded to first panel
|
// TODO Make dynamic, currently hardcoded to first panel
|
||||||
$page = $this->getSession()->getPage();
|
$page = $this->getSession()->getPage();
|
||||||
|
|
||||||
@ -197,8 +182,7 @@ class CmsUiContext extends BehatContext
|
|||||||
/**
|
/**
|
||||||
* @When /^I click the "([^"]*)" CMS tab$/
|
* @When /^I click the "([^"]*)" CMS tab$/
|
||||||
*/
|
*/
|
||||||
public function iClickTheCmsTab($tab)
|
public function iClickTheCmsTab($tab) {
|
||||||
{
|
|
||||||
$this->getSession()->wait(
|
$this->getSession()->wait(
|
||||||
5000,
|
5000,
|
||||||
"window.jQuery && window.jQuery('.ui-tabs-nav').size() > 0"
|
"window.jQuery && window.jQuery('.ui-tabs-nav').size() > 0"
|
||||||
@ -221,8 +205,7 @@ class CmsUiContext extends BehatContext
|
|||||||
/**
|
/**
|
||||||
* @Then /^the "([^"]*)" table should contain "([^"]*)"$/
|
* @Then /^the "([^"]*)" table should contain "([^"]*)"$/
|
||||||
*/
|
*/
|
||||||
public function theTableShouldContain($table, $text)
|
public function theTableShouldContain($table, $text) {
|
||||||
{
|
|
||||||
$table_element = $this->getGridfieldTable($table);
|
$table_element = $this->getGridfieldTable($table);
|
||||||
|
|
||||||
$element = $table_element->find('named', array('content', "'$text'"));
|
$element = $table_element->find('named', array('content', "'$text'"));
|
||||||
@ -232,8 +215,7 @@ class CmsUiContext extends BehatContext
|
|||||||
/**
|
/**
|
||||||
* @Then /^the "([^"]*)" table should not contain "([^"]*)"$/
|
* @Then /^the "([^"]*)" table should not contain "([^"]*)"$/
|
||||||
*/
|
*/
|
||||||
public function theTableShouldNotContain($table, $text)
|
public function theTableShouldNotContain($table, $text) {
|
||||||
{
|
|
||||||
$table_element = $this->getGridfieldTable($table);
|
$table_element = $this->getGridfieldTable($table);
|
||||||
|
|
||||||
$element = $table_element->find('named', array('content', "'$text'"));
|
$element = $table_element->find('named', array('content', "'$text'"));
|
||||||
@ -243,8 +225,7 @@ class CmsUiContext extends BehatContext
|
|||||||
/**
|
/**
|
||||||
* @Given /^I click on "([^"]*)" in the "([^"]*)" table$/
|
* @Given /^I click on "([^"]*)" in the "([^"]*)" table$/
|
||||||
*/
|
*/
|
||||||
public function iClickOnInTheTable($text, $table)
|
public function iClickOnInTheTable($text, $table) {
|
||||||
{
|
|
||||||
$table_element = $this->getGridfieldTable($table);
|
$table_element = $this->getGridfieldTable($table);
|
||||||
|
|
||||||
$element = $table_element->find('xpath', sprintf('//*[count(*)=0 and contains(.,"%s")]', $text));
|
$element = $table_element->find('xpath', sprintf('//*[count(*)=0 and contains(.,"%s")]', $text));
|
||||||
@ -255,16 +236,14 @@ class CmsUiContext extends BehatContext
|
|||||||
/**
|
/**
|
||||||
* @Then /^I can see the preview panel$/
|
* @Then /^I can see the preview panel$/
|
||||||
*/
|
*/
|
||||||
public function iCanSeeThePreviewPanel()
|
public function iCanSeeThePreviewPanel() {
|
||||||
{
|
|
||||||
$this->getMainContext()->assertElementOnPage('.cms-preview');
|
$this->getMainContext()->assertElementOnPage('.cms-preview');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Given /^the preview contains "([^"]*)"$/
|
* @Given /^the preview contains "([^"]*)"$/
|
||||||
*/
|
*/
|
||||||
public function thePreviewContains($content)
|
public function thePreviewContains($content) {
|
||||||
{
|
|
||||||
$driver = $this->getSession()->getDriver();
|
$driver = $this->getSession()->getDriver();
|
||||||
// TODO Remove once we have native support in Mink and php-webdriver,
|
// TODO Remove once we have native support in Mink and php-webdriver,
|
||||||
// see https://groups.google.com/forum/#!topic/behat/QNhOuGHKEWI
|
// see https://groups.google.com/forum/#!topic/behat/QNhOuGHKEWI
|
||||||
@ -278,8 +257,7 @@ class CmsUiContext extends BehatContext
|
|||||||
/**
|
/**
|
||||||
* @Given /^I set the CMS mode to "([^"]*)"$/
|
* @Given /^I set the CMS mode to "([^"]*)"$/
|
||||||
*/
|
*/
|
||||||
public function iSetTheCmsToMode($mode)
|
public function iSetTheCmsToMode($mode) {
|
||||||
{
|
|
||||||
return array(
|
return array(
|
||||||
new Step\When(sprintf('I fill in the "Change view mode" dropdown with "%s"', $mode)),
|
new Step\When(sprintf('I fill in the "Change view mode" dropdown with "%s"', $mode)),
|
||||||
new Step\When('I wait for 1 second') // wait for CMS layout to redraw
|
new Step\When('I wait for 1 second') // wait for CMS layout to redraw
|
||||||
@ -289,8 +267,7 @@ class CmsUiContext extends BehatContext
|
|||||||
/**
|
/**
|
||||||
* @Given /^I wait for the preview to load$/
|
* @Given /^I wait for the preview to load$/
|
||||||
*/
|
*/
|
||||||
public function iWaitForThePreviewToLoad()
|
public function iWaitForThePreviewToLoad() {
|
||||||
{
|
|
||||||
$driver = $this->getSession()->getDriver();
|
$driver = $this->getSession()->getDriver();
|
||||||
// TODO Remove once we have native support in Mink and php-webdriver,
|
// TODO Remove once we have native support in Mink and php-webdriver,
|
||||||
// see https://groups.google.com/forum/#!topic/behat/QNhOuGHKEWI
|
// see https://groups.google.com/forum/#!topic/behat/QNhOuGHKEWI
|
||||||
@ -307,8 +284,7 @@ class CmsUiContext extends BehatContext
|
|||||||
/**
|
/**
|
||||||
* @Given /^I switch the preview to "([^"]*)"$/
|
* @Given /^I switch the preview to "([^"]*)"$/
|
||||||
*/
|
*/
|
||||||
public function iSwitchThePreviewToMode($mode)
|
public function iSwitchThePreviewToMode($mode) {
|
||||||
{
|
|
||||||
$controls = $this->getSession()->getPage()->find('css', '.cms-preview-controls');
|
$controls = $this->getSession()->getPage()->find('css', '.cms-preview-controls');
|
||||||
assertNotNull($controls, 'Preview controls not found');
|
assertNotNull($controls, 'Preview controls not found');
|
||||||
|
|
||||||
@ -326,8 +302,7 @@ class CmsUiContext extends BehatContext
|
|||||||
/**
|
/**
|
||||||
* @Given /^the preview does not contain "([^"]*)"$/
|
* @Given /^the preview does not contain "([^"]*)"$/
|
||||||
*/
|
*/
|
||||||
public function thePreviewDoesNotContain($content)
|
public function thePreviewDoesNotContain($content) {
|
||||||
{
|
|
||||||
$driver = $this->getSession()->getDriver();
|
$driver = $this->getSession()->getDriver();
|
||||||
// TODO Remove once we have native support in Mink and php-webdriver,
|
// TODO Remove once we have native support in Mink and php-webdriver,
|
||||||
// see https://groups.google.com/forum/#!topic/behat/QNhOuGHKEWI
|
// see https://groups.google.com/forum/#!topic/behat/QNhOuGHKEWI
|
||||||
@ -344,8 +319,7 @@ class CmsUiContext extends BehatContext
|
|||||||
* @When /^(?:|I )fill in the "(?P<field>(?:[^"]|\\")*)" dropdown with "(?P<value>(?:[^"]|\\")*)"$/
|
* @When /^(?:|I )fill in the "(?P<field>(?:[^"]|\\")*)" dropdown with "(?P<value>(?:[^"]|\\")*)"$/
|
||||||
* @When /^(?:|I )fill in "(?P<value>(?:[^"]|\\")*)" for the "(?P<field>(?:[^"]|\\")*)" dropdown$/
|
* @When /^(?:|I )fill in "(?P<value>(?:[^"]|\\")*)" for the "(?P<field>(?:[^"]|\\")*)" dropdown$/
|
||||||
*/
|
*/
|
||||||
public function theIFillInTheDropdownWith($field, $value)
|
public function theIFillInTheDropdownWith($field, $value) {
|
||||||
{
|
|
||||||
$field = $this->fixStepArgument($field);
|
$field = $this->fixStepArgument($field);
|
||||||
$value = $this->fixStepArgument($value);
|
$value = $this->fixStepArgument($value);
|
||||||
|
|
||||||
@ -447,8 +421,7 @@ class CmsUiContext extends BehatContext
|
|||||||
*
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
protected function fixStepArgument($argument)
|
protected function fixStepArgument($argument) {
|
||||||
{
|
|
||||||
return str_replace('\\"', '"', $argument);
|
return str_replace('\\"', '"', $argument);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -461,11 +434,10 @@ class CmsUiContext extends BehatContext
|
|||||||
*/
|
*/
|
||||||
protected function findParentByClass(NodeElement $el, $class) {
|
protected function findParentByClass(NodeElement $el, $class) {
|
||||||
$container = $el->getParent();
|
$container = $el->getParent();
|
||||||
while($container && $container->getTagName() != 'body'
|
while($container && $container->getTagName() != 'body') {
|
||||||
) {
|
|
||||||
if($container->isVisible() && in_array($class, explode(' ', $container->getAttribute('class')))) {
|
if($container->isVisible() && in_array($class, explode(' ', $container->getAttribute('class')))) {
|
||||||
return $container;
|
return $container;
|
||||||
}
|
}
|
||||||
$container = $container->getParent();
|
$container = $container->getParent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ Feature: Log in
|
|||||||
|
|
||||||
Scenario: Bad login
|
Scenario: Bad login
|
||||||
Given I log in with "bad@example.com" and "badpassword"
|
Given I log in with "bad@example.com" and "badpassword"
|
||||||
Then I will see a bad log-in message
|
Then I will see a "bad" log-in message
|
||||||
|
|
||||||
Scenario: Valid login
|
Scenario: Valid login
|
||||||
Given I am logged in with "ADMIN" permissions
|
Given I am logged in with "ADMIN" permissions
|
||||||
|
@ -5,9 +5,9 @@ Feature: Manage users
|
|||||||
So that I can control access to the CMS
|
So that I can control access to the CMS
|
||||||
|
|
||||||
Background:
|
Background:
|
||||||
Given a "member" "Admin" belonging to "Admin Group" with "Email"="admin@test.com"
|
Given a "member" "ADMIN" belonging to "ADMIN Group" with "Email"="admin@test.com"
|
||||||
And a "member" "Staff" belonging to "Staff Group" with "Email"="staffmember@test.com"
|
And a "member" "Staff" belonging to "Staff Group" with "Email"="staffmember@test.com"
|
||||||
And the "group" "Admin Group" has permissions "Full administrative rights"
|
And the "group" "ADMIN group" has permissions "Full administrative rights"
|
||||||
And I am logged in with "ADMIN" permissions
|
And I am logged in with "ADMIN" permissions
|
||||||
And I go to "/admin/security"
|
And I go to "/admin/security"
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ Feature: Manage users
|
|||||||
Scenario: I can list all users in a specific group
|
Scenario: I can list all users in a specific group
|
||||||
When I click the "Groups" CMS tab
|
When I click the "Groups" CMS tab
|
||||||
# TODO Please check how performant this is
|
# TODO Please check how performant this is
|
||||||
And I click "Admin Group" in the "#Root_Groups" element
|
And I click "ADMIN group" in the "#Root_Groups" element
|
||||||
Then I should see "admin@test.com" in the "#Root_Members" element
|
Then I should see "admin@test.com" in the "#Root_Members" element
|
||||||
And I should not see "staffmember@test.com" in the "#Root_Members" element
|
And I should not see "staffmember@test.com" in the "#Root_Members" element
|
||||||
|
|
||||||
@ -39,13 +39,13 @@ Feature: Manage users
|
|||||||
Scenario: I can edit an existing user and add him to an existing group
|
Scenario: I can edit an existing user and add him to an existing group
|
||||||
When I click the "Users" CMS tab
|
When I click the "Users" CMS tab
|
||||||
And I click "staffmember@test.com" in the "#Root_Users" element
|
And I click "staffmember@test.com" in the "#Root_Users" element
|
||||||
And I select "Admin Group" from "Groups"
|
And I select "ADMIN group" from "Groups"
|
||||||
And I press the "Save" button
|
And I press the "Save" button
|
||||||
Then I should see a "Saved Member" message
|
Then I should see a "Saved Member" message
|
||||||
|
|
||||||
When I go to "admin/security"
|
When I go to "admin/security"
|
||||||
And I click the "Groups" CMS tab
|
And I click the "Groups" CMS tab
|
||||||
And I click "Admin Group" in the "#Root_Groups" element
|
And I click "ADMIN group" in the "#Root_Groups" element
|
||||||
Then I should see "staffmember@test.com"
|
Then I should see "staffmember@test.com"
|
||||||
|
|
||||||
Scenario: I can delete an existing user
|
Scenario: I can delete an existing user
|
||||||
|
@ -122,6 +122,26 @@ class CheckboxSetFieldTest extends SapphireTest {
|
|||||||
'CheckboxSetField loads data from a manymany relationship in an object through Form->loadDataFrom()'
|
'CheckboxSetField loads data from a manymany relationship in an object through Form->loadDataFrom()'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testSavingIntoTextField() {
|
||||||
|
$field = new CheckboxSetField('Content', 'Content', array(
|
||||||
|
'Test' => 'Test',
|
||||||
|
'Another' => 'Another',
|
||||||
|
'Something' => 'Something'
|
||||||
|
));
|
||||||
|
$article = new CheckboxSetFieldTest_Article();
|
||||||
|
$field->setValue(array('Test' => 'Test', 'Another' => 'Another'));
|
||||||
|
$field->saveInto($article);
|
||||||
|
$article->write();
|
||||||
|
|
||||||
|
$dbValue = DB::query(sprintf(
|
||||||
|
'SELECT "Content" FROM "CheckboxSetFieldTest_Article" WHERE "ID" = %s',
|
||||||
|
$article->ID
|
||||||
|
))->value();
|
||||||
|
|
||||||
|
$this->assertEquals('Test,Another', $dbValue);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CheckboxSetFieldTest_Article extends DataObject implements TestOnly {
|
class CheckboxSetFieldTest_Article extends DataObject implements TestOnly {
|
||||||
|
@ -220,6 +220,31 @@ class GridFieldDetailFormTest extends FunctionalTest {
|
|||||||
$form = $request->ItemEditForm();
|
$form = $request->ItemEditForm();
|
||||||
$this->assertNotNull($form->Fields()->fieldByName('Callback'));
|
$this->assertNotNull($form->Fields()->fieldByName('Callback'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that a has-many detail form is pre-populated with the parent ID.
|
||||||
|
*/
|
||||||
|
public function testHasManyFormPrePopulated() {
|
||||||
|
$group = $this->objFromFixture(
|
||||||
|
'GridFieldDetailFormTest_PeopleGroup', 'group'
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->logInWithPermission('ADMIN');
|
||||||
|
|
||||||
|
$response = $this->get('GridFieldDetailFormTest_Controller');
|
||||||
|
$parser = new CSSContentParser($response->getBody());
|
||||||
|
$addLink = $parser->getBySelector('.ss-gridfield .new-link');
|
||||||
|
$addLink = (string) $addLink[0]['href'];
|
||||||
|
|
||||||
|
$response = $this->get($addLink);
|
||||||
|
$parser = new CSSContentParser($response->getBody());
|
||||||
|
$title = $parser->getBySelector('#GroupID span');
|
||||||
|
$id = $parser->getBySelector('#GroupID input');
|
||||||
|
|
||||||
|
$this->assertEquals($group->Name, (string) $title[0]);
|
||||||
|
$this->assertEquals($group->ID, (string) $id[0]['value']);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class GridFieldDetailFormTest_Person extends DataObject implements TestOnly {
|
class GridFieldDetailFormTest_Person extends DataObject implements TestOnly {
|
||||||
|
146
tests/forms/gridfield/GridFieldSortableHeaderTest.php
Normal file
146
tests/forms/gridfield/GridFieldSortableHeaderTest.php
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @package framework
|
||||||
|
* @subpackage tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
class GridFieldSortableHeaderTest extends SapphireTest {
|
||||||
|
|
||||||
|
protected static $fixture_file = 'GridFieldSortableHeaderTest.yml';
|
||||||
|
|
||||||
|
protected $extraDataObjects = array(
|
||||||
|
'GridFieldSortableHeaderTest_Team',
|
||||||
|
'GridFieldSortableHeaderTest_Cheerleader',
|
||||||
|
'GridFieldSortableHeaderTest_CheerleaderHat'
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that the appropriate sortable headers are generated
|
||||||
|
*/
|
||||||
|
public function testRenderHeaders() {
|
||||||
|
|
||||||
|
// Generate sortable header and extract HTML
|
||||||
|
$list = new DataList('GridFieldSortableHeaderTest_Team');
|
||||||
|
$config = new GridFieldConfig_RecordEditor();
|
||||||
|
$form = new Form(Controller::curr(), 'Form', new FieldList(), new FieldList());
|
||||||
|
$gridField = new GridField('testfield', 'testfield', $list, $config);
|
||||||
|
$gridField->setForm($form);
|
||||||
|
$compontent = $gridField->getConfig()->getComponentByType('GridFieldSortableHeader');
|
||||||
|
$htmlFragment = $compontent->getHTMLFragments($gridField);
|
||||||
|
|
||||||
|
// Check that the output shows name and hat as sortable fields, but not city
|
||||||
|
$this->assertContains('<span class="non-sortable">City</span>', $htmlFragment['header']);
|
||||||
|
$this->assertContains('value="Name" class="action ss-gridfield-sort" id="action_SetOrderName"', $htmlFragment['header']);
|
||||||
|
$this->assertContains('value="Cheerleader Hat" class="action ss-gridfield-sort" id="action_SetOrderCheerleader-Hat-Colour"', $htmlFragment['header']);
|
||||||
|
|
||||||
|
// Check inverse of above
|
||||||
|
$this->assertNotContains('value="City" class="action ss-gridfield-sort" id="action_SetOrderCity"', $htmlFragment['header']);
|
||||||
|
$this->assertNotContains('<span class="non-sortable">Name</span>', $htmlFragment['header']);
|
||||||
|
$this->assertNotContains('<span class="non-sortable">Cheerleader Hat</span>', $htmlFragment['header']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetManipulatedData() {
|
||||||
|
$list = new DataList('GridFieldSortableHeaderTest_Team');
|
||||||
|
$config = new GridFieldConfig_RecordEditor();
|
||||||
|
$gridField = new GridField('testfield', 'testfield', $list, $config);
|
||||||
|
|
||||||
|
// Test normal sorting
|
||||||
|
$state = $gridField->State->GridFieldSortableHeader;
|
||||||
|
$state->SortColumn = 'City';
|
||||||
|
$state->SortDirection = 'asc';
|
||||||
|
|
||||||
|
$compontent = $gridField->getConfig()->getComponentByType('GridFieldSortableHeader');
|
||||||
|
$listA = $compontent->getManipulatedData($gridField, $list);
|
||||||
|
|
||||||
|
$state->SortDirection = 'desc';
|
||||||
|
$listB = $compontent->getManipulatedData($gridField, $list);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
array('Auckland', 'Cologne', 'Melbourne', 'Wellington'),
|
||||||
|
$listA->column('City')
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
array('Wellington', 'Melbourne', 'Cologne', 'Auckland'),
|
||||||
|
$listB->column('City')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test one relation 'deep'
|
||||||
|
$state->SortColumn = 'Cheerleader.Name';
|
||||||
|
$state->SortDirection = 'asc';
|
||||||
|
$relationListA = $compontent->getManipulatedData($gridField, $list);
|
||||||
|
|
||||||
|
$state->SortDirection = 'desc';
|
||||||
|
$relationListB = $compontent->getManipulatedData($gridField, $list);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
array('Wellington', 'Melbourne', 'Cologne', 'Auckland'),
|
||||||
|
$relationListA->column('City')
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
array('Auckland', 'Cologne', 'Melbourne', 'Wellington'),
|
||||||
|
$relationListB->column('City')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test two relations 'deep'
|
||||||
|
$state->SortColumn = 'Cheerleader.Hat.Colour';
|
||||||
|
$state->SortDirection = 'asc';
|
||||||
|
$relationListC = $compontent->getManipulatedData($gridField, $list);
|
||||||
|
|
||||||
|
$state->SortDirection = 'desc';
|
||||||
|
$relationListD = $compontent->getManipulatedData($gridField, $list);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
array('Cologne', 'Auckland', 'Wellington', 'Melbourne'),
|
||||||
|
$relationListC->column('City')
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
array('Melbourne', 'Wellington', 'Auckland', 'Cologne'),
|
||||||
|
$relationListD->column('City')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class GridFieldSortableHeaderTest_Team extends DataObject implements TestOnly {
|
||||||
|
|
||||||
|
private static $summary_fields = array(
|
||||||
|
'Name' => 'Name',
|
||||||
|
'City.Initial' => 'City',
|
||||||
|
'Cheerleader.Hat.Colour' => 'Cheerleader Hat'
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Name' => 'Varchar',
|
||||||
|
'City' => 'Varchar'
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $has_one = array(
|
||||||
|
'Cheerleader' => 'GridFieldSortableHeaderTest_Cheerleader'
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class GridFieldSortableHeaderTest_Cheerleader extends DataObject implements TestOnly {
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Name' => 'Varchar'
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $has_one = array(
|
||||||
|
'Team' => 'GridFieldSortableHeaderTest_Team',
|
||||||
|
'Hat' => 'GridFieldSortableHeaderTest_CheerleaderHat'
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class GridFieldSortableHeaderTest_CheerleaderHat extends DataObject implements TestOnly {
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Colour' => 'Varchar'
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $has_one = array(
|
||||||
|
'Cheerleader' => 'GridFieldSortableHeaderTest_Cheerleader'
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
39
tests/forms/gridfield/GridFieldSortableHeaderTest.yml
Normal file
39
tests/forms/gridfield/GridFieldSortableHeaderTest.yml
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
GridFieldSortableHeaderTest_CheerleaderHat:
|
||||||
|
hat1:
|
||||||
|
Colour: Blue
|
||||||
|
hat2:
|
||||||
|
Colour: Red
|
||||||
|
hat3:
|
||||||
|
Colour: Green
|
||||||
|
hat4:
|
||||||
|
Colour: Pink
|
||||||
|
GridFieldSortableHeaderTest_Cheerleader:
|
||||||
|
cheerleader1:
|
||||||
|
Name: Heather
|
||||||
|
Hat: =>GridFieldSortableHeaderTest_CheerleaderHat.hat2
|
||||||
|
cheerleader2:
|
||||||
|
Name: Bob
|
||||||
|
Hat: =>GridFieldSortableHeaderTest_CheerleaderHat.hat4
|
||||||
|
cheerleader3:
|
||||||
|
Name: Jenny
|
||||||
|
Hat: =>GridFieldSortableHeaderTest_CheerleaderHat.hat1
|
||||||
|
cheerleader4:
|
||||||
|
Name: Sam
|
||||||
|
Hat: =>GridFieldSortableHeaderTest_CheerleaderHat.hat3
|
||||||
|
GridFieldSortableHeaderTest_Team:
|
||||||
|
team1:
|
||||||
|
Name: Team 1
|
||||||
|
City: Cologne
|
||||||
|
Cheerleader: =>GridFieldSortableHeaderTest_Cheerleader.cheerleader3
|
||||||
|
team2:
|
||||||
|
Name: Team 2
|
||||||
|
City: Wellington
|
||||||
|
Cheerleader: =>GridFieldSortableHeaderTest_Cheerleader.cheerleader2
|
||||||
|
team3:
|
||||||
|
Name: Team 3
|
||||||
|
City: Auckland
|
||||||
|
Cheerleader: =>GridFieldSortableHeaderTest_Cheerleader.cheerleader4
|
||||||
|
team4:
|
||||||
|
Name: Team 4
|
||||||
|
City: Melbourne
|
||||||
|
Cheerleader: =>GridFieldSortableHeaderTest_Cheerleader.cheerleader1
|
@ -439,6 +439,32 @@ class ArrayListTest extends SapphireTest {
|
|||||||
$this->assertEquals($expected, $list->toArray(), 'List should only contain Steve and Steve and Clair');
|
$this->assertEquals($expected, $list->toArray(), 'List should only contain Steve and Steve and Clair');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* $list = $list->filterByCallback(function($item, $list) { return $item->Age == 21; })
|
||||||
|
*/
|
||||||
|
public function testFilterByCallback() {
|
||||||
|
$list = new ArrayList(array(
|
||||||
|
array('Name' => 'Steve', 'ID' => 1, 'Age' => 21),
|
||||||
|
array('Name' => 'Bob', 'ID' => 2, 'Age' => 18),
|
||||||
|
array('Name' => 'Clair', 'ID' => 2, 'Age' => 21),
|
||||||
|
array('Name' => 'Oscar', 'ID' => 2, 'Age' => 52),
|
||||||
|
array('Name' => 'Mike', 'ID' => 3, 'Age' => 43)
|
||||||
|
));
|
||||||
|
|
||||||
|
$list = $list->filterByCallback(function ($item, $list) {
|
||||||
|
return $item->Age == 21;
|
||||||
|
});
|
||||||
|
|
||||||
|
$expected = array(
|
||||||
|
new ArrayData(array('Name' => 'Steve', 'ID' => 1, 'Age' => 21)),
|
||||||
|
new ArrayData(array('Name' => 'Clair', 'ID' => 2, 'Age' => 21)),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(2, $list->count());
|
||||||
|
$this->assertEquals($expected, $list->toArray(), 'List should only contain Steve and Clair');
|
||||||
|
$this->assertTrue($list instanceof SS_Filterable, 'The List should be of type SS_Filterable');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* $list->exclude('Name', 'bob'); // exclude bob from list
|
* $list->exclude('Name', 'bob'); // exclude bob from list
|
||||||
*/
|
*/
|
||||||
|
@ -671,6 +671,24 @@ class DataListTest extends SapphireTest {
|
|||||||
$this->assertEquals(0, $list->exclude('ID', $obj->ID)->count());
|
$this->assertEquals(0, $list->exclude('ID', $obj->ID)->count());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* $list = $list->filterByCallback(function($item, $list) { return $item->Age == 21; })
|
||||||
|
*/
|
||||||
|
public function testFilterByCallback() {
|
||||||
|
$team1ID = $this->idFromFixture('DataObjectTest_Team', 'team1');
|
||||||
|
$list = DataObjectTest_TeamComment::get();
|
||||||
|
$list = $list->filterByCallback(function ($item, $list) use ($team1ID) {
|
||||||
|
return $item->TeamID == $team1ID;
|
||||||
|
});
|
||||||
|
|
||||||
|
$result = $list->column('Name');
|
||||||
|
$expected = array_intersect($result, array('Joe', 'Bob'));
|
||||||
|
|
||||||
|
$this->assertEquals(2, $list->count());
|
||||||
|
$this->assertEquals($expected, $result, 'List should only contain comments from Team 1 (Joe and Bob)');
|
||||||
|
$this->assertTrue($list instanceof SS_Filterable, 'The List should be of type SS_Filterable');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* $list->exclude('Name', 'bob'); // exclude bob from list
|
* $list->exclude('Name', 'bob'); // exclude bob from list
|
||||||
*/
|
*/
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package framework
|
||||||
|
* @subpackage tests
|
||||||
|
*/
|
||||||
class SS_MapTest extends SapphireTest {
|
class SS_MapTest extends SapphireTest {
|
||||||
|
|
||||||
// Borrow the model from DataObjectTest
|
// Borrow the model from DataObjectTest
|
||||||
protected static $fixture_file = 'DataObjectTest.yml';
|
protected static $fixture_file = 'DataObjectTest.yml';
|
||||||
|
|
||||||
@ -16,6 +21,43 @@ class SS_MapTest extends SapphireTest {
|
|||||||
'DataObjectTest_TeamComment'
|
'DataObjectTest_TeamComment'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
public function testValues() {
|
||||||
|
$list = DataObjectTest_TeamComment::get()->sort('Name');
|
||||||
|
$map = new SS_Map($list, 'Name', 'Comment');
|
||||||
|
|
||||||
|
$this->assertEquals(array(
|
||||||
|
'This is a team comment by Bob',
|
||||||
|
'This is a team comment by Joe',
|
||||||
|
'Phil is a unique guy, and comments on team2'
|
||||||
|
), $map->values());
|
||||||
|
|
||||||
|
|
||||||
|
$map->push('Push', 'Item');
|
||||||
|
|
||||||
|
$this->assertEquals(array(
|
||||||
|
'This is a team comment by Bob',
|
||||||
|
'This is a team comment by Joe',
|
||||||
|
'Phil is a unique guy, and comments on team2',
|
||||||
|
'Item'
|
||||||
|
), $map->values());
|
||||||
|
|
||||||
|
$map = new SS_Map(new ArrayList());
|
||||||
|
$map->push('Push', 'Pushed value');
|
||||||
|
|
||||||
|
$this->assertEquals(array(
|
||||||
|
'Pushed value'
|
||||||
|
), $map->values());
|
||||||
|
|
||||||
|
$map = new SS_Map(new ArrayList());
|
||||||
|
$map->unshift('Unshift', 'Unshift item');
|
||||||
|
|
||||||
|
$this->assertEquals(array(
|
||||||
|
'Unshift item'
|
||||||
|
), $map->values());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public function testArrayAccess() {
|
public function testArrayAccess() {
|
||||||
$list = DataObjectTest_TeamComment::get();
|
$list = DataObjectTest_TeamComment::get();
|
||||||
$map = new SS_Map($list, 'Name', 'Comment');
|
$map = new SS_Map($list, 'Name', 'Comment');
|
||||||
@ -65,6 +107,39 @@ class SS_MapTest extends SapphireTest {
|
|||||||
'Joe',
|
'Joe',
|
||||||
'Phil'
|
'Phil'
|
||||||
), $map->keys());
|
), $map->keys());
|
||||||
|
|
||||||
|
$map->unshift('Unshift', 'Item');
|
||||||
|
|
||||||
|
$this->assertEquals(array(
|
||||||
|
'Unshift',
|
||||||
|
'Bob',
|
||||||
|
'Joe',
|
||||||
|
'Phil'
|
||||||
|
), $map->keys());
|
||||||
|
|
||||||
|
$map->push('Push', 'Item');
|
||||||
|
|
||||||
|
$this->assertEquals(array(
|
||||||
|
'Unshift',
|
||||||
|
'Bob',
|
||||||
|
'Joe',
|
||||||
|
'Phil',
|
||||||
|
'Push'
|
||||||
|
), $map->keys());
|
||||||
|
|
||||||
|
$map = new SS_Map(new ArrayList());
|
||||||
|
$map->push('Push', 'Item');
|
||||||
|
|
||||||
|
$this->assertEquals(array(
|
||||||
|
'Push'
|
||||||
|
), $map->keys());
|
||||||
|
|
||||||
|
$map = new SS_Map(new ArrayList());
|
||||||
|
$map->unshift('Unshift', 'Item');
|
||||||
|
|
||||||
|
$this->assertEquals(array(
|
||||||
|
'Unshift'
|
||||||
|
), $map->keys());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testMethodAsValueField() {
|
public function testMethodAsValueField() {
|
||||||
@ -80,16 +155,6 @@ class SS_MapTest extends SapphireTest {
|
|||||||
), $map->values());
|
), $map->values());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testValues() {
|
|
||||||
$list = DataObjectTest_TeamComment::get()->sort('Name');
|
|
||||||
$map = new SS_Map($list, 'Name', 'Comment');
|
|
||||||
$this->assertEquals(array(
|
|
||||||
'This is a team comment by Bob',
|
|
||||||
'This is a team comment by Joe',
|
|
||||||
'Phil is a unique guy, and comments on team2'
|
|
||||||
), $map->values());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testUnshift() {
|
public function testUnshift() {
|
||||||
$list = DataObjectTest_TeamComment::get();
|
$list = DataObjectTest_TeamComment::get();
|
||||||
$map = new SS_Map($list, 'Name', 'Comment');
|
$map = new SS_Map($list, 'Name', 'Comment');
|
||||||
@ -137,7 +202,103 @@ class SS_MapTest extends SapphireTest {
|
|||||||
"Bob" => "Replaced",
|
"Bob" => "Replaced",
|
||||||
0 => "(Select)",
|
0 => "(Select)",
|
||||||
-1 => "(All)"), $map->toArray());
|
-1 => "(All)"), $map->toArray());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testPush() {
|
||||||
|
$list = DataObjectTest_TeamComment::get();
|
||||||
|
$map = new SS_Map($list, 'Name', 'Comment');
|
||||||
|
|
||||||
|
$map->push(1, '(All)');
|
||||||
|
|
||||||
|
$this->assertEquals(array(
|
||||||
|
"Joe" => "This is a team comment by Joe",
|
||||||
|
"Bob" => "This is a team comment by Bob",
|
||||||
|
"Phil" => "Phil is a unique guy, and comments on team2",
|
||||||
|
1 => "(All)"
|
||||||
|
), $map->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCount() {
|
||||||
|
$list = DataObjectTest_TeamComment::get();
|
||||||
|
$map = new SS_Map($list, 'Name', 'Comment');
|
||||||
|
|
||||||
|
$this->assertEquals(3, $map->count());
|
||||||
|
|
||||||
|
// pushing a new item should update the count
|
||||||
|
$map->push(1, 'Item pushed');
|
||||||
|
$this->assertEquals(4, $map->count());
|
||||||
|
|
||||||
|
$map->unshift(2, 'Item shifted');
|
||||||
|
$this->assertEquals(5, $map->count());
|
||||||
|
|
||||||
|
$map = new SS_Map(new ArrayList());
|
||||||
|
$map->unshift('1', 'shifted');
|
||||||
|
|
||||||
|
$this->assertEquals(1, $map->count());
|
||||||
|
|
||||||
|
unset($map[1]);
|
||||||
|
$this->assertEquals(0, $map->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIterationWithUnshift() {
|
||||||
|
$list = DataObjectTest_TeamComment::get()->sort('ID');
|
||||||
|
$map = new SS_Map($list, 'Name', 'Comment');
|
||||||
|
$map->unshift(1, 'Unshifted');
|
||||||
|
|
||||||
|
$text = "";
|
||||||
|
|
||||||
|
foreach($map as $k => $v) {
|
||||||
|
$text .= "$k: $v\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertEquals("1: Unshifted\n"
|
||||||
|
. "Joe: This is a team comment by Joe\n"
|
||||||
|
. "Bob: This is a team comment by Bob\n"
|
||||||
|
. "Phil: Phil is a unique guy, and comments on team2\n", $text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIterationWithPush() {
|
||||||
|
$list = DataObjectTest_TeamComment::get()->sort('ID');
|
||||||
|
$map = new SS_Map($list, 'Name', 'Comment');
|
||||||
|
$map->push(1, 'Pushed');
|
||||||
|
|
||||||
|
$text = "";
|
||||||
|
|
||||||
|
foreach($map as $k => $v) {
|
||||||
|
$text .= "$k: $v\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertEquals("Joe: This is a team comment by Joe\n"
|
||||||
|
. "Bob: This is a team comment by Bob\n"
|
||||||
|
. "Phil: Phil is a unique guy, and comments on team2\n"
|
||||||
|
. "1: Pushed\n", $text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIterationWithEmptyListUnshifted() {
|
||||||
|
$map = new SS_Map(new ArrayList());
|
||||||
|
$map->unshift('1', 'unshifted');
|
||||||
|
|
||||||
|
$text = "";
|
||||||
|
|
||||||
|
foreach($map as $k => $v) {
|
||||||
|
$text .= "$k: $v\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertEquals("1: unshifted\n", $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIterationWithEmptyListPushed() {
|
||||||
|
$map = new SS_Map(new ArrayList());
|
||||||
|
$map->push('1', 'pushed');
|
||||||
|
|
||||||
|
$text = "";
|
||||||
|
|
||||||
|
foreach($map as $k => $v) {
|
||||||
|
$text .= "$k: $v\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertEquals("1: pushed\n", $text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1299,6 +1299,38 @@ after')
|
|||||||
$this->assertEquals($expected, trim($this->render($template, $data)));
|
$this->assertEquals($expected, trim($this->render($template, $data)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testClosedBlockExtension() {
|
||||||
|
$count = 0;
|
||||||
|
$parser = new SSTemplateParser();
|
||||||
|
$parser->addClosedBlock(
|
||||||
|
'test',
|
||||||
|
function (&$res) use (&$count) {
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$template = new SSViewer_FromString("<% test %><% end_test %>", $parser);
|
||||||
|
$template->process(new SSViewerTestFixture());
|
||||||
|
|
||||||
|
$this->assertEquals(1, $count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOpenBlockExtension() {
|
||||||
|
$count = 0;
|
||||||
|
$parser = new SSTemplateParser();
|
||||||
|
$parser->addOpenBlock(
|
||||||
|
'test',
|
||||||
|
function (&$res) use (&$count) {
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$template = new SSViewer_FromString("<% test %>", $parser);
|
||||||
|
$template->process(new SSViewerTestFixture());
|
||||||
|
|
||||||
|
$this->assertEquals(1, $count);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -74,9 +74,33 @@ class SSTemplateParser extends Parser implements TemplateParser {
|
|||||||
protected $includeDebuggingComments = false;
|
protected $includeDebuggingComments = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override the Parser constructor to change the requirement of setting a string
|
* Stores the user-supplied closed block extension rules in the form:
|
||||||
|
* array(
|
||||||
|
* 'name' => function (&$res) {}
|
||||||
|
* )
|
||||||
|
* See SSTemplateParser::ClosedBlock_Handle_Loop for an example of what the callable should look like
|
||||||
|
* @var array
|
||||||
*/
|
*/
|
||||||
function __construct() {
|
protected $closedBlocks = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the user-supplied open block extension rules in the form:
|
||||||
|
* array(
|
||||||
|
* 'name' => function (&$res) {}
|
||||||
|
* )
|
||||||
|
* See SSTemplateParser::OpenBlock_Handle_Base_tag for an example of what the callable should look like
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $openBlocks = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow the injection of new closed & open block callables
|
||||||
|
* @param array $closedBlocks
|
||||||
|
* @param array $openBlocks
|
||||||
|
*/
|
||||||
|
public function __construct($closedBlocks = array(), $openBlocks = array()) {
|
||||||
|
$this->setClosedBlocks($closedBlocks);
|
||||||
|
$this->setOpenBlocks($openBlocks);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -88,6 +112,84 @@ class SSTemplateParser extends Parser implements TemplateParser {
|
|||||||
return $res;
|
return $res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the closed blocks that the template parser should use
|
||||||
|
*
|
||||||
|
* This method will delete any existing closed blocks, please use addClosedBlock if you don't
|
||||||
|
* want to overwrite
|
||||||
|
* @param array $closedBlocks
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function setClosedBlocks($closedBlocks) {
|
||||||
|
$this->closedBlocks = array();
|
||||||
|
foreach ((array) $closedBlocks as $name => $callable) {
|
||||||
|
$this->addClosedBlock($name, $callable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the open blocks that the template parser should use
|
||||||
|
*
|
||||||
|
* This method will delete any existing open blocks, please use addOpenBlock if you don't
|
||||||
|
* want to overwrite
|
||||||
|
* @param array $openBlocks
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function setOpenBlocks($openBlocks) {
|
||||||
|
$this->openBlocks = array();
|
||||||
|
foreach ((array) $openBlocks as $name => $callable) {
|
||||||
|
$this->addOpenBlock($name, $callable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a closed block callable to allow <% name %><% end_name %> syntax
|
||||||
|
* @param string $name The name of the token to be used in the syntax <% name %><% end_name %>
|
||||||
|
* @param callable $callable The function that modifies the generation of template code
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function addClosedBlock($name, $callable) {
|
||||||
|
$this->validateExtensionBlock($name, $callable, 'Closed block');
|
||||||
|
$this->closedBlocks[$name] = $callable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a closed block callable to allow <% name %> syntax
|
||||||
|
* @param string $name The name of the token to be used in the syntax <% name %>
|
||||||
|
* @param callable $callable The function that modifies the generation of template code
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function addOpenBlock($name, $callable) {
|
||||||
|
$this->validateExtensionBlock($name, $callable, 'Open block');
|
||||||
|
$this->openBlocks[$name] = $callable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the arguments to addOpenBlock and addClosedBlock are valid
|
||||||
|
* @param $name
|
||||||
|
* @param $callable
|
||||||
|
* @param $type
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
protected function validateExtensionBlock($name, $callable, $type) {
|
||||||
|
if (!is_string($name)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
sprintf(
|
||||||
|
"Name argument for %s must be a string",
|
||||||
|
$type
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} elseif (!is_callable($callable)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
sprintf(
|
||||||
|
"Callable %s argument named '%s' is not callable",
|
||||||
|
$type,
|
||||||
|
$name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Template: (Comment | Translate | If | Require | CacheBlock | UncachedBlock | OldI18NTag | Include | ClosedBlock |
|
/* Template: (Comment | Translate | If | Require | CacheBlock | UncachedBlock | OldI18NTag | Include | ClosedBlock |
|
||||||
OpenBlock | MalformedBlock | Injection | Text)+ */
|
OpenBlock | MalformedBlock | Injection | Text)+ */
|
||||||
protected $match_Template_typestack = array('Template');
|
protected $match_Template_typestack = array('Template');
|
||||||
@ -3591,10 +3693,13 @@ class SSTemplateParser extends Parser implements TemplateParser {
|
|||||||
$blockname = $res['BlockName']['text'];
|
$blockname = $res['BlockName']['text'];
|
||||||
|
|
||||||
$method = 'ClosedBlock_Handle_'.$blockname;
|
$method = 'ClosedBlock_Handle_'.$blockname;
|
||||||
if (method_exists($this, $method)) $res['php'] = $this->$method($res);
|
if (method_exists($this, $method)) {
|
||||||
else {
|
$res['php'] = $this->$method($res);
|
||||||
|
} else if (isset($this->closedBlocks[$blockname])) {
|
||||||
|
$res['php'] = call_user_func($this->closedBlocks[$blockname], $res);
|
||||||
|
} else {
|
||||||
throw new SSTemplateParseException('Unknown closed block "'.$blockname.'" encountered. Perhaps you are ' .
|
throw new SSTemplateParseException('Unknown closed block "'.$blockname.'" encountered. Perhaps you are ' .
|
||||||
'not supposed to close this block, or have mis-spelled it?', $this);
|
'not supposed to close this block, or have mis-spelled it?', $this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3738,10 +3843,13 @@ class SSTemplateParser extends Parser implements TemplateParser {
|
|||||||
$blockname = $res['BlockName']['text'];
|
$blockname = $res['BlockName']['text'];
|
||||||
|
|
||||||
$method = 'OpenBlock_Handle_'.$blockname;
|
$method = 'OpenBlock_Handle_'.$blockname;
|
||||||
if (method_exists($this, $method)) $res['php'] = $this->$method($res);
|
if (method_exists($this, $method)) {
|
||||||
else {
|
$res['php'] = $this->$method($res);
|
||||||
|
} elseif (isset($this->openBlocks[$blockname])) {
|
||||||
|
$res['php'] = call_user_func($this->openBlocks[$blockname], $res);
|
||||||
|
} else {
|
||||||
throw new SSTemplateParseException('Unknown open block "'.$blockname.'" encountered. Perhaps you missed ' .
|
throw new SSTemplateParseException('Unknown open block "'.$blockname.'" encountered. Perhaps you missed ' .
|
||||||
' the closing tag or have mis-spelled it?', $this);
|
' the closing tag or have mis-spelled it?', $this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,9 +95,33 @@ class SSTemplateParser extends Parser implements TemplateParser {
|
|||||||
protected $includeDebuggingComments = false;
|
protected $includeDebuggingComments = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override the Parser constructor to change the requirement of setting a string
|
* Stores the user-supplied closed block extension rules in the form:
|
||||||
|
* array(
|
||||||
|
* 'name' => function (&$res) {}
|
||||||
|
* )
|
||||||
|
* See SSTemplateParser::ClosedBlock_Handle_Loop for an example of what the callable should look like
|
||||||
|
* @var array
|
||||||
*/
|
*/
|
||||||
function __construct() {
|
protected $closedBlocks = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the user-supplied open block extension rules in the form:
|
||||||
|
* array(
|
||||||
|
* 'name' => function (&$res) {}
|
||||||
|
* )
|
||||||
|
* See SSTemplateParser::OpenBlock_Handle_Base_tag for an example of what the callable should look like
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $openBlocks = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow the injection of new closed & open block callables
|
||||||
|
* @param array $closedBlocks
|
||||||
|
* @param array $openBlocks
|
||||||
|
*/
|
||||||
|
public function __construct($closedBlocks = array(), $openBlocks = array()) {
|
||||||
|
$this->setClosedBlocks($closedBlocks);
|
||||||
|
$this->setOpenBlocks($openBlocks);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -109,6 +133,84 @@ class SSTemplateParser extends Parser implements TemplateParser {
|
|||||||
return $res;
|
return $res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the closed blocks that the template parser should use
|
||||||
|
*
|
||||||
|
* This method will delete any existing closed blocks, please use addClosedBlock if you don't
|
||||||
|
* want to overwrite
|
||||||
|
* @param array $closedBlocks
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function setClosedBlocks($closedBlocks) {
|
||||||
|
$this->closedBlocks = array();
|
||||||
|
foreach ((array) $closedBlocks as $name => $callable) {
|
||||||
|
$this->addClosedBlock($name, $callable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the open blocks that the template parser should use
|
||||||
|
*
|
||||||
|
* This method will delete any existing open blocks, please use addOpenBlock if you don't
|
||||||
|
* want to overwrite
|
||||||
|
* @param array $openBlocks
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function setOpenBlocks($openBlocks) {
|
||||||
|
$this->openBlocks = array();
|
||||||
|
foreach ((array) $openBlocks as $name => $callable) {
|
||||||
|
$this->addOpenBlock($name, $callable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a closed block callable to allow <% name %><% end_name %> syntax
|
||||||
|
* @param string $name The name of the token to be used in the syntax <% name %><% end_name %>
|
||||||
|
* @param callable $callable The function that modifies the generation of template code
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function addClosedBlock($name, $callable) {
|
||||||
|
$this->validateExtensionBlock($name, $callable, 'Closed block');
|
||||||
|
$this->closedBlocks[$name] = $callable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a closed block callable to allow <% name %> syntax
|
||||||
|
* @param string $name The name of the token to be used in the syntax <% name %>
|
||||||
|
* @param callable $callable The function that modifies the generation of template code
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function addOpenBlock($name, $callable) {
|
||||||
|
$this->validateExtensionBlock($name, $callable, 'Open block');
|
||||||
|
$this->openBlocks[$name] = $callable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the arguments to addOpenBlock and addClosedBlock are valid
|
||||||
|
* @param $name
|
||||||
|
* @param $callable
|
||||||
|
* @param $type
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
protected function validateExtensionBlock($name, $callable, $type) {
|
||||||
|
if (!is_string($name)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
sprintf(
|
||||||
|
"Name argument for %s must be a string",
|
||||||
|
$type
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} elseif (!is_callable($callable)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
sprintf(
|
||||||
|
"Callable %s argument named '%s' is not callable",
|
||||||
|
$type,
|
||||||
|
$name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*!* SSTemplateParser
|
/*!* SSTemplateParser
|
||||||
|
|
||||||
# Template is any structurally-complete portion of template (a full nested level in other words). It's the
|
# Template is any structurally-complete portion of template (a full nested level in other words). It's the
|
||||||
@ -771,10 +873,13 @@ class SSTemplateParser extends Parser implements TemplateParser {
|
|||||||
$blockname = $res['BlockName']['text'];
|
$blockname = $res['BlockName']['text'];
|
||||||
|
|
||||||
$method = 'ClosedBlock_Handle_'.$blockname;
|
$method = 'ClosedBlock_Handle_'.$blockname;
|
||||||
if (method_exists($this, $method)) $res['php'] = $this->$method($res);
|
if (method_exists($this, $method)) {
|
||||||
else {
|
$res['php'] = $this->$method($res);
|
||||||
|
} else if (isset($this->closedBlocks[$blockname])) {
|
||||||
|
$res['php'] = call_user_func($this->closedBlocks[$blockname], $res);
|
||||||
|
} else {
|
||||||
throw new SSTemplateParseException('Unknown closed block "'.$blockname.'" encountered. Perhaps you are ' .
|
throw new SSTemplateParseException('Unknown closed block "'.$blockname.'" encountered. Perhaps you are ' .
|
||||||
'not supposed to close this block, or have mis-spelled it?', $this);
|
'not supposed to close this block, or have mis-spelled it?', $this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -861,10 +966,13 @@ class SSTemplateParser extends Parser implements TemplateParser {
|
|||||||
$blockname = $res['BlockName']['text'];
|
$blockname = $res['BlockName']['text'];
|
||||||
|
|
||||||
$method = 'OpenBlock_Handle_'.$blockname;
|
$method = 'OpenBlock_Handle_'.$blockname;
|
||||||
if (method_exists($this, $method)) $res['php'] = $this->$method($res);
|
if (method_exists($this, $method)) {
|
||||||
else {
|
$res['php'] = $this->$method($res);
|
||||||
|
} elseif (isset($this->openBlocks[$blockname])) {
|
||||||
|
$res['php'] = call_user_func($this->openBlocks[$blockname], $res);
|
||||||
|
} else {
|
||||||
throw new SSTemplateParseException('Unknown open block "'.$blockname.'" encountered. Perhaps you missed ' .
|
throw new SSTemplateParseException('Unknown open block "'.$blockname.'" encountered. Perhaps you missed ' .
|
||||||
' the closing tag or have mis-spelled it?', $this);
|
' the closing tag or have mis-spelled it?', $this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user