Merge branch '3.0' into 3.1

This commit is contained in:
Andrew Short 2013-03-19 22:27:09 +11:00
commit b8a51c3792
10 changed files with 312 additions and 38 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

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

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

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

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

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