Merge branch '3.1'

This commit is contained in:
Andrew Short 2013-03-19 22:36:47 +11:00
commit 94f209eb74
29 changed files with 466 additions and 83 deletions

View File

@ -22,6 +22,7 @@ matrix:
- php: 5.4
env:
- PHPCS=1
- env: TESTDB=SQLITE
before_script:
- pear install pear/PHP_CodeSniffer

View File

@ -45,7 +45,7 @@ SS_Cache::add_backend('aggregatestore', 'File', array('cache_dir' => $aggregatec
SS_Cache::pick_backend('aggregatestore', 'aggregate', 1000);
// If you don't want to see deprecation errors for the new APIs, change this to 3.0.0-dev.
Deprecation::notification_version('3.0.0');
Deprecation::notification_version('3.1.0');
// TODO Remove once new ManifestBuilder with submodule support is in place
require_once('admin/_config.php');

View File

@ -691,7 +691,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
* @return String Nested unordered list with links to each page
*/
public function getSiteTreeFor($className, $rootID = null, $childrenMethod = null, $numChildrenMethod = null,
$filterFunction = null, $minNodeCount = 30) {
$filterFunction = null, $nodeCountThreshold = 30) {
// Filter criteria
$params = $this->request->getVar('q');
@ -719,7 +719,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
// Mark the nodes of the tree to return
if ($filterFunction) $obj->setMarkingFilterFunction($filterFunction);
$obj->markPartialTree($minNodeCount, $this, $childrenMethod, $numChildrenMethod);
$obj->markPartialTree($nodeCountThreshold, $this, $childrenMethod, $numChildrenMethod);
// Ensure current page is exposed
if($p = $this->currentPage()) $obj->markToExpose($p);
@ -744,7 +744,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
true,
$childrenMethod,
$numChildrenMethod,
$minNodeCount
$nodeCountThreshold
);
// Wrap the root if needs be.

View File

@ -437,8 +437,8 @@ jQuery.noConflict();
contentEls.removeClass('loading');
},
success: function(data, status, xhr) {
var els = self.handleAjaxResponse(data, status, xhr);
self.trigger('afterstatechange', {data: data, status: status, xhr: xhr, element: els});
var els = self.handleAjaxResponse(data, status, xhr, state);
self.trigger('afterstatechange', {data: data, status: status, xhr: xhr, element: els, state: state});
}
});
@ -449,8 +449,14 @@ jQuery.noConflict();
* Handles ajax responses containing plain HTML, or mulitple
* PJAX fragments wrapped in JSON (see PjaxResponseNegotiator PHP class).
* Can be hooked into an ajax 'success' callback.
*
* Parameters:
* (Object) data
* (String) status
* (XMLHTTPRequest) xhr
* (Object) state The original history state which the request was initiated with
*/
handleAjaxResponse: function(data, status, xhr) {
handleAjaxResponse: function(data, status, xhr, state) {
var self = this, url, selectedTabs, guessFragment;
// Support a full reload
@ -545,7 +551,7 @@ jQuery.noConflict();
this.redraw();
this.restoreTabState();
this.restoreTabState(state.data.tabState !== 'undefined' ? state.data.tabState : null);
return newContentEls;
},
@ -620,20 +626,35 @@ jQuery.noConflict();
/**
* Re-select previously saved tabs.
* Requires HTML5 sessionStorage support.
*
* Parameters:
* (Object) Map of tab container selectors to tab selectors.
* Used to mark a specific tab as active regardless of the previously saved options.
*/
restoreTabState: function() {
if(typeof(window.sessionStorage)=="undefined" || window.sessionStorage === null) return;
restoreTabState: function(overrideStates) {
var self = this, url = this._tabStateUrl(),
data = window.sessionStorage.getItem('tabs-' + url),
selectedTabs = data ? JSON.parse(data) : false;
if(selectedTabs) {
$.each(selectedTabs, function(i, selectedTab) {
var el = self.find('#' + selectedTab.id);
if(!el.data('tabs')) return; // don't act on uninit'ed controls
el.tabs('select', selectedTab.selected);
});
}
hasSessionStorage = (typeof(window.sessionStorage)!=="undefined" && window.sessionStorage),
sessionData = hasSessionStorage ? window.sessionStorage.getItem('tabs-' + url) : null,
sessionStates = sessionData ? JSON.parse(sessionData) : false;
this.find('.cms-tabset').each(function() {
var index, tabset = $(this), tabsetId = tabset.attr('id'), tab,
forcedTab = tabset.find('.ss-tabs-force-active');
if(!tabset.data('tabs')) return; // don't act on uninit'ed controls
if(forcedTab.length) {
index = forcedTab.index();
} else if(overrideStates && overrideStates[tabsetId]) {
tab = tabset.find(overrideStates[tabsetId].tabSelector);
if(tab.length) index = tab.index();
} else if(sessionStates) {
$.each(sessionStates, function(i, sessionState) {
if(tabset.is('#' + sessionState.id)) index = sessionState.selected;
});
}
if(index !== null) tabset.tabs('select', index);
});
},
/**

View File

@ -13,6 +13,7 @@
* - SS_DATABASE_SERVER: The database server to use, defaulting to localhost
* - SS_DATABASE_USERNAME: The database username (mandatory)
* - SS_DATABASE_PASSWORD: The database password (mandatory)
* - SS_DATABASE_PORT: The database port
* - SS_DATABASE_SUFFIX: A suffix to add to the database name.
* - SS_DATABASE_PREFIX: A prefix to add to the database name.
* - SS_DATABASE_TIMEZONE: Set the database timezone to something other than the system timezone.
@ -95,6 +96,11 @@ if(defined('SS_DATABASE_USERNAME') && defined('SS_DATABASE_PASSWORD')) {
. (defined('SS_DATABASE_SUFFIX') ? SS_DATABASE_SUFFIX : ''),
);
// Set the port if called for
if(defined('SS_DATABASE_PORT')) {
$databaseConfig['port'] = SS_DATABASE_PORT;
}
// Set the timezone if called for
if (defined('SS_DATABASE_TIMEZONE')) {
$databaseConfig['timezone'] = SS_DATABASE_TIMEZONE;

View File

@ -598,7 +598,13 @@ class Config_LRU {
protected $c = 0;
public function __construct() {
$this->cache = new SplFixedArray(self::SIZE);
if (version_compare(PHP_VERSION, '5.3.7', '<')) {
// SplFixedArray causes seg faults before PHP 5.3.7
$this->cache = array();
}
else {
$this->cache = new SplFixedArray(self::SIZE);
}
// Pre-fill with stdClass instances. By reusing we avoid object-thrashing
for ($i = 0; $i < self::SIZE; $i++) {

View File

@ -119,6 +119,7 @@ if(!isset($_SERVER['HTTP_HOST'])) {
if($_REQUEST) stripslashes_recursively($_REQUEST);
if($_GET) stripslashes_recursively($_GET);
if($_POST) stripslashes_recursively($_POST);
if($_COOKIE) stripslashes_recursively($_COOKIE);
}
/**

View File

@ -78,7 +78,7 @@ class SS_ConfigStaticManifest {
$static = $this->statics[$class][$name];
if ($static['access'] != T_PRIVATE) {
Deprecation::notice('3.1.0', "Config static $class::\$$name must be marked as private", Deprecation::SCOPE_GLOBAL);
Deprecation::notice('3.2.0', "Config static $class::\$$name must be marked as private", Deprecation::SCOPE_GLOBAL);
// Don't warn more than once per static
$static['access'] = T_PRIVATE;
}

View File

@ -12,6 +12,7 @@
* CMS form fields now support help text through `setDescription()`, both inline and as tooltips
* Removed SiteTree "MetaTitle" and "MetaKeywords" fields
* More legible and simplified tab and menu styling in the CMS
* Dropped support for Internet Explorer 7
### Framework

View File

@ -0,0 +1,68 @@
# Howto: Customize the Pages List in the CMS
The pages "list" view in the CMS is a powerful alternative to visualizing
your site's content, and can be better suited than a tree for large flat
hierarchies. A good example would be a collection of news articles,
all contained under a "holder" page type, a quite common pattern in SilverStripe.
The "list" view allows you to paginate through a large number of records,
as well as sort and filter them in a way that would be hard to achieve in a tree structure.
But sometimes the default behaviour isn't powerful enough, and you want a more
specific list view for certain page types, for example to sort the list by
a different criteria, or add more columns to filter on. The resulting
form is mainly based around a `[GridField](/reference/grid-field)` instance,
which in turn includes all children in a `[DataList](/topics/datamodel)`.
You can use these two classes as a starting point for your customizations.
Here's a brief example on how to add sorting and a new column for a
hypothetical `NewsPageHolder` type, which contains `NewsPage` children.
:::php
// mysite/code/NewsPageHolder.php
class NewsPageHolder extends Page {
static $allowed_children = array('NewsPage');
}
// mysite/code/NewsPage.php
class NewsPage extends Page {
static $has_one = array(
'Author' => 'Member',
);
}
We'll now add an `Extension` subclass to `LeftAndMain`, which is the main CMS controller.
This allows us to intercept the list building logic, and alter the `GridField`
before its rendered. In this case, we limit our logic to the desired page type,
although it's just as easy to implement changes which apply to all page types,
or across page types with common characteristics.
:::php
// mysite/code/NewsPageHolderCMSMainExtension.php
class NewsPageHolderCMSMainExtension extends Extension {
function updateListView($listView) {
$parentId = $listView->getController()->getRequest()->requestVar('ParentID');
$parent = ($parentId) ? Page::get()->byId($parentId) : new Page();
// Only apply logic for this page type
if($parent && $parent instanceof NewsPageHolder) {
$gridField = $listView->Fields()->dataFieldByName('Page');
if($gridField) {
// Sort by created
$list = $gridField->getList();
$gridField->setList($list->sort('Created', 'DESC'));
// Add author to columns
$cols = $gridField->getConfig()->getComponentByType('GridFieldDataColumns');
if($cols) {
$fields = $cols->getDisplayFields($gridField);
$fields['Author.Title'] = 'Author';
$cols->setDisplayFields($fields);
}
}
}
}
}
// mysite/_config/config.yml
LeftAndMain:
extensions:
- NewsPageHolderCMSMainExtension

View File

@ -6,6 +6,7 @@ on tasks and goals rather than going into deep details.
You will find it useful to read the introduction [tutorials](/tutorials) before tackling these How-Tos so you can understand some of
the language and functions which are used in the guides.
* [Howto: Customize the Pages List in the CMS](customize-cms-pages-list)
* [Import CSV Data](csv-import). Build a simple CSV importer using either [api:ModelAdmin] or a custom controller
* [Dynamic Default Fields](dynamic-default-fields). Pre populate a [api:DataObject] with data.
* [Grouping Lists](grouping-dataobjectsets). Group results in a [api:SS_List] to create sub sections.

View File

@ -42,7 +42,7 @@ A typical website page on a conservative single CPU machine (e.g., Intel 2Ghz) t
## Client side (CMS) requirements
SilverStripe CMS is designed to work well with Firefox 3.0+ and Internet Explorer 7.0+. We aim to provide satisfactory experiences in Apple Safari and Google Chrome. SilverStripe CMS works well across Windows, Linux, and Mac operating systems.
SilverStripe CMS is designed to work well with Google Chrome, Mozilla Firefox and Internet Explorer 8+. We aim to provide satisfactory experiences in Apple Safari. SilverStripe CMS works well across Windows, Linux, and Mac operating systems.
## End user requirements ##

View File

@ -9,7 +9,7 @@ implementation. Have a look at `[api:Object->useCustomClass()]`.
## Usage
Your extension will nee to be a subclass of `[api:DataExtension]` or the `[api:Extension]` class.
Your extension will need to be a subclass of `[api:DataExtension]` or the `[api:Extension]` class.
:::php
<?php
@ -155,4 +155,4 @@ extended by.
## API Documentation
`[api:DataExtension]`
`[api:DataExtension]`

View File

@ -9,23 +9,23 @@ This is a highlevel overview of available `[api:FormField]` subclasses. An autom
* `[api:ReadonlyField]`: Read-only field to display a non-editable value with a label.
* `[api:TextareaField]`: Multi-line text field.
* `[api:TextField]`: Single-line text field.
* `[api:PasswordField]`: Masked input field
* `[api:PasswordField]`: Masked input field.
## Actions
## Actions
* `[api:FormAction]`: Button element for forms, both for `<input type="submit">` and `<button>`.
* `[api:ResetFormAction]`: Action that clears all fields on a form.
## Formatted Input
* `[api:AjaxUniqueTextField]`: Text field that automatically checks that the value entered is unique for the given set of fields in a given set of tables
* `[api:AjaxUniqueTextField]`: Text field that automatically checks that the value entered is unique for the given set of fields in a given set of tables.
* `[api:ConfirmedPasswordField]`: Two masked input fields, checks for matching passwords.
* `[api:CountryDropdownField]`: A simple extension to dropdown field, pre-configured to list countries.
* `[api:CreditCardField]`: Allows input of credit card numbers via four separate form fields, including generic validation of its numeric values.
* `[api:CurrencyField]`: Text field, validating its input as a currency. Limited to US-centric formats, including a hardcoded currency symbol and decimal separators.
See `[api:MoneyField]` for a more flexible implementation.
* `[api:DateField]`: Represents a date in a single input field, or separated into day, month, and year. Can optionally use a calendar popup.
* `[api:DatetimeField]`: Combined date- and time field
* `[api:DatetimeField]`: Combined date- and time field.
* `[api:EmailField]`: Text input field with validation for correct email format according to RFC 2822.
* `[api:GroupedDropdownField]`: Grouped dropdown, using <optgroup> tags.
* `[api:HTMLEditorField].
@ -43,7 +43,7 @@ doesn't necessarily have any visible styling.
* `[api:FieldGroup] attached in CMS-context.
* `[api:FieldList]`: Basic container for sequential fields, or nested fields through CompositeField.
* `[api:TabSet]`: Collection of fields which is rendered as separate tabs. Can be nested.
* `[api:Tab]`: A single tab inside a `TabSet`
* `[api:Tab]`: A single tab inside a `TabSet`.
* `[api:ToggleCompositeField]`: Allows visibility of a group of fields to be toggled.
* `[api:ToggleField]`: ReadonlyField with added toggle-capabilities - will preview the first sentence of the contained text-value, and show the full content by a javascript-switch.
@ -58,7 +58,7 @@ doesn't necessarily have any visible styling.
* `[api:TableField]`: In-place editing of tabular data.
* `[api:TreeDropdownField]`: Dropdown-like field that allows you to select an item from a hierarchical AJAX-expandable tree.
* `[api:TreeMultiselectField]`: Represents many-many joins using a tree selector shown in a dropdown-like element
* `[api:GridField](/reference/grid-field)`: Displays a `[api:SS_List]` in a tabular format. Versatile base class which can be configured to allow editing, sorting, etc.
* `[api:GridField]`: Displays a `[api:SS_List]` in a tabular format. Versatile base class which can be configured to allow editing, sorting, etc.
* `[api:ListboxField]`: Multi-line listbox field, through `<select multiple>`.
@ -67,6 +67,6 @@ doesn't necessarily have any visible styling.
* `[api:DatalessField]` - Base class for fields which add some HTML to the form but don't submit any data or
save it to the database
* `[api:HeaderField]`: Renders a simple HTML header element.
* `[api:HiddenField]`
* `[api:HiddenField]`.
* `[api:LabelField]`: Simple label tag. This can be used to add extra text in your forms.
* `[api:LiteralField]`: Renders arbitrary HTML into a form.

View File

@ -54,5 +54,4 @@ adherence to conventions, writing documentation, and releasing updates. See [con
* [Modules](modules)
* [Module Release Process](module-release-process)
* [Debugging methods](/topics/debugging)
* [URL Variable Tools](/reference/urlvariabletools) - Lists a number of <20><><EFBFBD>page options<6E><73><EFBFBD> , <20><><EFBFBD>rendering tools<6C><73><EFBFBD> or <20><><EFBFBD>special
URL variables<65><73><EFBFBD> that you can use to debug your SilverStripe applications
* [URL Variable Tools](/reference/urlvariabletools) - Lists a number of page options, rendering tools or special URL variables that you can use to debug your SilverStripe applications

View File

@ -245,7 +245,8 @@ First, the template for displaying a single article:
:::ss
<div class="content-container">
<% include SideBar %>
<div class="content-container unit size3of4 lastUnit">
<article>
<h1>$Title</h1>
<div class="news-details">
@ -255,7 +256,6 @@ First, the template for displaying a single article:
</article>
$Form
</div>
<% include SideBar %>
Most of the code is just like the regular Page.ss, we include an informational div with the date and the author of the Article.
@ -278,7 +278,8 @@ We'll now create a template for the article holder. We want our news section to
**themes/simple/templates/Layout/ArticleHolder.ss**
:::ss
<div class="content-container">
<% include SideBar %>
<div class="content-container unit size3of4 lastUnit">
<article>
<h1>$Title</h1>
$Content
@ -293,7 +294,6 @@ We'll now create a template for the article holder. We want our news section to
<% end_loop %>
$Form
</div>
<% include SideBar %>
Here we use the page control *Children*. As the name suggests, this control allows you to iterate over the children of a page. In this case, the children are our news articles. The *$Link* variable will give the address of the article which we can use to create a link, and the *FirstParagraph* function of the `[api:HTMLText]` field gives us a nice summary of the article. The function strips all tags from the paragraph extracted.
@ -482,7 +482,8 @@ The staff section templates aren't too difficult to create, thanks to the utilit
**themes/simple/templates/Layout/StaffHolder.ss**
:::ss
<div class="content-container">
<% include SideBar %>
<div class="content-container unit size3of4 lastUnit">
<article>
<h1>$Title</h1>
$Content
@ -498,7 +499,6 @@ The staff section templates aren't too difficult to create, thanks to the utilit
<% end_loop %>
$Form
</div>
<% include SideBar %>
This template is very similar to the *ArticleHolder* template. The *SetWidth* method of the `[api:Image]` class
@ -512,7 +512,8 @@ The *StaffPage* template is also very straight forward.
**themes/simple/templates/Layout/StaffPage.ss**
:::ss
<div class="content-container">
<% include SideBar %>
<div class="content-container unit size3of4 lastUnit">
<article>
<h1>$Title</h1>
<div class="content">
@ -521,7 +522,6 @@ The *StaffPage* template is also very straight forward.
</article>
$Form
</div>
<% include SideBar %>
Here we use the *SetWidth* method to get a different sized image from the same source image. You should now have
a complete staff section.

View File

@ -278,7 +278,8 @@ a named list of object.
**themes/simple/templates/Layout/ProjectsHolder.ss**
:::ss
<div class="content-container typography">
<% include SideBar %>
<div class="content-container unit size3of4 lastUnit">
<article>
<h1>$Title</h1>
<div class="content">
@ -314,7 +315,6 @@ a named list of object.
</div>
</article>
</div>
<% include SideBar %>
Navigate to the holder page through your website navigation,
or the "Preview" feature in the CMS. You should see a list of all projects now.
@ -336,7 +336,8 @@ we can access the "Students" and "Mentors" relationships directly in the templat
**themes/simple/templates/Layout/Project.ss**
:::ss
<div class="content-container typography">
<% include SideBar %>
<div class="content-container unit size3of4 lastUnit">
<article>
<h1>$Title</h1>
<div class="content">
@ -364,7 +365,6 @@ we can access the "Students" and "Mentors" relationships directly in the templat
</div>
</article>
</div>
<% include SideBar %>
Follow the link to a project detail from from your holder page,
or navigate to it through the submenu provided by the theme.

View File

@ -57,8 +57,7 @@ class GDBackend extends Object implements Image_Backend {
}
public function setGD($gd) {
Deprecation::notice('3.1', 'Use GD::setImageResource instead',
Deprecation::SCOPE_CLASS);
Deprecation::notice('3.1', 'Use GD::setImageResource instead');
return $this->setImageResource($gd);
}
@ -67,8 +66,7 @@ class GDBackend extends Object implements Image_Backend {
}
public function getGD() {
Deprecation::notice('3.1', 'GD::getImageResource instead',
Deprecation::SCOPE_CLASS);
Deprecation::notice('3.1', 'GD::getImageResource instead');
return $this->getImageResource();
}

View File

@ -181,7 +181,7 @@ class CheckboxSetField extends OptionsetField {
public function saveInto(DataObjectInterface $record) {
$fieldname = $this->name;
$relation = ($fieldname && $record && $record->hasMethod($fieldname)) ? $record->$fieldname() : null;
if($fieldname && $record && $relation && $relation instanceof RelationList) {
if($fieldname && $record && $relation && ($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) {
$idList = array();
if($this->value) foreach($this->value as $id => $bool) {
if($bool) {

View File

@ -179,7 +179,7 @@ class ListboxField extends DropdownField {
if($this->multiple) {
$fieldname = $this->name;
$relation = ($fieldname && $record && $record->hasMethod($fieldname)) ? $record->$fieldname() : null;
if($fieldname && $record && $relation && $relation instanceof RelationList) {
if($fieldname && $record && $relation && ($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) {
$idList = (is_array($this->value)) ? array_values($this->value) : array();
if(!$record->ID) {
$record->write(); // record needs to have an ID in order to set relationships

View File

@ -115,13 +115,20 @@ class TreeMultiselectField extends TreeDropdownField {
$title = _t('DropdownField.CHOOSE', '(Choose)', 'start value of a dropdown');
}
$dataUrlTree = '';
if ($this->form){
$dataUrlTree = $this->Link('tree');
if (isset($idArray) && count($idArray)){
$dataUrlTree .= '?forceValue='.implode(',',$idArray);
}
}
return FormField::create_tag(
'div',
array (
'id' => "TreeDropdownField_{$this->id()}",
'class' => 'TreeDropdownField multiple' . ($this->extraClass() ? " {$this->extraClass()}" : '')
. ($this->showSearch ? " searchable" : ''),
'data-url-tree' => $this->form ? $this->Link('tree') : "",
'data-url-tree' => $dataUrlTree,
'data-title' => $title,
'title' => $this->getDescription()
),

View File

@ -13,7 +13,6 @@ var ss = ss || {};
* Caution: Incomplete and unstable API.
*/
ss.editorWrappers = {};
ss.editorWrappers.initial
ss.editorWrappers.tinyMCE = (function() {
return {
init: function(config) {

View File

@ -74,12 +74,17 @@ class Hierarchy extends DataExtension {
* @param string $childrenMethod The name of the method used to get children from each object
* @param boolean $rootCall Set to true for this first call, and then to false for calls inside the recursion. You
* should not change this.
* @param int $minNodeCount
* @param int $nodeCountThreshold The lower bounds for the amount of nodes to mark. If set, the logic will expand
* nodes until it eaches at least this number, and then stops. Root nodes will always
* show regardless of this settting. Further nodes can be lazy-loaded via ajax.
* This isn't a hard limit. Example: On a value of 10, with 20 root nodes, each having
* 30 children, the actual node count will be 50 (all root nodes plus first expanded child).
*
* @return string
*/
public function getChildrenAsUL($attributes = "", $titleEval = '"<li>" . $child->Title', $extraArg = null,
$limitToMarked = false, $childrenMethod = "AllChildrenIncludingDeleted",
$numChildrenMethod = "numChildren", $rootCall = true, $minNodeCount = 30) {
$numChildrenMethod = "numChildren", $rootCall = true, $nodeCountThreshold = 30) {
if($limitToMarked && $rootCall) {
$this->markingFinished($numChildrenMethod);
@ -103,9 +108,25 @@ class Hierarchy extends DataExtension {
if(!$limitToMarked || $child->isMarked()) {
$foundAChild = true;
$output .= (is_callable($titleEval)) ? $titleEval($child) : eval("return $titleEval;");
$output .= "\n" .
$child->getChildrenAsUL("", $titleEval, $extraArg, $limitToMarked, $childrenMethod,
$numChildrenMethod, false, $minNodeCount) . "</li>\n";
$output .= "\n";
$numChildren = $child->$numChildrenMethod();
if(
// Always traverse into opened nodes (they might be exposed as parents of search results)
$child->isExpanded()
// Only traverse into children if we haven't reached the maximum node count already.
// Otherwise, the remaining nodes are lazy loaded via ajax.
&& $child->isMarked()
) {
$output .= $child->getChildrenAsUL("", $titleEval, $extraArg, $limitToMarked, $childrenMethod,
$numChildrenMethod, false, $nodeCountThreshold);
}
elseif($child->isTreeOpened()) {
// Since we're not loading children, don't mark it as open either
$child->markClosed();
}
$output .= "</li>\n";
}
}
@ -125,21 +146,23 @@ class Hierarchy extends DataExtension {
* This method returns the number of nodes marked. After this method is called other methods
* can check isExpanded() and isMarked() on individual nodes.
*
* @param int $minNodeCount The minimum amount of nodes to mark.
* @param int $nodeCountThreshold See {@link getChildrenAsUL()}
* @return int The actual number of nodes marked.
*/
public function markPartialTree($minNodeCount = 30, $context = null,
public function markPartialTree($nodeCountThreshold = 30, $context = null,
$childrenMethod = "AllChildrenIncludingDeleted", $numChildrenMethod = "numChildren") {
if(!is_numeric($minNodeCount)) $minNodeCount = 30;
if(!is_numeric($nodeCountThreshold)) $nodeCountThreshold = 30;
$this->markedNodes = array($this->owner->ID => $this->owner);
$this->owner->markUnexpanded();
// foreach can't handle an ever-growing $nodes list
while(list($id, $node) = each($this->markedNodes)) {
$this->markChildren($node, $context, $childrenMethod, $numChildrenMethod);
if($minNodeCount && sizeof($this->markedNodes) >= $minNodeCount) {
$children = $this->markChildren($node, $context, $childrenMethod, $numChildrenMethod);
if($nodeCountThreshold && sizeof($this->markedNodes) > $nodeCountThreshold) {
// Undo marking children as opened since they're lazy loaded
if($children) foreach($children as $child) $child->markClosed();
break;
}
}
@ -200,6 +223,7 @@ class Hierarchy extends DataExtension {
/**
* Mark all children of the given node that match the marking filter.
* @param DataObject $node Parent node.
* @return DataList
*/
public function markChildren($node, $context = null, $childrenMethod = "AllChildrenIncludingDeleted",
$numChildrenMethod = "numChildren") {
@ -213,7 +237,13 @@ class Hierarchy extends DataExtension {
$node->markExpanded();
if($children) {
foreach($children as $child) {
if(!$this->markingFilter || $this->markingFilterMatches($child)) {
$markingMatches = $this->markingFilterMatches($child);
// Filtered results should always show opened, since actual matches
// might be hidden by non-matching parent nodes.
if($this->markingFilter && $markingMatches) {
$child->markOpened();
}
if(!$this->markingFilter || $markingMatches) {
if($child->$numChildrenMethod()) {
$child->markUnexpanded();
} else {
@ -223,6 +253,8 @@ class Hierarchy extends DataExtension {
}
}
}
return $children;
}
/**
@ -350,6 +382,15 @@ class Hierarchy extends DataExtension {
self::$marked[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
self::$treeOpened[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
}
/**
* Mark this DataObject's tree as closed.
*/
public function markClosed() {
if(isset(self::$treeOpened[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID])) {
unset(self::$treeOpened[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID]);
}
}
/**
* Check if this DataObject is marked.

View File

@ -54,7 +54,12 @@ class MySQLDatabase extends SS_Database {
* - timezone: (optional) The timezone offset. For example: +12:00, "Pacific/Auckland", or "SYSTEM"
*/
public function __construct($parameters) {
$this->dbConn = new MySQLi($parameters['server'], $parameters['username'], $parameters['password']);
if(!empty($parameters['port'])) {
$this->dbConn = new MySQLi($parameters['server'], $parameters['username'], $parameters['password'],
'', $parameters['port']);
} else {
$this->dbConn = new MySQLi($parameters['server'], $parameters['username'], $parameters['password']);
}
if($this->dbConn->connect_error) {
$this->databaseError("Couldn't connect to MySQL database | " . $this->dbConn->connect_error);

View File

@ -330,7 +330,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
} else {
$this->RememberLoginToken = null;
Cookie::set('alc_enc', null);
Cookie::forceExpiry('alc_enc');
Cookie::force_expiry('alc_enc');
}
// Clear the incorrect log-in count
@ -423,7 +423,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
$this->RememberLoginToken = null;
Cookie::set('alc_enc', null); // // Clear the Remember Me cookie
Cookie::forceExpiry('alc_enc');
Cookie::force_expiry('alc_enc');
// Switch back to live in order to avoid infinite loops when
// redirecting to the login screen (if this login screen is versioned)

View File

@ -46,8 +46,8 @@ class GDTest extends SapphireTest {
$samples = array();
for($y = 0; $y < $vertical; $y++) {
for($x = 0; $x < $horizontal; $x++) {
$colour = imagecolorat($gd->getGD(), $x * 5, $y * 5);
$samples[] = ImageColorsforIndex($gd->getGD(), $colour);
$colour = imagecolorat($gd->getImageResource(), $x * 5, $y * 5);
$samples[] = ImageColorsforIndex($gd->getImageResource(), $colour);
}
}
return $samples;

View File

@ -177,19 +177,23 @@ class DataExtensionTest_Player extends DataObject implements TestOnly {
class DataExtensionTest_PlayerExtension extends DataExtension implements TestOnly {
public static function add_to_class($class = null, $extensionClass = null, $args = null) {
public static function get_extra_config($class = null, $extensionClass = null, $args = null) {
$config = array();
// Only add these extensions if the $class is set to DataExtensionTest_Player, to
// test that the argument works.
if($class == 'DataExtensionTest_Player') {
Config::inst()->update($class, 'db', array(
$config['db'] = array(
'Address' => 'Text',
'DateBirth' => 'Date',
'Status' => "Enum('Shooter,Goalie')"
));
Config::inst()->update($class, 'defaults', array(
);
$config['defaults'] = array(
'Status' => 'Goalie'
));
);
}
return $config;
}
}

View File

@ -394,13 +394,6 @@ class DataListTest extends SapphireTest {
// $this->assertEquals('Joe', $list->Last()->Name, 'Last comment should be from Joe');
// }
public function testSimpleNegationFilter() {
$list = DataObjectTest_TeamComment::get();
$list = $list->filter('TeamID:Negation', $this->idFromFixture('DataObjectTest_Team', 'team1'));
$this->assertEquals(1, $list->count());
$this->assertEquals('Phil', $list->first()->Name, 'First comment should be from Bob');
}
public function testSimplePartialMatchFilter() {
$list = DataObjectTest_TeamComment::get();
$list = $list->filter('Name:PartialMatch', 'o')->sort('Name');

View File

@ -180,6 +180,238 @@ class HierarchyTest extends SapphireTest {
$this->assertEquals('Obj 2 &raquo; Obj 2a &raquo; Obj 2aa', $obj2aa->getBreadcrumbs());
}
public function testGetChildrenAsUL() {
$obj1 = $this->objFromFixture('HierarchyTest_Object', 'obj1');
$obj2 = $this->objFromFixture('HierarchyTest_Object', 'obj2');
$obj2a = $this->objFromFixture('HierarchyTest_Object', 'obj2a');
$obj2aa = $this->objFromFixture('HierarchyTest_Object', 'obj2aa');
$nodeCountThreshold = 30;
$root = new HierarchyTest_Object();
$root->markPartialTree($nodeCountThreshold);
$html = $root->getChildrenAsUL(
"",
'"<li id=\"" . $child->ID . "\">" . $child->Title',
null,
false,
"AllChildrenIncludingDeleted",
"numChildren",
true, // rootCall
$nodeCountThreshold
);
$parser = new CSSContentParser($html);
$node2 = $parser->getByXpath(
'//ul/li[@id="' . $obj2->ID . '"]'
);
$this->assertTrue(
(bool)$node2,
'Contains root elements'
);
$node2a = $parser->getByXpath(
'//ul/li[@id="' . $obj2->ID . '"]' .
'/ul/li[@id="' . $obj2a->ID . '"]'
);
$this->assertTrue(
(bool)$node2a,
'Contains child elements (in correct nesting)'
);
$node2aa = $parser->getByXpath(
'//ul/li[@id="' . $obj2->ID . '"]' .
'/ul/li[@id="' . $obj2a->ID . '"]' .
'/ul/li[@id="' . $obj2aa->ID . '"]'
);
$this->assertTrue(
(bool)$node2aa,
'Contains grandchild elements (in correct nesting)'
);
}
public function testGetChildrenAsULMinNodeCount() {
$obj1 = $this->objFromFixture('HierarchyTest_Object', 'obj1');
$obj2 = $this->objFromFixture('HierarchyTest_Object', 'obj2');
$obj2a = $this->objFromFixture('HierarchyTest_Object', 'obj2a');
// Set low enough that it should be fulfilled by root only elements
$nodeCountThreshold = 3;
$root = new HierarchyTest_Object();
$root->markPartialTree($nodeCountThreshold);
$html = $root->getChildrenAsUL(
"",
'"<li id=\"" . $child->ID . "\">" . $child->Title',
null,
false,
"AllChildrenIncludingDeleted",
"numChildren",
true,
$nodeCountThreshold
);
$parser = new CSSContentParser($html);
$node1 = $parser->getByXpath(
'//ul/li[@id="' . $obj1->ID . '"]'
);
$this->assertTrue(
(bool)$node1,
'Contains root elements'
);
$node2 = $parser->getByXpath(
'//ul/li[@id="' . $obj2->ID . '"]'
);
$this->assertTrue(
(bool)$node2,
'Contains root elements'
);
$node2a = $parser->getByXpath(
'//ul/li[@id="' . $obj2->ID . '"]' .
'/ul/li[@id="' . $obj2a->ID . '"]'
);
$this->assertFalse(
(bool)$node2a,
'Does not contains child elements because they exceed minNodeCount'
);
}
public function testGetChildrenAsULMinNodeCountWithMarkToExpose() {
$obj2 = $this->objFromFixture('HierarchyTest_Object', 'obj2');
$obj2a = $this->objFromFixture('HierarchyTest_Object', 'obj2a');
$obj2aa = $this->objFromFixture('HierarchyTest_Object', 'obj2aa');
// Set low enough that it should be fulfilled by root only elements
$nodeCountThreshold = 3;
$root = new HierarchyTest_Object();
$root->markPartialTree($nodeCountThreshold);
// Mark certain node which should be included regardless of minNodeCount restrictions
$root->markToExpose($obj2aa);
$html = $root->getChildrenAsUL(
"",
'"<li id=\"" . $child->ID . "\">" . $child->Title',
null,
false,
"AllChildrenIncludingDeleted",
"numChildren",
true,
$nodeCountThreshold
);
$parser = new CSSContentParser($html);
$node2 = $parser->getByXpath(
'//ul/li[@id="' . $obj2->ID . '"]'
);
$this->assertTrue(
(bool)$node2,
'Contains root elements'
);
$node2aa = $parser->getByXpath(
'//ul/li[@id="' . $obj2->ID . '"]' .
'/ul/li[@id="' . $obj2a->ID . '"]' .
'/ul/li[@id="' . $obj2aa->ID . '"]'
);
$this->assertTrue((bool)$node2aa);
}
public function testGetChildrenAsULMinNodeCountWithFilters() {
$obj1 = $this->objFromFixture('HierarchyTest_Object', 'obj1');
$obj2 = $this->objFromFixture('HierarchyTest_Object', 'obj2');
$obj2a = $this->objFromFixture('HierarchyTest_Object', 'obj2a');
$obj2aa = $this->objFromFixture('HierarchyTest_Object', 'obj2aa');
// Set low enough that it should fit all search matches without lazy loading
$nodeCountThreshold = 3;
$root = new HierarchyTest_Object();
// Includes nodes by filter regardless of minNodeCount restrictions
$root->setMarkingFilterFunction(function($record) use($obj2, $obj2a, $obj2aa) {
// Results need to include parent hierarchy, even if we just want to
// match the innermost node.
// var_dump($record->Title);
// var_dump(in_array($record->ID, array($obj2->ID, $obj2a->ID, $obj2aa->ID)));
return in_array($record->ID, array($obj2->ID, $obj2a->ID, $obj2aa->ID));
});
$root->markPartialTree($nodeCountThreshold);
$html = $root->getChildrenAsUL(
"",
'"<li id=\"" . $child->ID . "\">" . $child->Title',
null,
true, // limit to marked
"AllChildrenIncludingDeleted",
"numChildren",
true,
$nodeCountThreshold
);
$parser = new CSSContentParser($html);
$node1 = $parser->getByXpath(
'//ul/li[@id="' . $obj1->ID . '"]'
);
$this->assertFalse(
(bool)$node1,
'Does not contain root elements which dont match the filter'
);
$node2aa = $parser->getByXpath(
'//ul/li[@id="' . $obj2->ID . '"]' .
'/ul/li[@id="' . $obj2a->ID . '"]' .
'/ul/li[@id="' . $obj2aa->ID . '"]'
);
$this->assertTrue(
(bool)$node2aa,
'Contains non-root elements which match the filter'
);
}
public function testGetChildrenAsULHardLimitsNodes() {
$obj1 = $this->objFromFixture('HierarchyTest_Object', 'obj1');
$obj2 = $this->objFromFixture('HierarchyTest_Object', 'obj2');
$obj2a = $this->objFromFixture('HierarchyTest_Object', 'obj2a');
$obj2aa = $this->objFromFixture('HierarchyTest_Object', 'obj2aa');
// Set low enough that it should fit all search matches without lazy loading
$nodeCountThreshold = 3;
$root = new HierarchyTest_Object();
// Includes nodes by filter regardless of minNodeCount restrictions
$root->setMarkingFilterFunction(function($record) use($obj2, $obj2a, $obj2aa) {
// Results need to include parent hierarchy, even if we just want to
// match the innermost node.
// var_dump($record->Title);
// var_dump(in_array($record->ID, array($obj2->ID, $obj2a->ID, $obj2aa->ID)));
return in_array($record->ID, array($obj2->ID, $obj2a->ID, $obj2aa->ID));
});
$root->markPartialTree($nodeCountThreshold);
$html = $root->getChildrenAsUL(
"",
'"<li id=\"" . $child->ID . "\">" . $child->Title',
null,
true, // limit to marked
"AllChildrenIncludingDeleted",
"numChildren",
true,
$nodeCountThreshold
);
$parser = new CSSContentParser($html);
$node1 = $parser->getByXpath(
'//ul/li[@id="' . $obj1->ID . '"]'
);
$this->assertFalse(
(bool)$node1,
'Does not contain root elements which dont match the filter'
);
$node2aa = $parser->getByXpath(
'//ul/li[@id="' . $obj2->ID . '"]' .
'/ul/li[@id="' . $obj2a->ID . '"]' .
'/ul/li[@id="' . $obj2aa->ID . '"]'
);
$this->assertTrue(
(bool)$node2aa,
'Contains non-root elements which match the filter'
);
}
}
class HierarchyTest_Object extends DataObject implements TestOnly {
@ -191,4 +423,4 @@ class HierarchyTest_Object extends DataObject implements TestOnly {
'Hierarchy',
"Versioned('Stage', 'Live')",
);
}
}