Merge remote-tracking branch 'origin/3.1'

This commit is contained in:
Ingo Schommer 2013-10-06 19:07:39 +02:00
commit 60fc7e5346
37 changed files with 762 additions and 118 deletions

View File

@ -1,16 +1,21 @@
<?php
/**
* The object manages the main CMS menu. See {@link LeftAndMain::init()} for example usage.
* The object manages the main CMS menu. See {@link LeftAndMain::init()} for
* example usage.
*
* The menu will be automatically populated with menu items for subclasses of {@link LeftAndMain}.
* That is, for each class in the CMS that creates an administration panel, a CMS menu item will be created.
* The default configuration will also include a 'help' link to the SilverStripe user documentation.
* The menu will be automatically populated with menu items for subclasses of
* {@link LeftAndMain}. That is, for each class in the CMS that creates an
* administration panel, a CMS menu item will be created. The default
* configuration will also include a 'help' link to the SilverStripe user
* documentation.
*
* Additional CMSMenu items can be added through {@link LeftAndMainExtension::init()}
* extensions added to {@link LeftAndMain}.
*
* @package cms
* @subpackage content
* @package framework
* @subpackage admin
*/
class CMSMenu extends Object implements IteratorAggregate, i18nEntityProvider
{
class CMSMenu extends Object implements IteratorAggregate, i18nEntityProvider {
/**
* An array of changes to be made to the menu items, in the order that the changes should be
@ -79,10 +84,12 @@ class CMSMenu extends Object implements IteratorAggregate, i18nEntityProvider
* @param string $url The url of the link
* @param integer $priority The menu priority (sorting order) of the menu item. Higher priorities will be further
* left.
* @param array $attributes an array of attributes to include on the link.
*
* @return boolean The result of the operation.
*/
public static function add_link($code, $menuTitle, $url, $priority = -1) {
return self::add_menu_item($code, $menuTitle, $url, null, $priority);
public static function add_link($code, $menuTitle, $url, $priority = -1, $attributes = null) {
return self::add_menu_item($code, $menuTitle, $url, null, $priority, $attributes);
}
/**
@ -97,13 +104,17 @@ class CMSMenu extends Object implements IteratorAggregate, i18nEntityProvider
* @param string $controllerClass The controller class for this menu, used to check permisssions.
* If blank, it's assumed that this is public, and always shown to users who
* have the rights to access some other part of the admin area.
* @param array $attributes an array of attributes to include on the link.
*
* @return boolean Success
*/
public static function add_menu_item($code, $menuTitle, $url, $controllerClass = null, $priority = -1) {
public static function add_menu_item($code, $menuTitle, $url, $controllerClass = null, $priority = -1, $attributes = null) {
// If a class is defined, then force the use of that as a code. This helps prevent menu item duplication
if($controllerClass) $code = $controllerClass;
if($controllerClass) {
$code = $controllerClass;
}
return self::replace_menu_item($code, $menuTitle, $url, $controllerClass, $priority);
return self::replace_menu_item($code, $menuTitle, $url, $controllerClass, $priority, $attributes);
}
/**
@ -223,18 +234,26 @@ class CMSMenu extends Object implements IteratorAggregate, i18nEntityProvider
* @param string $controllerClass The controller class for this menu, used to check permisssions.
* If blank, it's assumed that this is public, and always shown to users who
* have the rights to access some other part of the admin area.
* @param array $attributes an array of attributes to include on the link.
*
* @return boolean Success
*/
public static function replace_menu_item($code, $menuTitle, $url, $controllerClass = null, $priority = -1) {
public static function replace_menu_item($code, $menuTitle, $url, $controllerClass = null, $priority = -1, $attributes = null) {
$item = new CMSMenuItem($menuTitle, $url, $controllerClass, $priority);
if($attributes) {
$item->setAttributes($attributes);
}
self::$menu_item_changes[] = array(
'type' => 'add',
'code' => $code,
'item' => new CMSMenuItem($menuTitle, $url, $controllerClass, $priority),
'item' => $item,
);
}
/**
* Add a previously built menuitem object to the menu
* Add a previously built menu item object to the menu
*/
protected static function add_menu_item_obj($code, $cmsMenuItem) {
self::$menu_item_changes[] = array(

View File

@ -1,12 +1,18 @@
<?php
/**
* A simple CMS menu item
* A simple CMS menu item.
*
* Items can be added to the menu through custom {@link LeftAndMainExtension}
* classes and {@link CMSMenu}.
*
* @package cms
* @subpackage content
* @see CMSMenu
*
* @package framework
* @subpackage admin
*/
class CMSMenuItem extends Object
{
class CMSMenuItem extends Object {
/**
* The (translated) menu title
* @var string $title
@ -31,8 +37,17 @@ class CMSMenuItem extends Object
*/
public $priority;
/**
* Attributes for the link. For instance, custom data attributes or standard
* HTML anchor properties.
*
* @var string
*/
protected $attributes = array();
/**
* Create a new CMS Menu Item
*
* @param string $title
* @param string $url
* @param string $controller Controller class name
@ -43,7 +58,41 @@ class CMSMenuItem extends Object
$this->url = $url;
$this->controller = $controller;
$this->priority = $priority;
parent::__construct();
}
/**
* @param array $attributes
*/
public function setAttributes($attributes) {
$this->attributes = $attributes;
}
/**
* @param array
*
* @return HTML
*/
public function getAttributesHTML($attrs = null) {
$exclude = (is_string($attrs)) ? func_get_args() : null;
if(!$attrs || is_string($attrs)) {
$attrs = $this->attributes;
}
// Remove empty
$attrs = array_filter((array)$attrs, function($v) {
return ($v || $v === 0 || $v === '0');
});
// Create markkup
$parts = array();
foreach($attrs as $name => $value) {
$parts[] = ($value === true) ? "{$name}=\"{$name}\"" : "{$name}=\"" . Convert::raw2att($value) . "\"";
}
return implode(' ', $parts);
}
}

View File

@ -219,7 +219,10 @@ class LeftAndMain extends Controller implements PermissionProvider {
'Help',
_t('LeftAndMain.HELP', 'Help', 'Menu title'),
$this->config()->help_link,
-2
-2,
array(
'target' => '_blank'
)
);
// Allow customisation of the access check by a extension
@ -616,6 +619,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
if($menuItems) {
foreach($menuItems as $code => $menuItem) {
// alternate permission checks (in addition to LeftAndMain->canView())
if(
isset($menuItem->controller)
&& $this->hasMethod('alternateMenuDisplayCheck')
@ -661,9 +665,10 @@ class LeftAndMain extends Controller implements PermissionProvider {
$menuIcon = LeftAndMain::menu_icon_for_class($menuItem->controller);
if (!empty($menuIcon)) $menuIconStyling .= $menuIcon;
}
$menu->push(new ArrayData(array(
"MenuItem" => $menuItem,
"AttributesHTML" => $menuItem->getAttributesHTML(),
"Title" => Convert::raw2xml($title),
"Code" => DBField::create_field('Text', $code),
"Link" => $menuItem->url,

View File

@ -747,6 +747,7 @@ form.import-form label.left { width: 250px; }
.cms .jstree li.jstree-open > ul, .TreeDropdownField .treedropdownfield-panel .jstree li.jstree-open > ul { display: block; }
.cms .jstree li.jstree-closed > ul, .TreeDropdownField .treedropdownfield-panel .jstree li.jstree-closed > ul { display: none; }
.cms .jstree li.disabled > a, .TreeDropdownField .treedropdownfield-panel .jstree li.disabled > a { color: #aaaaaa; }
.cms .jstree li.disabled > a:hover, .TreeDropdownField .treedropdownfield-panel .jstree li.disabled > a:hover { background: transparent; cursor: default; }
.cms .jstree li.edit-disabled > a, .TreeDropdownField .treedropdownfield-panel .jstree li.edit-disabled > a { color: #aaaaaa; }
.cms .jstree li > .jstree-icon, .TreeDropdownField .treedropdownfield-panel .jstree li > .jstree-icon { cursor: pointer; }
.cms .jstree ins, .TreeDropdownField .treedropdownfield-panel .jstree ins { display: inline-block; text-decoration: none; width: 18px; height: 18px; margin: 0 0 0 0; padding: 0; float: left; }
@ -817,6 +818,7 @@ form.import-form label.left { width: 250px; }
.tree-holder.jstree-apple li.Root > a .jstree-icon, .cms-tree.jstree-apple li.Root > a .jstree-icon { background-position: -56px -36px; }
.tree-holder.jstree-apple li.status-deletedonlive .text, .cms-tree.jstree-apple li.status-deletedonlive .text { text-decoration: line-through; }
.tree-holder.jstree-apple li.jstree-checked > a, .tree-holder.jstree-apple li.jstree-checked > a:link, .cms-tree.jstree-apple li.jstree-checked > a, .cms-tree.jstree-apple li.jstree-checked > a:link { background-color: #efe999; }
.tree-holder.jstree-apple li.disabled > a > .jstree-checkbox, .cms-tree.jstree-apple li.disabled > a > .jstree-checkbox { background-position: -57px -54px; }
.tree-holder.jstree-apple li.readonly, .cms-tree.jstree-apple li.readonly { color: #aaaaaa; padding-left: 18px; }
.tree-holder.jstree-apple li.readonly a, .tree-holder.jstree-apple li.readonly a:link, .cms-tree.jstree-apple li.readonly a, .cms-tree.jstree-apple li.readonly a:link { margin: 0; padding: 0; }
.tree-holder.jstree-apple li.readonly .jstree-icon, .cms-tree.jstree-apple li.readonly .jstree-icon { display: none; }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -230,6 +230,13 @@
// Ignore external links, fallback to standard link behaviour
var isExternal = $.path.isExternal(this.attr('href'));
if(e.which > 1 || isExternal) return;
// if the developer has this to open in a new window, handle
// that
if(this.attr('target') == "_blank") {
return;
}
e.preventDefault();
var item = this.getMenuItem();

View File

@ -32,6 +32,10 @@
}
&.disabled > a {
color: #aaaaaa;
&:hover {
background: transparent;
cursor: default;
}
}
&.edit-disabled > a {
color: #aaaaaa;
@ -438,7 +442,12 @@
> a, > a:link{
background-color: $color-cms-batchactions-menu-selected-background;
}
}
}
&.disabled {
> a > .jstree-checkbox {
background-position: -57px -54px;
}
}
&.readonly {
color: $color-text-disabled;
padding-left: 18px;

View File

@ -24,7 +24,7 @@
<ul class="cms-menu-list">
<% loop $MainMenu %>
<li class="$LinkingMode $FirstLast <% if $LinkingMode == 'link' %><% else %>opened<% end_if %>" id="Menu-$Code" title="$Title.ATT">
<a href="$Link" <% if $Code == 'Help' %>target="_blank"<% end_if%>>
<a href="$Link" $AttributesHTML>
<span class="icon icon-16 icon-{$Code.LowerCase}">&nbsp;</span>
<span class="text">$Title</span>
</a>

View File

@ -1,6 +1,6 @@
<?php
/**
* @package cms
* @package framework
* @subpackage tests
*/
class CMSMenuTest extends SapphireTest implements TestOnly {
@ -35,7 +35,21 @@ class CMSMenuTest extends SapphireTest implements TestOnly {
$this->assertNull($menuItem->controller, 'Link menu item has no controller class');
$this->assertEquals($menuItem->priority, -1, 'Link menu item has the correct priority');
CMSMenu::clear_menu();
}
public function testLinkWithExternalAttributes() {
CMSMenu::clear_menu();
CMSMenu::add_link('LinkCode', 'link title', 'http://www.example.com', -2, array(
'target' => '_blank'
));
$menuItems = CMSMenu::get_menu_items();
$menuItem = $menuItems['LinkCode'];
$this->assertEquals('target="_blank"', $menuItem->getAttributesHTML());
CMSMenu::clear_menu();
}
public function testCmsClassDetection() {
@ -81,8 +95,15 @@ class CMSMenuTest extends SapphireTest implements TestOnly {
}
/**
* @package framework
* @subpackage tests
*/
class CMSMenuTest_LeftAndMainController extends LeftAndMain implements TestOnly {
private static $url_segment = 'CMSMenuTest_LeftAndMainController';
private static $menu_title = 'CMSMenuTest_LeftAndMainController';
private static $menu_priority = 50;
}

View File

@ -184,6 +184,7 @@ class ContentNegotiator extends Object {
$content = str_replace('<hr>','<hr />', $content);
$content = preg_replace('#(<img[^>]*[^/>])>#i', '\\1/>', $content);
$content = preg_replace('#(<input[^>]*[^/>])>#i', '\\1/>', $content);
$content = preg_replace('#(<param[^>]*[^/>])>#i', '\\1/>', $content);
$content = preg_replace("#(\<option[^>]*[\s]+selected)(?!\s*\=)#si", "$1=\"selected\"$2", $content);
$content = preg_replace("#(\<input[^>]*[\s]+checked)(?!\s*\=)#si", "$1=\"checked\"$2", $content);

View File

@ -14,4 +14,13 @@ See [announcement](http://www.silverstripe.org/ss-2013-009-xss-in-cms-pages-sect
Due to cross-site scripting concerns when user data is used for form messages,
it is no longer possible to use HTML in `Form->sessionMessage()`, and consequently
in the `FormField->validate()` API.
in the `FormField->validate()` API.
## Changelog
### Bugfixes
* 2013-09-24 [114fb59](https://github.com/silverstripe/sapphire/commit/114fb59) Auto-escape titles in TreeDropdownField (Ingo Schommer)
* 2013-09-24 [e170f4c](https://github.com/silverstripe/silverstripe-cms/commit/e170f4c) Escaping in "dependent pages" (SS-2013-009) (Ingo Schommer)
* 2013-09-20 [b383a07](https://github.com/silverstripe/sapphire/commit/b383a07) Fixing tabindex added to CreditCardField when tabindex is NULL (Sean Harvey)
* 2013-09-20 [c453ea3](https://github.com/silverstripe/sapphire/commit/c453ea3) Fixing tabindex added to CreditCardField when tabindex is NULL (Sean Harvey)

View File

@ -1,4 +1,4 @@
# 3.1.0 (unreleased)
# 3.1.0
## Overview ##
@ -400,7 +400,7 @@ the following configuration: `RestfulService::set_default_curl_option(CURLOPT_SS
The `[api:Deprecation]` API generates deprecation notices to help you future-proof your code.
Calls to ceprecated methods will only produce errors if the API was deprecated in the
release equal to or earlier than the "notification version" (currently set to "3.1.0").
If you change the notification version to 3.1.0-dev, then only methods deprecated in older versions
(e.g. 3.0) will trigger notices, and the other methods will silently pass. This can be useful if
you don't yet have time to remove all calls to deprecated methods.
@ -469,8 +469,8 @@ you can enable those warnings and future-proof your code already.
* `DataList#dataQuery` has been changed to return a clone of the query, and so can't be used to modify the
list's query directly. Use `DataList#alterDataQuery` instead to modify dataQuery in a safe manner.
* `ScheduledTask`, `QuarterHourlyTask`, `HourlyTask`, `DailyTask`, `MonthlyTask`, `WeeklyTask` and
`YearlyTask` are deprecated, please extend from `BuildTask` or `CliController`,
and invoke them in self-defined frequencies through Unix cronjobs etc.
`YearlyTask` are deprecated, please extend from `BuildTask` or `CliController`,
and invoke them in self-defined frequencies through Unix cronjobs etc.
* `i18n::$common_locales` and `i18n::$common_languages` are now accessed via the Config API, and contain
associative rather than indexed arrays.
Before: `array('de_DE' => array('German', 'Deutsch'))`,
@ -484,13 +484,9 @@ you can enable those warnings and future-proof your code already.
To remove the hints, use `setDescription(null)` and `setAttribute('placeholder', null)`.
* Changed the way FreeStrings in `SSTemplateParser` are recognized, they will now also break on inequality
operators (`<`, `>`). If you use inequality operators in free strings in comparisions like
`<% if Some<String == Some>Other>String %>...<% end_if %>`
you have to replace them with explicitly markes strings like
`<% if "Some<String" == "Some>Other>String" %>...<% end_if %>`
`<% if "Some<String" == "Some>Other>String" %>...<% end_if %>`.
This change was necessary in order to support inequality operators in comparisons in templates
* Hard limit displayed pages in the CMS tree to `500`, and the number of direct children to `250`,
to avoid excessive resource usage. Configure through `Hierarchy.node_threshold_total` and `
@ -503,3 +499,4 @@ you can enable those warnings and future-proof your code already.
* Forms created in the CMS should now be instances of a new `CMSForm` class,
and have the CMS controller's response negotiator passed into them.
Example: `$form = new CMSForm(...); $form->setResponseNegotiator($this->getResponseNegotiator());`

View File

@ -9,9 +9,9 @@ For information on how to upgrade to newer versions consult the [upgrading](/ins
## Stable Releases
* [3.1.0](3.1.0) - Unreleased
* [3.1.0](3.1.0) - 1 October 2013
* [3.0.5](3.0.5) - 2013-02-20
* [3.0.5](3.0.5) - 20 February 2013
* [3.0.4](3.0.4) - 19 February 2013
* [3.0.3](3.0.3) - 26 November 2012
* [3.0.2](3.0.2) - 17 September 2012

View File

@ -1,11 +1,10 @@
# 3.1.0-rc3
# Overview
## Overview
### Security: XSS in CMS "Security" section (SS-2013-007)
See [announcement](http://www.silverstripe.org/ss-2013-007-xss-in-cms-security-section/)
### Security: XSS in form validation errors (SS-2013-008)
See [announcement](http://www.silverstripe.org/ss-2013-008-xss-in-numericfield-validation/)

View File

@ -1,16 +1,24 @@
# How to customize the CMS Menu #
# How to customize the CMS Menu
## Defining a Custom Icon ##
## Adding a administration panel
Every time you add a new extension of the `api:LeftAndMain` class to the CMS, SilverStripe will automatically create a new menu-item for it, with a default title and icon.
We can easily change that behaviour by using the static `$menu_title` and `$menu_icon` statics to
Every time you add a new extension of the `[api:LeftAndMain]` class to the CMS,
SilverStripe will automatically create a new `[api:CMSMenuItem]` for it
The most popular extension of LeftAndMain is a `[api:ModelAdmin]` class, so
for a more detailed introduction to creating new `ModelAdmin` interfaces, read
the [ModelAdmin referencee](../reference/modeladmin).
In this document we'll take the `ProductAdmin` class used in the
[ModelAdmin referencee](../reference/modeladmin#setup) and so how we can change
the menu behaviour by using the static `$menu_title` and `$menu_icon` statics to
provide a custom title and icon.
The most popular extension of LeftAndMain is the `api:ModelAdmin` class, so we'll use that for an example.
We'll take the `ProductAdmin` class used in the [ModelAdmin reference](../reference/modeladmin#setup).
### Defining a Custom Icon
First we'll need a custom icon. For this purpose SilverStripe uses 16x16 black-and-transparent PNG graphics.
In this case we'll place the icon in `mysite/images`, but you are free to use any location.
First we'll need a custom icon. For this purpose SilverStripe uses 16x16
black-and-transparent PNG graphics. In this case we'll place the icon in
`mysite/images`, but you are free to use any location.
:::php
class ProductAdmin extends ModelAdmin {
@ -18,11 +26,11 @@ In this case we'll place the icon in `mysite/images`, but you are free to use an
private static $menu_icon = 'mysite/images/product-icon.png';
}
## Defining a Custom Title ##
### Defining a Custom Title
The title of menu entries is configured through the `$menu_title` static.
If its not defined, the CMS falls back to using the class name of the controller,
removing the "Admin" bit at the end.
If its not defined, the CMS falls back to using the class name of the
controller, removing the "Admin" bit at the end.
:::php
class ProductAdmin extends ModelAdmin {
@ -30,9 +38,58 @@ removing the "Admin" bit at the end.
private static $menu_title = 'My Custom Admin';
}
In order to localize the menu title in different languages, use the `<classname>.MENUTITLE`
entity name, which is automatically created when running the i18n text collection.
For more information on language and translations, please refer to the [i18n](../reference/ii8n) docs.
In order to localize the menu title in different languages, use the
`<classname>.MENUTITLE` entity name, which is automatically created when running
the i18n text collection.
For more information on language and translations, please refer to the
[i18n](../reference/ii8n) docs.
## Adding an external link to the menu
On top of your administration windows, the menu can also have external links
(e.g to external reference). In this example, we're going to add a link to
Google to the menu.
First, we need to define a `[api:LeftAndMainExtension]` which will contain our
button configuration.
:::php
<?php
class CustomLeftAndMain extends LeftAndMainExtension {
public function init() {
// unique identifier for this item. Will have an ID of Menu-$ID
$id = 'LinkToGoogle';
// your 'nice' title
$title = 'Google';
// the link you want to item to go to
$link = 'http://google.com';
// priority controls the ordering of the link in the stack. The
// lower the number, the lower in the list
$priority = -2;
// Add your own attributes onto the link. In our case, we want to
// open the link in a new window (not the original)
$attributes = array(
'target' => '_blank'
);
CMSMenu::add_link($id, $title, $link, $priority, $attributes);
}
}
To have the link appear, make sure you add the extension to the `LeftAndMain`
class. For more information about configuring extensions see the
[DataExtension referencee](../reference/dataextension).
:::php
LeftAndMain::add_extension('CustomLeftAndMain')
## Related

View File

@ -260,6 +260,14 @@ This is not the only way to set things up in Composer. For more information on t
## FAQ
### Error "The requested package silverstripe/framework 1.0.0 could not be found"
Composer needs hints about the base package version, either by using `composer create-project`
as described above, or by checking out the `silverstripe-installer` project directly from version control.
In order to use Composer on archive downloads from silverstripe.org, or other unversioned sources,
an advanced workaround is to set the `COMPOSER_ROOT_VERSION` before every command
([details](http://getcomposer.org/doc/03-cli.md#composer-root-version))
### How do I convert an existing module to using Composer?
Simply decide on a [unique name and vendor prefix](https://packagist.org/about),

View File

@ -16,16 +16,16 @@ class CreditCardField extends TextField {
$parts = array_pad($parts, 4, "");
// TODO Mark as disabled/readonly
$field = "<span id=\"{$this->name}_Holder\" class=\"creditCardField\">"
. "<input autocomplete=\"off\" name=\"{$this->name}[0]\" value=\"$parts[0]\" maxlength=\"4\""
. $this->getTabIndexHTML(0) . " /> - "
. "<input autocomplete=\"off\" name=\"{$this->name}[1]\" value=\"$parts[1]\" maxlength=\"4\""
. $this->getTabIndexHTML(1) . " /> - "
. "<input autocomplete=\"off\" name=\"{$this->name}[2]\" value=\"$parts[2]\" maxlength=\"4\""
. $this->getTabIndexHTML(2) . " /> - "
. "<input autocomplete=\"off\" name=\"{$this->name}[3]\" value=\"$parts[3]\" maxlength=\"4\""
. $this->getTabIndexHTML(3) . " /></span>";
return $field;
$properties['ValueOne'] = $parts[0];
$properties['ValueTwo'] = $parts[1];
$properties['ValueThree'] = $parts[2];
$properties['ValueFour'] = $parts[3];
$properties['TabIndexOne'] = $this->getTabIndexHTML(0);
$properties['TabIndexTwo'] = $this->getTabIndexHTML(1);
$properties['TabIndexThree'] = $this->getTabIndexHTML(2);
$properties['TabIndexFour'] = $this->getTabIndexHTML(3);
return parent::Field($properties);
}
/**

View File

@ -19,16 +19,23 @@ class RequiredFields extends Validator {
* to the constructor of this object. (an array of elements are ok)
*/
public function __construct() {
$Required = func_get_args();
if( isset($Required[0]) && is_array( $Required[0] ) )
$Required = $Required[0];
$this->required = $Required;
$required = func_get_args();
if(isset($required[0]) && is_array($required[0])) {
$required = $required[0];
}
if(!empty($required)) {
$this->required = ArrayLib::valuekey($required);
} else {
$this->required = array();
}
parent::__construct();
}
public function useLabels($flag) {
Deprecation::notice('3.2', 'useLabels will be removed from 3.2, please do not use it or implement it yourself');
$this->useLabels = $flag;
return $this;
}
/**
@ -36,6 +43,7 @@ class RequiredFields extends Validator {
*/
public function removeValidation(){
$this->required = array();
return $this;
}
/**
@ -74,7 +82,7 @@ class RequiredFields extends Validator {
// submitted data for file upload fields come back as an array
$value = isset($data[$fieldName]) ? $data[$fieldName] : null;
if(is_array($value)) {
if ($formField instanceof FileField && isset($value['error']) && $value['error']) {
if($formField instanceof FileField && isset($value['error']) && $value['error']) {
$error = true;
} else {
$error = (count($value)) ? false : true;
@ -115,22 +123,21 @@ class RequiredFields extends Validator {
* Add's a single required field to requiredfields stack
*/
public function addRequiredField( $field ) {
$this->required[] = $field;
$this->required[$field] = $field;
return $this;
}
public function removeRequiredField($field) {
foreach ($this->required as $i => $required) {
if ($field == $required) {
array_splice($this->required, $i);
}
}
unset($this->required[$field]);
return $this;
}
/**
* allows you too add more required fields to this object after construction.
*/
public function appendRequiredFields($requiredFields){
$this->required = array_merge($this->required,$requiredFields->getRequired());
$this->required = $this->required + ArrayLib::valuekey($requiredFields->getRequired());
return $this;
}
/**
@ -138,14 +145,14 @@ class RequiredFields extends Validator {
* Used by FormField to return a value for FormField::Required(), to do things like show *s on the form template.
*/
public function fieldIsRequired($fieldName) {
return in_array($fieldName, $this->required);
return isset($this->required[$fieldName]);
}
/**
* getter function for append
*/
public function getRequired(){
return $this->required;
return array_values($this->required);
}
}

View File

@ -53,7 +53,8 @@ class TreeDropdownField extends FormField {
/**
* @ignore
*/
protected $sourceObject, $keyField, $labelField, $filterCallback, $searchCallback, $baseID = 0;
protected $sourceObject, $keyField, $labelField, $filterCallback,
$disableCallback, $searchCallback, $baseID = 0;
/**
* @var string default child method in Hierarcy->getChildrenAsUL
*/
@ -122,6 +123,20 @@ class TreeDropdownField extends FormField {
$this->filterCallback = $callback;
return $this;
}
/**
* Set a callback used to disable checkboxes for some items in the tree
*
* @param callback $callback
*/
public function setDisableFunction($callback) {
if(!is_callable($callback, true)) {
throw new InvalidArgumentException('TreeDropdownField->setDisableFunction(): not passed a valid callback');
}
$this->disableCallback = $callback;
return $this;
}
/**
* Set a callback used to search the hierarchy globally, even before
@ -181,11 +196,11 @@ class TreeDropdownField extends FormField {
if($record) {
$title = $record->{$this->labelField};
} else {
if($this->showSearch){
if($this->showSearch) {
$title = _t('DropdownField.CHOOSESEARCH', '(Choose or Search)', 'start value of a dropdown');
}else{
$title = _t('DropdownField.CHOOSE', '(Choose)', 'start value of a dropdown');
}
} else {
$title = _t('DropdownField.CHOOSE', '(Choose)', 'start value of a dropdown');
}
}
// TODO Implement for TreeMultiSelectField
@ -276,12 +291,13 @@ class TreeDropdownField extends FormField {
$keyField = $self->keyField;
$labelField = $self->labelField;
return sprintf(
'<li id="selector-%s-%s" data-id="%s" class="class-%s %s"><a rel="%d">%s</a>',
'<li id="selector-%s-%s" data-id="%s" class="class-%s %s %s"><a rel="%d">%s</a>',
Convert::raw2xml($self->getName()),
Convert::raw2xml($child->$keyField),
Convert::raw2xml($child->$keyField),
Convert::raw2xml($child->class),
Convert::raw2xml($child->markingClasses()),
($self->nodeIsDisabled($child)) ? 'disabled' : '',
(int)$child->ID,
$escapeLabelField ? Convert::raw2xml($child->$labelField) : $child->$labelField
);
@ -351,6 +367,15 @@ class TreeDropdownField extends FormField {
return true;
}
/**
* Marking a specific node in the tree as disabled
* @param $node
* @return boolean
*/
public function nodeIsDisabled($node) {
return ($this->disableCallback && call_user_func($this->disableCallback, $node));
}
/**
* @param String $field
*/
@ -409,7 +434,7 @@ class TreeDropdownField extends FormField {
$sourceObject = $this->sourceObject;
$wheres = array();
if(singleton($sourceObject)->hasDatabaseField($this->labelField)) {
$wheres[] = "\"$searchField\" LIKE '%$this->search%'";
$wheres[] = "\"$this->labelField\" LIKE '%$this->search%'";
} else {
if(singleton($sourceObject)->hasDatabaseField('Title')) {
$wheres[] = "\"Title\" LIKE '%$this->search%'";

View File

@ -119,7 +119,7 @@ class TreeMultiselectField extends TreeDropdownField {
if ($this->form){
$dataUrlTree = $this->Link('tree');
if (isset($idArray) && count($idArray)){
$dataUrlTree .= '?forceValue='.implode(',',$idArray);
$dataUrlTree = Controller::join_links($dataUrlTree, '?forceValue='.implode(',',$idArray));
}
}
return FormField::create_tag(

View File

@ -107,8 +107,7 @@ class GridFieldAddExistingAutocompleter
$searchField = new TextField('gridfield_relationsearch',
_t('GridField.RelationSearch', "Relation search"), $value);
// Apparently the data-* needs to be double qouted for the jQuery.meta data plugin
$searchField->setAttribute('data-search-url', '\''.Controller::join_links($gridField->Link('search').'\''));
$searchField->setAttribute('data-search-url', Controller::join_links($gridField->Link('search')));
$searchField->setAttribute('placeholder', $this->getPlaceholderText($dataClass));
$searchField->addExtraClass('relation-search no-change-track');

View File

@ -508,9 +508,11 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
// TODO Save this item into the given relationship
$link = '<a href="' . $this->Link('edit') . '">"'
. htmlspecialchars($this->record->Title, ENT_QUOTES)
. '"</a>';
// TODO Allow HTML in form messages
// $link = '<a href="' . $this->Link('edit') . '">"'
// . htmlspecialchars($this->record->Title, ENT_QUOTES)
// . '"</a>';
$link = '"' . $this->record->Title . '"';
$message = _t(
'GridFieldDetailForm.Saved',
'Saved {name} {link}',

View File

@ -316,14 +316,12 @@
source: function(request, response){
var searchField = $(this.element);
var form = $(this.element).closest("form");
// Due to some very weird behaviout of jquery.metadata, the url have to be double quoted
var suggestionUrl = $(searchField).attr('data-search-url').substr(1,$(searchField).attr('data-search-url').length-2);
$.ajax({
headers: {
"X-Pjax" : 'Partial'
},
type: "GET",
url: suggestionUrl,
url: $(searchField).data('searchUrl'),
data: encodeURIComponent(searchField.attr('name'))+'='+encodeURIComponent(searchField.val()),
success: function(data) {
response( $.map(JSON.parse(data), function( name, id ) {

View File

@ -228,7 +228,25 @@
'themes': {
'theme': 'apple'
},
'plugins': ['html_data', 'ui', 'themes']
'types' : {
'types' : {
'default': {
'check_node': function(node) {
return ( ! node.hasClass('disabled'));
},
'uncheck_node': function(node) {
return ( ! node.hasClass('disabled'));
},
'select_node': function(node) {
return ( ! node.hasClass('disabled'));
},
'deselect_node': function(node) {
return ( ! node.hasClass('disabled'));
}
}
}
},
'plugins': ['html_data', 'ui', 'themes', 'types']
};
},
/**

View File

@ -13,6 +13,7 @@ it:
SIZE: 'Dimensione'
TITLE: Titolo
TYPE: 'Tipo di file'
URL: URL
AssetUploadField:
ChooseFiles: 'Scegli file'
DRAGFILESHERE: 'Trascina file qui'
@ -20,6 +21,7 @@ it:
EDITALL: 'Modifica tutti'
EDITANDORGANIZE: 'Modifica & organizza'
EDITINFO: 'Modifica file'
FILES: Files
FROMCOMPUTER: 'Scegli file dal tuo computer'
FROMCOMPUTERINFO: 'Carica dal tuo conputer'
TOTAL: Totale
@ -72,7 +74,9 @@ it:
ChangePasswordEmail_ss:
CHANGEPASSWORDTEXT1: 'Hai cambiato la password per'
CHANGEPASSWORDTEXT2: 'Ora puoi utilizzare le seguenti credenziali per accedere:'
EMAIL: Email
HELLO: Ciao
PASSWORD: Password
ComplexTableField:
CLOSEPOPUP: 'Chiudi Finestra'
SUCCESSADD2: 'Aggiunto {name}'
@ -95,12 +99,19 @@ it:
FOURTH: quarto
SECOND: secondo
THIRD: terzo
CurrencyField:
CURRENCYSYMBOL: $
Date:
DAY: giorno
DAYS: giorni
HOUR: ora
HOURS: ora
MIN: minuto
MINS: minuti
MONTH: mese
SECS: sec
MONTHS: mesi
SEC: secondo
SECS: secondi
TIMEDIFFAGO: '{difference} fa'
YEAR: anno
YEARS: anni
@ -145,6 +156,7 @@ it:
PLURALNAME: File
PdfType: 'File Adobe Acrobat PDF'
PngType: 'Immagine PNG - consigliata per utilizzo generico'
SINGULARNAME: File
TOOLARGE: 'La dimensione del file è troppo grande, massimo consentito {size}'
TOOLARGESHORT: 'La dimensione del file eccede {size}'
TiffType: 'Immagine TIFF'
@ -158,6 +170,7 @@ it:
ATTACHONCESAVED2: 'I file possono essere allegati quando hai salvato per la prima volta.'
DELETE: 'Elimina {type}'
DISALLOWEDFILETYPE: 'Non è consentito caricare questo tipo di file'
FILE: File
FROMCOMPUTER: 'Dal tuo computer'
FROMFILESTORE: 'Dal CMS'
NOSOURCE: 'Seleziona un file da allegare'
@ -221,9 +234,12 @@ it:
GridFieldItemEditView_ss:
Go_back: 'Torna indietro'
Group:
AddRole: 'Aggiungi un ruolo per questo gruppo'
Code: 'Codice gruppo'
DefaultGroupTitleAdministrators: Amministratori
DefaultGroupTitleContentAuthors: 'Autori di contenuto'
Description: Descrizione
GroupReminder: 'Se scegli un gruppo genitore, questo gruppo erediterà tutti i suoi ruoli'
Locked: 'Bloccato?'
NoRoles: 'Nessun ruolo trovato'
PLURALNAME: Gruppi
@ -235,6 +251,7 @@ it:
many_many_Members: Membri
GroupImportForm:
Help1: '<p>Importa gruppi in formato <em>CSV</em> (valori separati da virgole). <small><a href="#" class="toggle-advanced">Mostra utilizzo avanzato</a></small></p>'
Help2: "<div class=\"advanced\">\n<h4>Utilizzo avanzato</h4>\n<ul>\n<li>Colonne consentite: <em>%s</em></li>\n<li>Gruppi esistenti sono individuati tramite il loro valore univoco <em>Code</em> e aggiornati con i nuovi valori dal file importato.</li>\n<li>È possibile creare gerarchie tra i gruppi usando la colonna <em>ParentCode</em></li>\n<li>Codici di permessi possono essere assegnati tramite la colonna <em>PermissionCode</em>. Codici di permessi esistenti non verranno cancellati.</li>\n</ul>\n</div>"
ResultCreated: 'Creati {count} gruppi'
ResultDeleted: 'Eliminati %d gruppi'
ResultUpdated: 'Aggiornati %d gruppi'
@ -256,6 +273,7 @@ it:
CSSCLASSRIGHT: 'Sulla destra, con il testo inserito attorno.'
DETAILS: Dettagli
EMAIL: 'Indirizzo email'
FILE: File
FOLDER: Cartella
FROMCMS: 'Dal CMS'
FROMCOMPUTER: 'Dal tuo computer'
@ -281,11 +299,18 @@ it:
LINKOPENNEWWIN: 'Apri il link in una nuova finestra?'
LINKTO: 'Collega a'
PAGE: Pagina
URL: URL
URLNOTANOEMBEDRESOURCE: 'L''URL ''{url}'' non può essere convertito in una risorsa media.'
UpdateMEDIA: 'Aggiorna Media'
BUTTONADDURL: 'Aggiungi url'
Image:
PLURALNAME: Files
SINGULARNAME: File
ImageField:
IMAGE: Immagine
Image_Cached:
PLURALNAME: Files
SINGULARNAME: File
Image_iframe_ss:
TITLE: 'Iframe per l''inserimento dell''immagine'
LeftAndMain:
@ -326,9 +351,11 @@ it:
DATEFORMAT: 'Formato della data'
DefaultAdminFirstname: 'Amministratore Predefinito'
DefaultDateTime: predefinito
EMAIL: Email
EMPTYNEWPASSWORD: 'La nuova password non può essere vuota, riprova'
ENTEREMAIL: 'Inserisci un indirizzo e-mail per ricevere il link di azzeramento della password'
ERRORLOCKEDOUT: 'Il tuo account è stato temporaneamente disabilitato perchè ci sono stati troppi tentativi di accesso errati. Riprova tra 20 minuti.'
ERRORLOCKEDOUT2: 'Il tuo account è stato temporaneamente disabilitato perchè ci sono stati troppi tentativi di accesso errati. Riprova tra {count} minuti.'
ERRORNEWPASSWORD: 'Hai inserito la tua nuova password in modo differente, prova di nuovo'
ERRORPASSWORDNOTMATCH: 'La tua password attuale non corrisponde, per favore prova ancora'
ERRORWRONGCRED: 'E-mail o password non sembrano essere corretti. Per favore, prova di nuovo.'
@ -337,6 +364,7 @@ it:
INVALIDNEWPASSWORD: 'Non possiamo accettare questa password: {password}'
LOGGEDINAS: 'Sei collegato come {name}.'
NEWPASSWORD: 'Nuova password'
PASSWORD: Password
PLURALNAME: Utenti
REMEMBERME: 'Ricordati di me la prossima volta?'
SINGULARNAME: Utente
@ -353,7 +381,11 @@ it:
db_Locale: 'Localizzazione interfaccia'
db_LockedOutUntil: 'Bloccato fino al'
db_NumVisit: 'Numero di visite'
db_Password: Password
db_PasswordExpiry: 'Data di scadenza della password'
NoPassword: 'Manca la password per questo utente.'
MemberAuthenticator:
TITLE: 'E-mail &amp; Password'
MemberDatetimeOptionsetField:
AMORPM: 'AM (Ante meridiem) o PM (Post meridiem)'
Custom: Personalizza
@ -374,8 +406,10 @@ it:
TWODIGITMONTH: 'Mese a due cifre (01=Gennaio, ecc.)'
TWODIGITSECOND: 'Secondi a due cifre (00 a 59)'
TWODIGITYEAR: 'Anno a due cifre'
Toggle: 'Mostra aiuto per la formattazione'
MemberImportForm:
Help1: '<p>Importa utenti in <em>formato CSV</em> (valori separati da virgole). <small><a href="#" class="toggle-advanced">Mostra utilizzo avanzato</a></small></p>'
Help2: "<div class=\"advanced\">\n<h4>Utilizzo avanzato</h4>\n<ul>\n<li>Colonne consentite: <em>%s</em></li>\n<li>Utenti esistenti sono individuati attraverso la proprietà univoca <em>Code</em> e aggiornati con i nuovi valori dal file importato.</li>\n<li>I gruppi possono essere assegnati attraverso la colonna <em>Groups</em>. I gruppi sono identificati attraverso la loro colonna <em>Code</em>, più gruppi devono essere separati da virgola. L'appartenenza esistente a gruppi non viene cancellata.</li>\n</ul>\n</div>"
ResultCreated: 'Creati {count} utenti'
ResultDeleted: 'Eliminati %d utenti'
ResultNone: 'Nessun cambiamento'
@ -435,9 +469,11 @@ it:
SINGULARNAME: Ruolo
Title: Titolo
PermissionRoleCode:
PLURALNAME: 'Codici di Ruolo'
SINGULARNAME: 'Codice Ruolo'
Permissions:
PERMISSIONS_CATEGORY: 'Ruoli e permessi d''accesso'
UserPermissionsIntro: 'Assegnando gruppi a questo utente modificherà i suoi permessi. Vedi la sezione gruppi per dettagli sui permessi dei singoli gruppi.'
PhoneNumberField:
VALIDATION: 'Per favore inserisci un numero di telefono valido'
RelationComplexTableField_ss:
@ -446,6 +482,7 @@ it:
NOTFOUND: 'Nessun elemento trovato'
Security:
ALREADYLOGGEDIN: 'Non hai accesso a questa pagina. Se hai un altro account che può accederci, puoi autenticarti qui sotto.'
LOSTPASSWORDHEADER: 'Password smarrita'
BUTTONSEND: 'Inviami il link per azzerare la password'
CHANGEPASSWORDBELOW: 'Puoi cambiare la tua password qui sotto.'
CHANGEPASSWORDHEADER: 'Cambia la tua password'
@ -463,10 +500,13 @@ it:
APPLY_ROLES: 'Applica ruoli ai gruppi'
APPLY_ROLES_HELP: 'Abilità di modificare i ruoli assegnati a un gruppo. Richiede il permesso "Accesso alla sezione ''Utenti''".'
EDITPERMISSIONS: 'Modifica i permessi e gli indirizzi IP in ogni gruppo'
EDITPERMISSIONS_HELP: 'Capacità di modificare Permessi e Indirizzi IP per un gruppo. Richiede il permesso "Accesso alla sezione ''Sicurezza''"'
GROUPNAME: 'Nome del gruppo'
IMPORTGROUPS: 'Importa gruppi'
IMPORTUSERS: 'Importa utenti'
MEMBERS: Utenti
MENUTITLE: Sicurezza
MemberListCaution: 'Attenzione: Gli utenti rimossi da questa lista verranno rimossi anche da tutti i gruppi e dal database'
NEWGROUP: 'Nuovo gruppo'
PERMISSIONS: Permessi
ROLES: Ruoli
@ -478,7 +518,13 @@ it:
FileFieldLabel: 'File CSV <small>(Estensioni consentite: *.csv)</small>'
SilverStripeNavigator:
Edit: Modifica
Auto: Auto
ChangeViewMode: 'Cambia visualizzazione'
Desktop: Desktop
EditView: 'Modalità modifica'
Mobile: Mobile
PreviewView: 'Modalità anteprima'
Responsive: Responsivo
Tablet: Preview mode
ViewDeviceWidth: 'Seleziona una larghezza di preview'
Width: larghezza
@ -532,15 +578,28 @@ it:
HOTLINKINFO: 'Info: Questa immagine sarà collegata. Assicurati di avere il permesso di farlo.'
MAXNUMBEROFFILES: 'Numero massimo di {count} file ecceduto.'
MAXNUMBEROFFILESSHORT: 'Puoi caricare solo {count} file'
MAXNUMBEROFFILESONE: 'Puoi caricare solo un file'
REMOVE: Rimuovi
REMOVEERROR: 'Errore eliminando il file'
REMOVEINFO: 'Rimuove il file da qui, ma non lo elimina dal CMS'
STARTALL: 'Avvia tutti'
STARTALLINFO: 'Avvia tutti i caricamenti'
Saved: Salvato
CHOOSEANOTHERFILE: 'Scegli un altro file'
CHOOSEANOTHERINFO: 'Sostituisci questo file con un altro dal CMS'
OVERWRITEWARNING: 'Esiste già un file con lo stesso nome'
UPLOADSINTO: 'salva in /{path}'
Versioned:
has_many_Versions: Versioni
CMSPageHistoryController_versions_ss:
PREVIEW: 'Preview del sito'
GridFieldEditButton_ss:
EDIT: Modifica
ContentController:
NOTLOGGEDIN: 'Non autenticato'
GridFieldItemEditView:
Go_back: 'Torna indietro'
PasswordValidator:
LOWCHARSTRENGTH: 'Perfavore aumenta la sicurezza della password aggiungendo alcuni dei seguenti caratteri: %s'
PREVPASSWORD: 'Hai già usato questa password in passato, per favore scegline una nuova'
TOOSHORT: 'La password è troppo corta, deve essere lunga %s o più caratteri'

View File

@ -1,4 +1,4 @@
ja_JP:
ja:
AssetAdmin:
NEWFOLDER: 新しいフォルダ
AssetTableField:

View File

@ -392,6 +392,7 @@ nl:
db_NumVisit: 'Aantal bezoeken'
db_Password: Wachtwoord
db_PasswordExpiry: 'Wachtwoord Vervaldatum'
NoPassword: 'Er is geen wachtwoord voor deze gebruiker.'
MemberAuthenticator:
TITLE: 'Email &amp; Wachtwoord'
MemberDatetimeOptionsetField:
@ -490,7 +491,8 @@ nl:
NOTFOUND: 'Geen items gevonden.'
Security:
ALREADYLOGGEDIN: 'Je hebt niet de juiste rechten, om deze pagina te kunnen bekijken. Als je een ander account met de juiste rechten hebt, kun je hier <a href="%s">opnieuw inloggen</a>.'
BUTTONSEND: 'Zend mij de link om mijn wachtwoord opnieuw aan te maken'
LOSTPASSWORDHEADER: 'Wachtwoord vergeten'
BUTTONSEND: 'Nieuw wachtwoord aanmaken'
CHANGEPASSWORDBELOW: 'U kunt Uw wachtwoord hier beneden veranderen.'
CHANGEPASSWORDHEADER: 'Verander Uw wachtwoord'
ENTERNEWPASSWORD: 'Voer een nieuw wachtwoord in.'
@ -499,7 +501,7 @@ nl:
LOGIN: 'Meld aan'
NOTEPAGESECURED: 'Deze pagina is beveiligd. Voer Uw gegevens in en U wordt automatisch doorgestuurd.'
NOTERESETLINKINVALID: '<p>De link om uw wachtwoord te kunnen wijzigen is niet meer geldig.</p><p>U kunt het <a href="{link1}">opnieuw proberen</a> of uw wachtwoord aanpassen door <a href="{link2}">in te loggen</a>.</p>'
NOTERESETPASSWORD: 'Voer Uw emailadres in en we zenden U een link waarmee U Uw wachtwoord opnieuw kunt aanmaken'
NOTERESETPASSWORD: 'Voer uw e-mailadres in en we sturen een link waarmee u een nieuw wachtwoord kunt instellen.'
PASSWORDSENTHEADER: 'Wachtwoord herstel link verzonden naar {email}'
PASSWORDSENTTEXT: 'Bedankt! Er is een link verstuurt naar {email} om uw wachtwoord te herstellen.'
SecurityAdmin:
@ -605,3 +607,11 @@ nl:
PREVIEW: 'Website voorbeeld'
GridFieldEditButton_ss:
EDIT: Bewerken
ContentController:
NOTLOGGEDIN: 'Niet ingelogd'
GridFieldItemEditView:
Go_back: 'Ga terug'
PasswordValidator:
LOWCHARSTRENGTH: 'Maak a.u.b. uw wachtwoord sterker door meer van de volgende karakters te gebruiken: %s'
PREVPASSWORD: 'U heeft dit wachtwoord in het verleden al gebruikt, kies a.u.b. een nieuw wachtwoord.'
TOOSHORT: 'Het wachtwoord is te kort, het moet minimaal %s karakters hebben'

View File

@ -256,7 +256,10 @@ class DataQuery {
$baseClass = array_shift($tableClasses);
if($orderby = $query->getOrderBy()) {
$newOrderby = array();
foreach($orderby as $k => $dir) {
$newOrderby[$k] = $dir;
// don't touch functions in the ORDER BY or public function calls
// selected as fields
if(strpos($k, '(') !== false) continue;
@ -283,10 +286,10 @@ class DataQuery {
$qualCol = "\"$parts[0]\"";
}
// remove original sort
unset($orderby[$k]);
// add new columns sort
$orderby[$qualCol] = $dir;
// remove original sort
unset($newOrderby[$k]);
// add new columns sort
$newOrderby[$qualCol] = $dir;
// To-do: Remove this if block once SQLQuery::$select has been refactored to store getSelect()
// format internally; then this check can be part of selectField()
@ -305,7 +308,7 @@ class DataQuery {
}
}
$query->setOrderBy($orderby);
$query->setOrderBy($newOrderby);
}
}

View File

@ -519,7 +519,7 @@ class SQLQuery {
* @example $sql->orderby("Column", "DESC");
* @example $sql->orderby(array("Column" => "ASC", "ColumnTwo" => "DESC"));
*
* @param string|array $orderby Clauses to add (escaped SQL statements)
* @param string|array $clauses Clauses to add (escaped SQL statements)
* @param string $dir Sort direction, ASC or DESC
*
* @return SQLQuery
@ -566,21 +566,23 @@ class SQLQuery {
// directly in the ORDER BY
if($this->orderby) {
$i = 0;
$orderby = array();
foreach($this->orderby as $clause => $dir) {
// public function calls and multi-word columns like "CASE WHEN ..."
if(strpos($clause, '(') !== false || strpos($clause, " ") !== false ) {
// remove the old orderby
unset($this->orderby[$clause]);
// Move the clause to the select fragment, substituting a placeholder column in the sort fragment.
$clause = trim($clause);
$column = "_SortColumn{$i}";
$this->selectField($clause, $column);
$this->addOrderBy('"' . $column . '"', $dir);
$clause = '"' . $column . '"';
$i++;
}
$orderby[$clause] = $dir;
}
$this->orderby = $orderby;
}
return $this;

View File

@ -14,6 +14,15 @@ class MemberLoginForm extends LoginForm {
protected $authenticator_class = 'MemberAuthenticator';
/**
* Since the logout and dologin actions may be conditionally removed, it's necessary to ensure these
* remain valid actions regardless of the member login state.
*
* @var array
* @config
*/
private static $allowed_actions = array('dologin', 'logout');
/**
* Constructor
*

View File

@ -0,0 +1,6 @@
<span id="{$Name}_Holder" class="creditCardField">
<input autocomplete="off" name="{$Name}[0]" value="{$ValueOne}" maxlength="4" $TabIndexOne/> -
<input autocomplete="off" name="{$Name}[1]" value="{$ValueTwo}" maxlength="4" $TabIndexTwo/> -
<input autocomplete="off" name="{$Name}[2]" value="{$ValueThree}" maxlength="4" $TabIndexThree/> -
<input autocomplete="off" name="{$Name}[3]" value="{$ValueFour}" maxlength="4" $TabIndexFour/>
</span>

View File

@ -74,7 +74,10 @@ class CmsUiContext extends BehatContext
protected function getCmsTabsElement()
{
$this->getSession()->wait(5000, "window.jQuery('.cms-content-header-tabs').size() > 0");
$this->getSession()->wait(
5000,
"window.jQuery && window.jQuery('.cms-content-header-tabs').size() > 0"
);
$page = $this->getSession()->getPage();
$cms_content_header_tabs = $page->find('css', '.cms-content-header-tabs');
@ -87,7 +90,7 @@ class CmsUiContext extends BehatContext
{
$this->getSession()->wait(
5000,
"window.jQuery('.cms-content-toolbar').size() > 0 "
"window.jQuery && window.jQuery('.cms-content-toolbar').size() > 0 "
. "&& window.jQuery('.cms-content-toolbar').children().size() > 0"
);
@ -100,7 +103,10 @@ class CmsUiContext extends BehatContext
protected function getCmsTreeElement()
{
$this->getSession()->wait(5000, "window.jQuery('.cms-tree').size() > 0");
$this->getSession()->wait(
5000,
"window.jQuery && window.jQuery('.cms-tree').size() > 0"
);
$page = $this->getSession()->getPage();
$cms_tree_element = $page->find('css', '.cms-tree');
@ -193,7 +199,10 @@ class CmsUiContext extends BehatContext
*/
public function iClickTheCmsTab($tab)
{
$this->getSession()->wait(5000, "window.jQuery('.ui-tabs-nav').size() > 0");
$this->getSession()->wait(
5000,
"window.jQuery && window.jQuery('.ui-tabs-nav').size() > 0"
);
$page = $this->getSession()->getPage();
$tabsets = $page->findAll('css', '.ui-tabs-nav');
@ -407,7 +416,8 @@ class CmsUiContext extends BehatContext
// wait for ajax dropdown to load
$this->getSession()->wait(
5000,
"jQuery('#" . $container->getAttribute('id') . " .treedropdownfield-panel li').length > 0"
"window.jQuery && "
. "window.jQuery('#" . $container->getAttribute('id') . " .treedropdownfield-panel li').length > 0"
);
} else {
// wait for dropdown overlay to appear (might be animated)

View File

@ -27,7 +27,7 @@ class CacheTest extends SapphireTest {
$cache->save('Good', 'cachekey');
$this->assertEquals('Good', $cache->load('cachekey'));
sleep(1);
sleep(2);
$this->assertFalse($cache->load('cachekey'));
}

View File

@ -66,10 +66,11 @@ class LookupFieldTest extends SapphireTest {
$member1 = $this->objFromFixture('Member', 'member1');
$member2 = $this->objFromFixture('Member', 'member2');
$member3 = $this->objFromFixture('Member', 'member3');
$source = DataObject::get('Member');
$f = new LookupField('test', 'test', $source->map());
$f = new LookupField('test', 'test', $source->map('ID', 'FirstName'));
$f->setValue(array($member1->ID, $member2->ID));
$this->assertEquals(
sprintf(
'<span class="readonly" id="test">member1, member2</span>'

View File

@ -0,0 +1,278 @@
<?php
/**
* @package framework
* @subpackage tests
*
* @todo Test the validation method php()
*/
class RequiredFieldsTest extends SapphireTest {
public function testConstructingWithArray() {
//can we construct with an array?
$fields = array(
'Title',
'Content',
'Image',
'AnotherField'
);
$requiredFields = new RequiredFields($fields);
//check the fields and the array match
$this->assertEquals(
$fields,
$requiredFields->getRequired(),
"Failed to set the required fields using an array"
);
}
public function testConstructingWithArguments() {
//can we construct with arguments?
$requiredFields = new RequiredFields(
'Title',
'Content',
'Image',
'AnotherField'
);
//check the fields match
$this->assertEquals(
array(
'Title',
'Content',
'Image',
'AnotherField'
),
$requiredFields->getRequired(),
"Failed to set the required fields using arguments"
);
}
public function testRemoveValidation() {
//can we remove all fields at once?
$requiredFields = new RequiredFields(
'Title',
'Content',
'Image',
'AnotherField'
);
$requiredFields->removeValidation();
//check there are no required fields
$this->assertEmpty(
$requiredFields->getRequired(),
"Failed to remove all the required fields using 'removeValidation()'"
);
}
public function testRemoveRequiredField() {
//set up the required fields
$requiredFields = new RequiredFields(
'Title',
'Content',
'Image',
'AnotherField'
);
//remove one
$requiredFields->removeRequiredField('Content');
//compare the arrays
$this->assertEquals(
array(
'Title',
'Image',
'AnotherField'
),
$requiredFields->getRequired(),
"Failed to remove the 'Content' field from required list"
);
//let's remove another
$requiredFields->removeRequiredField('Title');
$this->assertEquals(
array(
'Image',
'AnotherField'
),
$requiredFields->getRequired(),
"Failed to remove 'Title' field from required list"
);
//lets try to remove one that doesn't exist
$requiredFields->removeRequiredField('DontExists');
$this->assertEquals(
array(
'Image',
'AnotherField'
),
$requiredFields->getRequired(),
"Removing a non-existant field from required list altered the list of required fields"
);
}
public function testAddRequiredField() {
//set up the validator
$requiredFields = new RequiredFields(
'Title'
);
//add a field
$requiredFields->addRequiredField('Content');
//check it was added
$this->assertEquals(
array(
'Title',
'Content'
),
$requiredFields->getRequired(),
"Failed to add a new field to the required list"
);
//add another for good measure
$requiredFields->addRequiredField('Image');
//check it was added
$this->assertEquals(
array(
'Title',
'Content',
'Image'
),
$requiredFields->getRequired(),
"Failed to add a second new field to the required list"
);
//remove a field
$requiredFields->removeRequiredField('Title');
//check it was removed
$this->assertEquals(
array(
'Content',
'Image'
),
$requiredFields->getRequired(),
"Failed to remove 'Title' field from required list"
);
//add the same field back to check we can add and remove at will
$requiredFields->addRequiredField('Title');
//check it's in there
$this->assertEquals(
array(
'Content',
'Image',
'Title'
),
$requiredFields->getRequired(),
"Failed to add 'Title' back to the required field list"
);
//add a field that already exists (we can't have the same field twice, can we?)
$requiredFields->addRequiredField('Content');
//check the field wasn't added
$this->assertEquals(
array(
'Content',
'Image',
'Title'
),
$requiredFields->getRequired(),
"Adding a duplicate field to required field list had unexpected behaviour"
);
}
public function testAppendRequiredFields() {
//get the validator
$requiredFields = new RequiredFields(
'Title',
'Content',
'Image',
'AnotherField'
);
//create another validator with other fields
$otherRequiredFields = new RequiredFields(array(
'ExtraField1',
'ExtraField2'
));
//append the new fields
$requiredFields->appendRequiredFields($otherRequiredFields);
//check they were added correctly
$this->assertEquals(
array(
'Title',
'Content',
'Image',
'AnotherField',
'ExtraField1',
'ExtraField2'
),
$requiredFields->getRequired(),
"Merging of required fields failed to behave as expected"
);
// create the standard validator so we can check duplicates are ignored
$otherRequiredFields = new RequiredFields(
'Title',
'Content',
'Image',
'AnotherField'
);
//add the new validator
$requiredFields->appendRequiredFields($otherRequiredFields);
//check nothing was changed
$this->assertEquals(
array(
'Title',
'Content',
'Image',
'AnotherField',
'ExtraField1',
'ExtraField2'
),
$requiredFields->getRequired(),
"Merging of required fields with duplicates failed to behave as expected"
);
//add some new fields and some old ones in a strange order
$otherRequiredFields = new RequiredFields(
'ExtraField3',
'Title',
'ExtraField4',
'Image',
'Content'
);
//add the new validator
$requiredFields->appendRequiredFields($otherRequiredFields);
//check that only the new fields were added
$this->assertEquals(
array(
'Title',
'Content',
'Image',
'AnotherField',
'ExtraField1',
'ExtraField2',
'ExtraField3',
'ExtraField4'
),
$requiredFields->getRequired(),
"Merging of required fields with some duplicates in a muddled order failed to behave as expected"
);
}
public function testFieldIsRequired() {
//get the validator
$requiredFields = new RequiredFields($fieldNames = array(
'Title',
'Content',
'Image',
'AnotherField'
));
foreach($fieldNames as $field) {
$this->assertTrue(
$requiredFields->fieldIsRequired($field),
sprintf("Failed to find '%s' field in required list", $field)
);
}
//add a new field
$requiredFields->addRequiredField('ExtraField1');
//check the new field is required
$this->assertTrue(
$requiredFields->fieldIsRequired('ExtraField1'),
"Failed to find 'ExtraField1' field in required list after adding it to the list"
);
//check a non-existant field returns false
$this->assertFalse(
$requiredFields->fieldIsRequired('DoesntExist'),
"Unexpectedly returned true for a non-existant field"
);
}
}

View File

@ -115,6 +115,15 @@ class DataQueryTest extends SapphireTest {
$this->assertEquals($dq->sql(), $orgDq->sql());
}
public function testOrderByMultiple() {
$dq = new DataQuery('SQLQueryTest_DO');
$dq = $dq->sort('"Name" ASC, MID("Name", 8, 1) DESC');
$this->assertContains(
'ORDER BY "Name" ASC, "_SortColumn0" DESC',
$dq->sql()
);
}
}

View File

@ -479,6 +479,31 @@ class SQLQueryTest extends SapphireTest {
$this->assertEquals('Object 2', $records[0]['Name']);
$this->assertEquals('2012-05-01 09:00:00', $records['0']['_SortColumn0']);
}
/**
* Test that multiple order elements are maintained in the given order
*/
public function testOrderByMultiple() {
if(DB::getConn() instanceof MySQLDatabase) {
$query = new SQLQuery();
$query->setSelect(array('"Name"', '"Meta"'));
$query->setFrom('"SQLQueryTest_DO"');
$query->setOrderBy(array('MID("Name", 8, 1) DESC', '"Name" ASC'));
$records = array();
foreach($query->execute() as $record) {
$records[] = $record;
}
$this->assertCount(2, $records);
$this->assertEquals('Object 2', $records[0]['Name']);
$this->assertEquals('2', $records[0]['_SortColumn0']);
$this->assertEquals('Object 1', $records[1]['Name']);
$this->assertEquals('1', $records[1]['_SortColumn0']);
}
}
/**
* Test passing in a LIMIT with OFFSET clause string.