Merge remote-tracking branch 'origin/3.1' into 3.2

Conflicts:
	dev/SapphireTest.php
	docs/en/02_Developer_Guides/01_Templates/01_Syntax.md
	forms/DatetimeField.php
	forms/NullableField.php
	forms/NumericField.php
	forms/gridfield/GridField.php
	tests/control/DirectorTest.php
	tests/model/DataObjectSchemaGenerationTest.php
	tests/model/MySQLDatabaseTest.php
This commit is contained in:
Damian Mooyman 2015-06-19 10:48:07 +12:00
commit 1d122803cc
24 changed files with 926 additions and 595 deletions

View File

@ -557,11 +557,11 @@ body.cms { overflow: hidden; }
.cms-content-batchactions { float: left; position: relative; display: block; } .cms-content-batchactions { float: left; position: relative; display: block; }
.cms-content-batchactions .view-mode-batchactions-wrapper { height: 18px; float: left; padding: 4px 6px; border: 1px solid #aaa; margin-bottom: 8px; margin-right: -1px; background-color: #D9D9D9; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2Q5ZDlkOSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #d9d9d9)); background-image: -moz-linear-gradient(top, #ffffff, #d9d9d9); background-image: -webkit-linear-gradient(top, #ffffff, #d9d9d9); background-image: linear-gradient(to bottom, #ffffff, #d9d9d9); border-top-left-radius: 4px; border-bottom-left-radius: 4px; } .cms-content-batchactions .view-mode-batchactions-wrapper { height: 18px; float: left; padding: 4px 6px; border: 1px solid #aaa; margin-bottom: 8px; margin-right: -1px; background-color: #D9D9D9; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2Q5ZDlkOSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #d9d9d9)); background-image: -moz-linear-gradient(top, #ffffff, #d9d9d9); background-image: -webkit-linear-gradient(top, #ffffff, #d9d9d9); background-image: linear-gradient(to bottom, #ffffff, #d9d9d9); border-top-left-radius: 4px; border-bottom-left-radius: 4px; }
.cms-content-batchactions .view-mode-batchactions-wrapper input { vertical-align: middle; } .cms-content-batchactions .view-mode-batchactions-wrapper input { vertical-align: middle; }
.cms-content-batchactions .view-mode-batchactions-wrapper label { vertical-align: middle; display: none; } .cms-content-batchactions .view-mode-batchactions-wrapper .view-mode-batchactions-label { vertical-align: middle; display: none; }
.cms-content-batchactions .view-mode-batchactions-wrapper fieldset, .cms-content-batchactions .view-mode-batchactions-wrapper .Actions { display: inline-block; } .cms-content-batchactions .view-mode-batchactions-wrapper fieldset, .cms-content-batchactions .view-mode-batchactions-wrapper .Actions { display: inline-block; }
.cms-content-batchactions .view-mode-batchactions-wrapper #view-mode-batchactions { margin-top: 2px; } .cms-content-batchactions .view-mode-batchactions-wrapper #view-mode-batchactions { margin-top: 2px; }
.cms-content-batchactions.inactive .view-mode-batchactions-wrapper { border-radius: 4px; } .cms-content-batchactions.inactive .view-mode-batchactions-wrapper { border-radius: 4px; }
.cms-content-batchactions.inactive .view-mode-batchactions-wrapper label { display: inline; } .cms-content-batchactions.inactive .view-mode-batchactions-wrapper .view-mode-batchactions-label { display: inline; }
.cms-content-batchactions form > * { display: block; float: left; } .cms-content-batchactions form > * { display: block; float: left; }
.cms-content-batchactions form.cms-batch-actions { float: left; } .cms-content-batchactions form.cms-batch-actions { float: left; }
.cms-content-batchactions.inactive form { display: none; } .cms-content-batchactions.inactive form { display: none; }

View File

@ -907,7 +907,7 @@ body.cms {
vertical-align: middle; vertical-align: middle;
} }
label { .view-mode-batchactions-label {
vertical-align: middle; vertical-align: middle;
display: none; display: none;
} }
@ -923,7 +923,7 @@ body.cms {
&.inactive .view-mode-batchactions-wrapper { &.inactive .view-mode-batchactions-wrapper {
border-radius: 4px; border-radius: 4px;
label { .view-mode-batchactions-label {
display: inline; display: inline;
} }
} }

View File

@ -5,12 +5,12 @@ require_once 'TestRunner.php';
* Test case class for the Sapphire framework. * Test case class for the Sapphire framework.
* Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier * Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier
* to work with. * to work with.
* *
* @package framework * @package framework
* @subpackage testing * @subpackage testing
*/ */
class SapphireTest extends PHPUnit_Framework_TestCase { class SapphireTest extends PHPUnit_Framework_TestCase {
/** @config */ /** @config */
private static $dependencies = array( private static $dependencies = array(
'fixtureFactory' => '%$FixtureFactory', 'fixtureFactory' => '%$FixtureFactory',
@ -21,7 +21,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
* If passed as an array, multiple fixture files will be loaded. * If passed as an array, multiple fixture files will be loaded.
* Please note that you won't be able to refer with "=>" notation * Please note that you won't be able to refer with "=>" notation
* between the fixtures, they act independent of each other. * between the fixtures, they act independent of each other.
* *
* @var string|array * @var string|array
*/ */
protected static $fixture_file = null; protected static $fixture_file = null;
@ -30,19 +30,19 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
* @var FixtureFactory * @var FixtureFactory
*/ */
protected $fixtureFactory; protected $fixtureFactory;
/** /**
* @var bool Set whether to include this test in the TestRunner or to skip this. * @var bool Set whether to include this test in the TestRunner or to skip this.
*/ */
protected $skipTest = false; protected $skipTest = false;
/** /**
* @var Boolean If set to TRUE, this will force a test database to be generated * @var Boolean If set to TRUE, this will force a test database to be generated
* in {@link setUp()}. Note that this flag is overruled by the presence of a * in {@link setUp()}. Note that this flag is overruled by the presence of a
* {@link $fixture_file}, which always forces a database build. * {@link $fixture_file}, which always forces a database build.
*/ */
protected $usesDatabase = null; protected $usesDatabase = null;
protected $originalMailer; protected $originalMailer;
protected $originalMemberPasswordValidator; protected $originalMemberPasswordValidator;
protected $originalRequirements; protected $originalRequirements;
@ -50,33 +50,33 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
protected $originalTheme; protected $originalTheme;
protected $originalNestedURLsState; protected $originalNestedURLsState;
protected $originalMemoryLimit; protected $originalMemoryLimit;
protected $mailer; protected $mailer;
/** /**
* Pointer to the manifest that isn't a test manifest * Pointer to the manifest that isn't a test manifest
*/ */
protected static $regular_manifest; protected static $regular_manifest;
/** /**
* @var boolean * @var boolean
*/ */
protected static $is_running_test = false; protected static $is_running_test = false;
protected static $test_class_manifest; protected static $test_class_manifest;
/** /**
* By default, setUp() does not require default records. Pass * By default, setUp() does not require default records. Pass
* class names in here, and the require/augment default records * class names in here, and the require/augment default records
* function will be called on them. * function will be called on them.
*/ */
protected $requireDefaultRecordsFrom = array(); protected $requireDefaultRecordsFrom = array();
/** /**
* A list of extensions that can't be applied during the execution of this run. If they are * A list of extensions that can't be applied during the execution of this run. If they are
* applied, they will be temporarily removed and a database migration called. * applied, they will be temporarily removed and a database migration called.
* *
* The keys of the are the classes that the extensions can't be applied the extensions to, and * The keys of the are the classes that the extensions can't be applied the extensions to, and
* the values are an array of illegal extensions on that class. * the values are an array of illegal extensions on that class.
*/ */
@ -86,10 +86,10 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
/** /**
* A list of extensions that must be applied during the execution of this run. If they are * A list of extensions that must be applied during the execution of this run. If they are
* not applied, they will be temporarily added and a database migration called. * not applied, they will be temporarily added and a database migration called.
* *
* The keys of the are the classes to apply the extensions to, and the values are an array * The keys of the are the classes to apply the extensions to, and the values are an array
* of required extensions on that class. * of required extensions on that class.
* *
* Example: * Example:
* <code> * <code>
* array("MyTreeDataObject" => array("Versioned", "Hierarchy")) * array("MyTreeDataObject" => array("Versioned", "Hierarchy"))
@ -97,35 +97,35 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
*/ */
protected $requiredExtensions = array( protected $requiredExtensions = array(
); );
/** /**
* By default, the test database won't contain any DataObjects that have the interface TestOnly. * By default, the test database won't contain any DataObjects that have the interface TestOnly.
* This variable lets you define additional TestOnly DataObjects to set up for this test. * This variable lets you define additional TestOnly DataObjects to set up for this test.
* Set it to an array of DataObject subclass names. * Set it to an array of DataObject subclass names.
*/ */
protected $extraDataObjects = array(); protected $extraDataObjects = array();
/** /**
* We need to disabling backing up of globals to avoid overriding * We need to disabling backing up of globals to avoid overriding
* the few globals SilverStripe relies on, like $lang for the i18n subsystem. * the few globals SilverStripe relies on, like $lang for the i18n subsystem.
* *
* @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html * @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html
*/ */
protected $backupGlobals = FALSE; protected $backupGlobals = FALSE;
/** /**
* Helper arrays for illegalExtensions/requiredExtensions code * Helper arrays for illegalExtensions/requiredExtensions code
*/ */
private $extensionsToReapply = array(), $extensionsToRemove = array(); private $extensionsToReapply = array(), $extensionsToRemove = array();
/** /**
* Determines if unit tests are currently run (via {@link TestRunner}). * Determines if unit tests are currently run (via {@link TestRunner}).
* This is used as a cheap replacement for fully mockable state * This is used as a cheap replacement for fully mockable state
* in certain contiditions (e.g. access checks). * in certain contiditions (e.g. access checks).
* Caution: When set to FALSE, certain controllers might bypass * Caution: When set to FALSE, certain controllers might bypass
* access checks, so this is a very security sensitive setting. * access checks, so this is a very security sensitive setting.
* *
* @return boolean * @return boolean
*/ */
public static function is_running_test() { public static function is_running_test() {
@ -133,7 +133,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
} }
public static function set_is_running_test($bool) { public static function set_is_running_test($bool) {
self::$is_running_test = $bool; self::$is_running_test = $bool;
} }
/** /**
@ -157,20 +157,31 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
return static::$fixture_file; return static::$fixture_file;
} }
/**
* @var array $fixtures Array of {@link YamlFixture} instances
* @deprecated 3.1 Use $fixtureFactory instad
*/
protected $fixtures = array();
protected $model; protected $model;
public function setUp() { public function setUp() {
//nest config and injector for each test so they are effectively sandboxed per test
Config::nest();
Injector::nest();
// We cannot run the tests on this abstract class. // We cannot run the tests on this abstract class.
if(get_class($this) == "SapphireTest") $this->skipTest = true; if(get_class($this) == "SapphireTest") $this->skipTest = true;
if($this->skipTest) { if($this->skipTest) {
$this->markTestSkipped(sprintf( $this->markTestSkipped(sprintf(
'Skipping %s ', get_class($this) 'Skipping %s ', get_class($this)
)); ));
return; return;
} }
// Mark test as being run // Mark test as being run
$this->originalIsRunningTest = self::$is_running_test; $this->originalIsRunningTest = self::$is_running_test;
self::$is_running_test = true; self::$is_running_test = true;
@ -179,16 +190,16 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
i18n::set_locale(i18n::default_locale()); i18n::set_locale(i18n::default_locale());
i18n::config()->date_format = null; i18n::config()->date_format = null;
i18n::config()->time_format = null; i18n::config()->time_format = null;
// Set default timezone consistently to avoid NZ-specific dependencies // Set default timezone consistently to avoid NZ-specific dependencies
date_default_timezone_set('UTC'); date_default_timezone_set('UTC');
// Remove password validation // Remove password validation
$this->originalMemberPasswordValidator = Member::password_validator(); $this->originalMemberPasswordValidator = Member::password_validator();
$this->originalRequirements = Requirements::backend(); $this->originalRequirements = Requirements::backend();
Member::set_password_validator(null); Member::set_password_validator(null);
Config::inst()->update('Cookie', 'report_errors', false); Config::inst()->update('Cookie', 'report_errors', false);
if(class_exists('RootURLController')) RootURLController::reset(); if(class_exists('RootURLController')) RootURLController::reset();
if(class_exists('Translatable')) Translatable::reset(); if(class_exists('Translatable')) Translatable::reset();
Versioned::reset(); Versioned::reset();
@ -212,7 +223,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$this->mailer = new TestMailer(); $this->mailer = new TestMailer();
Email::set_mailer($this->mailer); Email::set_mailer($this->mailer);
Config::inst()->remove('Email', 'send_all_emails_to'); Config::inst()->remove('Email', 'send_all_emails_to');
// Todo: this could be a special test model // Todo: this could be a special test model
$this->model = DataModel::inst(); $this->model = DataModel::inst();
@ -227,9 +238,9 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
} }
singleton('DataObject')->flushCache(); singleton('DataObject')->flushCache();
self::empty_temp_db(); self::empty_temp_db();
foreach($this->requireDefaultRecordsFrom as $className) { foreach($this->requireDefaultRecordsFrom as $className) {
$instance = singleton($className); $instance = singleton($className);
if (method_exists($instance, 'requireDefaultRecords')) $instance->requireDefaultRecords(); if (method_exists($instance, 'requireDefaultRecords')) $instance->requireDefaultRecords();
@ -244,14 +255,14 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
foreach($fixtureFiles as $fixtureFilePath) { foreach($fixtureFiles as $fixtureFilePath) {
// Support fixture paths relative to the test class, rather than relative to webroot // Support fixture paths relative to the test class, rather than relative to webroot
// String checking is faster than file_exists() calls. // String checking is faster than file_exists() calls.
$isRelativeToFile = (strpos('/', $fixtureFilePath) === false $isRelativeToFile = (strpos('/', $fixtureFilePath) === false
|| preg_match('/^\.\./', $fixtureFilePath)); || preg_match('/^\.\./', $fixtureFilePath));
if($isRelativeToFile) { if($isRelativeToFile) {
$resolvedPath = realpath($pathForClass . '/' . $fixtureFilePath); $resolvedPath = realpath($pathForClass . '/' . $fixtureFilePath);
if($resolvedPath) $fixtureFilePath = $resolvedPath; if($resolvedPath) $fixtureFilePath = $resolvedPath;
} }
$fixture = Injector::inst()->create('YamlFixture', $fixtureFilePath); $fixture = Injector::inst()->create('YamlFixture', $fixtureFilePath);
$fixture->writeInto($this->getFixtureFactory()); $fixture->writeInto($this->getFixtureFactory());
$this->fixtures[] = $fixture; $this->fixtures[] = $fixture;
@ -261,20 +272,20 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$i++; $i++;
} }
} }
$this->logInWithPermission("ADMIN"); $this->logInWithPermission("ADMIN");
} }
// Preserve memory settings // Preserve memory settings
$this->originalMemoryLimit = ini_get('memory_limit'); $this->originalMemoryLimit = ini_get('memory_limit');
// turn off template debugging // turn off template debugging
Config::inst()->update('SSViewer', 'source_file_comments', false); Config::inst()->update('SSViewer', 'source_file_comments', false);
// Clear requirements // Clear requirements
Requirements::clear(); Requirements::clear();
} }
/** /**
* Called once per test case ({@link SapphireTest} subclass). * Called once per test case ({@link SapphireTest} subclass).
* This is different to {@link setUp()}, which gets called once * This is different to {@link setUp()}, which gets called once
@ -284,6 +295,10 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
* for tearing down the state again. * for tearing down the state again.
*/ */
public function setUpOnce() { public function setUpOnce() {
//nest config and injector for each suite so they are effectively sandboxed
Config::nest();
Injector::nest();
$isAltered = false; $isAltered = false;
if(!Director::isDev()) { if(!Director::isDev()) {
@ -314,46 +329,34 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
} }
} }
} }
// If we have made changes to the extensions present, then migrate the database schema. // If we have made changes to the extensions present, then migrate the database schema.
if($isAltered || $this->extensionsToReapply || $this->extensionsToRemove || $this->extraDataObjects) { if($isAltered || $this->extensionsToReapply || $this->extensionsToRemove || $this->extraDataObjects) {
if(!self::using_temp_db()) self::create_temp_db(); if(!self::using_temp_db()) self::create_temp_db();
$this->resetDBSchema(true); $this->resetDBSchema(true);
} }
// clear singletons, they're caching old extension info // clear singletons, they're caching old extension info
// which is used in DatabaseAdmin->doBuild() // which is used in DatabaseAdmin->doBuild()
Injector::inst()->unregisterAllObjects(); Injector::inst()->unregisterAllObjects();
// Set default timezone consistently to avoid NZ-specific dependencies // Set default timezone consistently to avoid NZ-specific dependencies
date_default_timezone_set('UTC'); date_default_timezone_set('UTC');
} }
/** /**
* tearDown method that's called once per test class rather once per test method. * tearDown method that's called once per test class rather once per test method.
*/ */
public function tearDownOnce() { public function tearDownOnce() {
// If we have made changes to the extensions present, then migrate the database schema. //unnest injector / config now that the test suite is over
if($this->extensionsToReapply || $this->extensionsToRemove) { // this will reset all the extensions on the object too (see setUpOnce)
// Remove extensions added for testing Injector::unnest();
foreach($this->extensionsToRemove as $class => $extensions) { Config::unnest();
foreach($extensions as $extension) {
$class::remove_extension($extension);
}
}
// Reapply ones removed if(!empty($this->extensionsToReapply) || !empty($this->extensionsToRemove) || !empty($this->extraDataObjects)) {
foreach($this->extensionsToReapply as $class => $extensions) {
foreach($extensions as $extension) {
$class::add_extension($extension);
}
}
}
if($this->extensionsToReapply || $this->extensionsToRemove || $this->extraDataObjects) {
$this->resetDBSchema(); $this->resetDBSchema();
} }
} }
/** /**
* @return FixtureFactory * @return FixtureFactory
*/ */
@ -366,10 +369,10 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$this->fixtureFactory = $factory; $this->fixtureFactory = $factory;
return $this; return $this;
} }
/** /**
* Get the ID of an object from the fixture. * Get the ID of an object from the fixture.
* *
* @param $className The data class, as specified in your fixture file. Parent classes won't work * @param $className The data class, as specified in your fixture file. Parent classes won't work
* @param $identifier The identifier string, as provided in your fixture file * @param $identifier The identifier string, as provided in your fixture file
* @return int * @return int
@ -415,12 +418,12 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
"Couldn't find object '%s' (class: %s)", "Couldn't find object '%s' (class: %s)",
$identifier, $identifier,
$className $className
), E_USER_ERROR); ), E_USER_ERROR);
} }
return $obj; return $obj;
} }
/** /**
* Load a YAML fixture file into the database. * Load a YAML fixture file into the database.
* Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture. * Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture.
@ -433,18 +436,18 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$fixture->writeInto($this->getFixtureFactory()); $fixture->writeInto($this->getFixtureFactory());
$this->fixtures[] = $fixture; $this->fixtures[] = $fixture;
} }
/** /**
* Clear all fixtures which were previously loaded through * Clear all fixtures which were previously loaded through
* {@link loadFixture()} * {@link loadFixture()}
*/ */
public function clearFixtures() { public function clearFixtures() {
$this->getFixtureFactory()->clear(); $this->getFixtureFactory()->clear();
} }
/** /**
* Useful for writing unit tests without hardcoding folder structures. * Useful for writing unit tests without hardcoding folder structures.
* *
* @return String Absolute path to current class. * @return String Absolute path to current class.
*/ */
protected function getCurrentAbsolutePath() { protected function getCurrentAbsolutePath() {
@ -452,7 +455,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
if(!$filename) throw new LogicException("getItemPath returned null for " . get_class($this)); if(!$filename) throw new LogicException("getItemPath returned null for " . get_class($this));
return dirname($filename); return dirname($filename);
} }
/** /**
* @return String File path relative to webroot * @return String File path relative to webroot
*/ */
@ -462,7 +465,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
if(substr($path,0,strlen($base)) == $base) $path = preg_replace('/^\/*/', '', substr($path,strlen($base))); if(substr($path,0,strlen($base)) == $base) $path = preg_replace('/^\/*/', '', substr($path,strlen($base)));
return $path; return $path;
} }
public function tearDown() { public function tearDown() {
// Preserve memory settings // Preserve memory settings
ini_set('memory_limit', ($this->originalMemoryLimit) ? $this->originalMemoryLimit : -1); ini_set('memory_limit', ($this->originalMemoryLimit) ? $this->originalMemoryLimit : -1);
@ -472,16 +475,16 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
Email::set_mailer($this->originalMailer); Email::set_mailer($this->originalMailer);
$this->originalMailer = null; $this->originalMailer = null;
} }
$this->mailer = null; $this->mailer = null;
// Restore password validation // Restore password validation
if($this->originalMemberPasswordValidator) { if($this->originalMemberPasswordValidator) {
Member::set_password_validator($this->originalMemberPasswordValidator); Member::set_password_validator($this->originalMemberPasswordValidator);
} }
// Restore requirements // Restore requirements
if($this->originalRequirements) { if($this->originalRequirements) {
Requirements::set_backend($this->originalRequirements); Requirements::set_backend($this->originalRequirements);
} }
// Mark test as no longer being run - we use originalIsRunningTest to allow for nested SapphireTest calls // Mark test as no longer being run - we use originalIsRunningTest to allow for nested SapphireTest calls
@ -490,15 +493,18 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
// Reset mocked datetime // Reset mocked datetime
SS_Datetime::clear_mock_now(); SS_Datetime::clear_mock_now();
// Stop the redirection that might have been requested in the test. // Stop the redirection that might have been requested in the test.
// Note: Ideally a clean Controller should be created for each test. // Note: Ideally a clean Controller should be created for each test.
// Now all tests executed in a batch share the same controller. // Now all tests executed in a batch share the same controller.
$controller = Controller::has_curr() ? Controller::curr() : null; $controller = Controller::has_curr() ? Controller::curr() : null;
if ( $controller && $controller->response && $controller->response->getHeader('Location') ) { if ( $controller && $controller->response && $controller->response->getHeader('Location') ) {
$controller->response->setStatusCode(200); $controller->response->setStatusCode(200);
$controller->response->removeHeader('Location'); $controller->response->removeHeader('Location');
} }
//unnest injector / config now that tests are over
Injector::unnest();
Config::unnest();
} }
public static function assertContains( public static function assertContains(
@ -545,7 +551,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
public function findEmail($to, $from = null, $subject = null, $content = null) { public function findEmail($to, $from = null, $subject = null, $content = null) {
return $this->mailer->findEmail($to, $from, $subject, $content); return $this->mailer->findEmail($to, $from, $subject, $content);
} }
/** /**
* Assert that the matching email was sent since the last call to clearEmails() * Assert that the matching email was sent since the last call to clearEmails()
* All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression. * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
@ -577,7 +583,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
/** /**
* Assert that the given {@link SS_List} includes DataObjects matching the given key-value * Assert that the given {@link SS_List} includes DataObjects matching the given key-value
* pairs. Each match must correspond to 1 distinct record. * pairs. Each match must correspond to 1 distinct record.
* *
* @param $matches The patterns to match. Each pattern is a map of key-value pairs. You can * @param $matches The patterns to match. Each pattern is a map of key-value pairs. You can
* either pass a single pattern or an array of patterns. * either pass a single pattern or an array of patterns.
* @param $dataObjectSet The {@link SS_List} to test. * @param $dataObjectSet The {@link SS_List} to test.
@ -585,19 +591,19 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
* Examples * Examples
* -------- * --------
* Check that $members includes an entry with Email = sam@example.com: * Check that $members includes an entry with Email = sam@example.com:
* $this->assertDOSContains(array('Email' => '...@example.com'), $members); * $this->assertDOSContains(array('Email' => '...@example.com'), $members);
* *
* Check that $members includes entries with Email = sam@example.com and with * Check that $members includes entries with Email = sam@example.com and with
* Email = ingo@example.com: * Email = ingo@example.com:
* $this->assertDOSContains(array( * $this->assertDOSContains(array(
* array('Email' => '...@example.com'), * array('Email' => '...@example.com'),
* array('Email' => 'i...@example.com'), * array('Email' => 'i...@example.com'),
* ), $members); * ), $members);
*/ */
public function assertDOSContains($matches, $dataObjectSet) { public function assertDOSContains($matches, $dataObjectSet) {
$extracted = array(); $extracted = array();
foreach($dataObjectSet as $item) $extracted[] = $item->toMap(); foreach($dataObjectSet as $item) $extracted[] = $item->toMap();
foreach($matches as $match) { foreach($matches as $match) {
$matched = false; $matched = false;
foreach($extracted as $i => $item) { foreach($extracted as $i => $item) {
@ -613,35 +619,35 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$this->assertTrue( $this->assertTrue(
$matched, $matched,
"Failed asserting that the SS_List contains an item matching " "Failed asserting that the SS_List contains an item matching "
. var_export($match, true) . "\n\nIn the following SS_List:\n" . var_export($match, true) . "\n\nIn the following SS_List:\n"
. $this->DOSSummaryForMatch($dataObjectSet, $match) . $this->DOSSummaryForMatch($dataObjectSet, $match)
); );
} }
} }
/** /**
* Assert that the given {@link SS_List} includes only DataObjects matching the given * Assert that the given {@link SS_List} includes only DataObjects matching the given
* key-value pairs. Each match must correspond to 1 distinct record. * key-value pairs. Each match must correspond to 1 distinct record.
* *
* @param $matches The patterns to match. Each pattern is a map of key-value pairs. You can * @param $matches The patterns to match. Each pattern is a map of key-value pairs. You can
* either pass a single pattern or an array of patterns. * either pass a single pattern or an array of patterns.
* @param $dataObjectSet The {@link SS_List} to test. * @param $dataObjectSet The {@link SS_List} to test.
* *
* Example * Example
* -------- * --------
* Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members. Order doesn't * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members. Order doesn't
* matter: * matter:
* $this->assertDOSEquals(array( * $this->assertDOSEquals(array(
* array('FirstName' =>'Sam', 'Surname' => 'Minnee'), * array('FirstName' =>'Sam', 'Surname' => 'Minnee'),
* array('FirstName' => 'Ingo', 'Surname' => 'Schommer'), * array('FirstName' => 'Ingo', 'Surname' => 'Schommer'),
* ), $members); * ), $members);
*/ */
public function assertDOSEquals($matches, $dataObjectSet) { public function assertDOSEquals($matches, $dataObjectSet) {
if(!$dataObjectSet) return false; if(!$dataObjectSet) return false;
$extracted = array(); $extracted = array();
foreach($dataObjectSet as $item) $extracted[] = $item->toMap(); foreach($dataObjectSet as $item) $extracted[] = $item->toMap();
foreach($matches as $match) { foreach($matches as $match) {
$matched = false; $matched = false;
foreach($extracted as $i => $item) { foreach($extracted as $i => $item) {
@ -657,11 +663,11 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$this->assertTrue( $this->assertTrue(
$matched, $matched,
"Failed asserting that the SS_List contains an item matching " "Failed asserting that the SS_List contains an item matching "
. var_export($match, true) . "\n\nIn the following SS_List:\n" . var_export($match, true) . "\n\nIn the following SS_List:\n"
. $this->DOSSummaryForMatch($dataObjectSet, $match) . $this->DOSSummaryForMatch($dataObjectSet, $match)
); );
} }
// If we have leftovers than the DOS has extra data that shouldn't be there // If we have leftovers than the DOS has extra data that shouldn't be there
$this->assertTrue( $this->assertTrue(
(count($extracted) == 0), (count($extracted) == 0),
@ -669,19 +675,19 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
"Failed asserting that the SS_List contained only the given items, the " "Failed asserting that the SS_List contained only the given items, the "
. "following items were left over:\n" . var_export($extracted, true) . "following items were left over:\n" . var_export($extracted, true)
); );
} }
/** /**
* Assert that the every record in the given {@link SS_List} matches the given key-value * Assert that the every record in the given {@link SS_List} matches the given key-value
* pairs. * pairs.
* *
* @param $match The pattern to match. The pattern is a map of key-value pairs. * @param $match The pattern to match. The pattern is a map of key-value pairs.
* @param $dataObjectSet The {@link SS_List} to test. * @param $dataObjectSet The {@link SS_List} to test.
* *
* Example * Example
* -------- * --------
* Check that every entry in $members has a Status of 'Active': * Check that every entry in $members has a Status of 'Active':
* $this->assertDOSAllMatch(array('Status' => 'Active'), $members); * $this->assertDOSAllMatch(array('Status' => 'Active'), $members);
*/ */
public function assertDOSAllMatch($match, $dataObjectSet) { public function assertDOSAllMatch($match, $dataObjectSet) {
$extracted = array(); $extracted = array();
@ -690,7 +696,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
foreach($extracted as $i => $item) { foreach($extracted as $i => $item) {
$this->assertTrue( $this->assertTrue(
$this->dataObjectArrayMatch($item, $match), $this->dataObjectArrayMatch($item, $match),
"Failed asserting that the the following item matched " "Failed asserting that the the following item matched "
. var_export($match, true) . ": " . var_export($item, true) . var_export($match, true) . ": " . var_export($item, true)
); );
} }
@ -763,7 +769,6 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$this->assertNotContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity); $this->assertNotContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
} }
/** /**
* Helper function for the DOS matchers * Helper function for the DOS matchers
*/ */
@ -792,14 +797,14 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
return $dbConn && (substr($dbConn->getSelectedDatabase(), 0, strlen($prefix) + 5) return $dbConn && (substr($dbConn->getSelectedDatabase(), 0, strlen($prefix) + 5)
== strtolower(sprintf('%stmpdb', $prefix))); == strtolower(sprintf('%stmpdb', $prefix)));
} }
public static function kill_temp_db() { public static function kill_temp_db() {
// Delete our temporary database // Delete our temporary database
if(self::using_temp_db()) { if(self::using_temp_db()) {
$dbConn = DB::get_conn(); $dbConn = DB::get_conn();
$dbName = $dbConn->getSelectedDatabase(); $dbName = $dbConn->getSelectedDatabase();
if($dbName && DB::get_conn()->databaseExists($dbName)) { if($dbName && DB::get_conn()->databaseExists($dbName)) {
// Some DataExtensions keep a static cache of information that needs to // Some DataExtensions keep a static cache of information that needs to
// be reset whenever the database is killed // be reset whenever the database is killed
foreach(ClassInfo::subclassesFor('DataExtension') as $class) { foreach(ClassInfo::subclassesFor('DataExtension') as $class) {
$toCall = array($class, 'on_db_reset'); $toCall = array($class, 'on_db_reset');
@ -811,15 +816,15 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
} }
} }
} }
/** /**
* Remove all content from the temporary database. * Remove all content from the temporary database.
*/ */
public static function empty_temp_db() { public static function empty_temp_db() {
if(self::using_temp_db()) { if(self::using_temp_db()) {
DB::get_conn()->clearAllData(); DB::get_conn()->clearAllData();
// Some DataExtensions keep a static cache of information that needs to // Some DataExtensions keep a static cache of information that needs to
// be reset whenever the database is cleaned out // be reset whenever the database is cleaned out
$classes = array_merge(ClassInfo::subclassesFor('DataExtension'), ClassInfo::subclassesFor('DataObject')); $classes = array_merge(ClassInfo::subclassesFor('DataExtension'), ClassInfo::subclassesFor('DataObject'));
foreach($classes as $class) { foreach($classes as $class) {
@ -828,7 +833,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
} }
} }
} }
public static function create_temp_db() { public static function create_temp_db() {
// Disable PHPUnit error handling // Disable PHPUnit error handling
restore_error_handler(); restore_error_handler();
@ -848,13 +853,13 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$st = Injector::inst()->create('SapphireTest'); $st = Injector::inst()->create('SapphireTest');
$st->resetDBSchema(); $st->resetDBSchema();
// Reinstate PHPUnit error handling // Reinstate PHPUnit error handling
set_error_handler(array('PHPUnit_Util_ErrorHandler', 'handleError')); set_error_handler(array('PHPUnit_Util_ErrorHandler', 'handleError'));
return $dbname; return $dbname;
} }
public static function delete_all_temp_dbs() { public static function delete_all_temp_dbs() {
$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_'; $prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
foreach(DB::get_schema()->databaseList() as $dbName) { foreach(DB::get_schema()->databaseList() as $dbName) {
@ -869,7 +874,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
} }
} }
} }
/** /**
* Reset the testing database's schema. * Reset the testing database's schema.
* @param $includeExtraDataObjects If true, the extraDataObjects tables will also be included * @param $includeExtraDataObjects If true, the extraDataObjects tables will also be included
@ -909,7 +914,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
singleton('DataObject')->flushCache(); singleton('DataObject')->flushCache();
} }
} }
/** /**
* Create a member and group with the given permission code, and log in with it. * Create a member and group with the given permission code, and log in with it.
* Returns the member ID. * Returns the member ID.
@ -924,25 +929,25 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$permission->Code = $permCode; $permission->Code = $permCode;
$permission->write(); $permission->write();
$group->Permissions()->add($permission); $group->Permissions()->add($permission);
$member = DataObject::get_one('Member', array( $member = DataObject::get_one('Member', array(
'"Member"."Email"' => "$permCode@example.org" '"Member"."Email"' => "$permCode@example.org"
)); ));
if(!$member) $member = Injector::inst()->create('Member'); if(!$member) $member = Injector::inst()->create('Member');
$member->FirstName = $permCode; $member->FirstName = $permCode;
$member->Surname = "User"; $member->Surname = "User";
$member->Email = "$permCode@example.org"; $member->Email = "$permCode@example.org";
$member->write(); $member->write();
$group->Members()->add($member); $group->Members()->add($member);
$this->cache_generatedMembers[$permCode] = $member; $this->cache_generatedMembers[$permCode] = $member;
} }
$this->cache_generatedMembers[$permCode]->logIn(); $this->cache_generatedMembers[$permCode]->logIn();
return $this->cache_generatedMembers[$permCode]->ID; return $this->cache_generatedMembers[$permCode]->ID;
} }
/** /**
* Cache for logInWithPermission() * Cache for logInWithPermission()
*/ */

View File

@ -493,4 +493,4 @@ for adding notes for other developers but for things you don't want published in
## API Documentation ## API Documentation
* [api:SSViewer] * [api:SSViewer]
* [api:SS_TemplateManifest] * [api:SS_TemplateManifest]

View File

@ -182,18 +182,10 @@ end of each test.
$page->publish('Stage', 'Live'); $page->publish('Stage', 'Live');
} }
// reset configuration for the test. // set custom configuration for the test.
Config::nest();
Config::inst()->update('Foo', 'bar', 'Hello!'); Config::inst()->update('Foo', 'bar', 'Hello!');
} }
public function tearDown() {
// restores the config variables
Config::unnest();
parent::tearDown();
}
public function testMyMethod() { public function testMyMethod() {
// .. // ..
} }
@ -223,6 +215,32 @@ individual test case.
// .. // ..
} }
} }
### Config and Injector Nesting
A powerful feature of both [`Config`](/developer_guides/configuration/configuration/) and [`Injector`](/developer_guides/extending/injector/) is the ability to "nest" them so that you can make changes that can easily be discarded without having to manage previous values.
The testing suite makes use of this to "sandbox" each of the unit tests as well as each suite to prevent leakage between tests.
If you need to make changes to `Config` (or `Injector) for each test (or the whole suite) you can safely update `Config` (or `Injector`) settings in the `setUp` or `tearDown` functions.
It's important to remember that the `parent::setUp();` functions will need to be called first to ensure the nesting feature works as expected.
:::php
function setUpOnce() {
parent::setUpOnce();
//this will remain for the whole suite and be removed for any other tests
Config::inst()->update('ClassName', 'var_name', 'var_value');
}
function testFeatureDoesAsExpected() {
//this will be reset to 'var_value' at the end of this test function
Config::inst()->update('ClassName', 'var_name', 'new_var_value');
}
function testAnotherFeatureDoesAsExpected() {
Config::inst()->get('ClassName', 'var_name'); // this will be 'var_value'
}
## Generating a Coverage Report ## Generating a Coverage Report
@ -266,4 +284,4 @@ some `thirdparty/` directories add the following to the `phpunit.xml` configurat
* [api:TestRunner] * [api:TestRunner]
* [api:SapphireTest] * [api:SapphireTest]
* [api:FunctionalTest] * [api:FunctionalTest]

View File

@ -1,11 +1,11 @@
title: Changelogs title: Changelogs
introduction: Key information on new features and improvements in each version. introduction: Key information on new features and improvements in each version.
Keep up to date with new releases subscribe to the [SilverStripe Release Announcements](https://groups.google.com/group/silverstripe-announce) group, Keep up to date with new releases by subscribing to the [SilverStripe Release Announcements](https://groups.google.com/group/silverstripe-announce) group,
or read our [blog posts about releases](http://silverstripe.org/blog/tag/release). or read our [blog posts about releases](http://silverstripe.org/blog/tag/release).
We also keep an overview of [security-related releases](http://silverstripe.org/security-releases/). We also keep an overview of [security-related releases](http://silverstripe.org/security-releases/).
For information on how to upgrade to newer versions consult the [upgrading](/upgrading) guide. For information on how to upgrade to newer versions consult the [upgrading](/upgrading) guide.
[CHILDREN] [CHILDREN]

View File

@ -62,4 +62,4 @@ read our guide on [how to write secure code](/developer_guides/security/secure_c
* [silverstripe.org/forums](http://www.silverstripe.org/community/forums/): Forums on silverstripe.org * [silverstripe.org/forums](http://www.silverstripe.org/community/forums/): Forums on silverstripe.org
* [silverstripe-dev](http://groups.google.com/group/silverstripe-dev/): Core development mailinglist * [silverstripe-dev](http://groups.google.com/group/silverstripe-dev/): Core development mailinglist
* [silverstripe-documentation](http://groups.google.com/group/silverstripe-translators/): Translation team mailing list * [silverstripe-documentation](http://groups.google.com/group/silverstripe-documentation/): Documentation mailing list

View File

@ -16,8 +16,7 @@ page you want to edit. Alternatively, locate the appropriate .md file in the
* After editing the documentation, describe your changes in the "commit summary" and "extended description" fields below then press "Commit Changes". * After editing the documentation, describe your changes in the "commit summary" and "extended description" fields below then press "Commit Changes".
* After that you will see a form to submit a Pull Request: "[pull requests](http://help.github.com/pull-requests/)". You should be able to adjust the version your * After that you will see a form to submit a Pull Request: "[pull requests](http://help.github.com/pull-requests/)". You should be able to adjust the version your documentation changes apply to and then submit the form. Your changes will be sent to the core committers for approval.
* documentation changes are for and then submit the form. Your changes will be sent to the core committers for approval.
<div class="warning" markdown='1'> <div class="warning" markdown='1'>
You should make your changes in the lowest branch they apply to. For instance, if you fix a spelling issue that you found in the 3.1 documentation, submit your fix to that branch in Github and it'll be copied to the master (3.2) version of the documentation automatically. *Don't submit multiple pull requests*. You should make your changes in the lowest branch they apply to. For instance, if you fix a spelling issue that you found in the 3.1 documentation, submit your fix to that branch in Github and it'll be copied to the master (3.2) version of the documentation automatically. *Don't submit multiple pull requests*.
@ -57,7 +56,7 @@ for documenting open source software.
* Use PHPDoc in source code: Leave low level technical documentation to code comments within PHP, in [PHPDoc](http://en.wikipedia.org/wiki/PHPDoc) format. * Use PHPDoc in source code: Leave low level technical documentation to code comments within PHP, in [PHPDoc](http://en.wikipedia.org/wiki/PHPDoc) format.
* API and developer guides are two forms of source code documentation that complement each other. * API and developer guides are two forms of source code documentation that complement each other.
* API documentation should provide context, ie, the "bigger picture", by referring to developer guides inside your PHPDoc. * API documentation should provide context, ie, the "bigger picture", by referring to developer guides inside your PHPDoc.
* Make your documentation easy to find: Documentation lives by interlinking content so please make sure your contribution doesn't become an inaccessible island. At the very least, put a link to your should on the index page in the same folder. A link to your page can also appear * Make your documentation easy to find: Documentation is useful only when it is interlinked so please make sure your contribution doesn't become an inaccessible island. At the very least, put a link to your index page in the same folder. A link to your page can also appear
as "related content" on other resource (e.g. `/tutorials/site_search` might link to `/developer_guides/forms/introduction`). as "related content" on other resource (e.g. `/tutorials/site_search` might link to `/developer_guides/forms/introduction`).
## Writing style ## Writing style
@ -85,7 +84,7 @@ sparingly.
"Tip box": A tip box is great for adding, deepening or accenting information in the main text. They can be used for background knowledge, or to provide links to further information (ie, a "see also" link). "Tip box": A tip box is great for adding, deepening or accenting information in the main text. They can be used for background knowledge, or to provide links to further information (ie, a "see also" link).
</div> </div>
Code: Code for a Tip box:
<div class="hint" markdown='1'> <div class="hint" markdown='1'>
... ...
@ -95,7 +94,7 @@ Code:
"Notification box": A notification box is good for technical notifications relating to the main text. For example, notifying users about a deprecated feature. "Notification box": A notification box is good for technical notifications relating to the main text. For example, notifying users about a deprecated feature.
</div> </div>
Code: Code for a Notification box:
<div class="notice" markdown='1'> <div class="notice" markdown='1'>
... ...
@ -105,7 +104,7 @@ Code:
"Warning box": A warning box is useful for highlighting a severe bug or a technical issue requiring a user's attention. For example, suppose a rare edge case sometimes leads to a variable being overwritten incorrectly. A warning box can be used to alert the user to this case so they can write their own code to handle it. "Warning box": A warning box is useful for highlighting a severe bug or a technical issue requiring a user's attention. For example, suppose a rare edge case sometimes leads to a variable being overwritten incorrectly. A warning box can be used to alert the user to this case so they can write their own code to handle it.
</div> </div>
Code: Code for a Warning box:
<div class="warning" markdown='1'> <div class="warning" markdown='1'>
... ...

View File

@ -1,12 +1,12 @@
title: Implement Internationalization title: Implement Internationalisation
summary: Implement SilverStripe's internationalization system in your own modules. summary: Implement SilverStripe's internationalisation system in your own modules.
# Implementing Internationalization # Implementing Internationalisation
To find out about how to assist with translating SilverStripe from a users point of view, see the To find out about how to assist with translating SilverStripe from a user's point of view, see the
[Contributing Translations page](/contributing/translations). [Contributing Translations page](/contributing/translations).
## Set up your own module for localization ## Set up your own module for localisation
### Collecting translatable text ### Collecting translatable text
@ -33,7 +33,7 @@ source_lang = en
type = YML type = YML
``` ```
If you don't have existing translations, your project is ready to go - simply point translators to the URL, have them If you don't have existing translations to import, your project is ready to go - simply point translators to the URL, have them
sign up, and they can create languages and translations as required. sign up, and they can create languages and translations as required.
### Import existing translations ### Import existing translations
@ -57,7 +57,7 @@ assumes you're adhering to the following guidelines:
- Run the `i18nTextCollectorTask` with the `merge=true` option to avoid deleting unused entities - Run the `i18nTextCollectorTask` with the `merge=true` option to avoid deleting unused entities
(which might still be relevant in older release branches) (which might still be relevant in older release branches)
### Converting your language files from 2.4 PHP format ### Converting your language files from 2.4 PHP format to YML
The conversion from PHP format to YML is taken care of by a module called The conversion from PHP format to YML is taken care of by a module called
[i18n_yml_converter](https://github.com/chillu/i18n_yml_converter). [i18n_yml_converter](https://github.com/chillu/i18n_yml_converter).
@ -116,7 +116,7 @@ First of all, you need to create those source files in JSON, and store them in `
source_lang = en source_lang = en
type = KEYVALUEJSON type = KEYVALUEJSON
Now you can upload the source files via a normal `tx push`. Once translations come in, you need to convert the source Then you can upload the source files via a normal `tx push`. Once translations come in, you need to convert the source
files back into the JS files SilverStripe can actually read. This requires an installation of our files back into the JS files SilverStripe can actually read. This requires an installation of our
[buildtools](https://github.com/silverstripe/silverstripe-buildtools). [buildtools](https://github.com/silverstripe/silverstripe-buildtools).
@ -130,4 +130,4 @@ files back into the JS files SilverStripe can actually read. This requires an in
* [i18n](/developer_guides/i18n/): Developer-level documentation of Silverstripe's i18n capabilities * [i18n](/developer_guides/i18n/): Developer-level documentation of Silverstripe's i18n capabilities
* [Contributing Translations](/contributing/translations): Information for translators looking to contribute translations of the SilverStripe UI. * [Contributing Translations](/contributing/translations): Information for translators looking to contribute translations of the SilverStripe UI.
* [translatable](https://github.com/silverstripe/silverstripe-translatable): DataObject-interface powering the website-content translations * [translatable](https://github.com/silverstripe/silverstripe-translatable): DataObject-interface powering the website-content translations
* ["Translatable ModelAdmin" module](http://silverstripe.org/translatablemodeladmin-module/): An extension which allows translations of DataObjects inside ModelAdmin * ["Translatable ModelAdmin" module](http://silverstripe.org/translatablemodeladmin-module/): An extension which allows translations of DataObjects inside ModelAdmin

View File

@ -633,7 +633,7 @@ class File extends DataObject {
// If it's changed, check for duplicates // If it's changed, check for duplicates
if($oldName && $oldName != $name) { if($oldName && $oldName != $name) {
$base = pathinfo($name, PATHINFO_BASENAME); $base = pathinfo($name, PATHINFO_FILENAME);
$ext = self::get_file_extension($name); $ext = self::get_file_extension($name);
$suffix = 1; $suffix = 1;
@ -645,7 +645,7 @@ class File extends DataObject {
))->first() ))->first()
) { ) {
$suffix++; $suffix++;
$name = "$base-$suffix$ext"; $name = "$base-$suffix.$ext";
} }
} }

View File

@ -96,8 +96,12 @@ class CheckboxSetField extends OptionsetField {
} }
} }
} elseif($values && is_string($values)) { } elseif($values && is_string($values)) {
$items = explode(',', $values); if(!empty($values)) {
$items = str_replace('{comma}', ',', $items); $items = explode(',', $values);
$items = str_replace('{comma}', ',', $items);
} else {
$items = array();
}
} }
} }
} else { } else {
@ -109,8 +113,12 @@ class CheckboxSetField extends OptionsetField {
$items = array(); $items = array();
} }
else { else {
$items = explode(',', $values); if(!empty($values)) {
$items = str_replace('{comma}', ',', $items); $items = explode(',', $values);
$items = str_replace('{comma}', ',', $items);
} else {
$items = array();
}
} }
} }
} }

View File

@ -63,7 +63,7 @@ class DatetimeField extends FormField {
->addExtraClass('fieldgroup-field'); ->addExtraClass('fieldgroup-field');
$this->timeField = TimeField::create($name . '[time]', false) $this->timeField = TimeField::create($name . '[time]', false)
->addExtraClass('fieldgroup-field'); ->addExtraClass('fieldgroup-field');
$this->timezoneField = new HiddenField($this->getName() . '[timezone]'); $this->timezoneField = new HiddenField($name . '[timezone]');
parent::__construct($name, $title, $value); parent::__construct($name, $title, $value);
} }

View File

@ -1,35 +1,43 @@
<?php <?php
/** /**
* NullableField is a field that wraps other fields when you want to allow the user to specify whether the value of * NullableField is a field that wraps other fields when you want to allow the user to specify
* the field is null or not. * whether the value of the field is null or not.
*
* The classic case is to wrap a TextField so that the user can distinguish between an empty string
* and a null string.
* *
* The classic case is to wrap a TextField so that the user can distinguish between an empty string and a null string.
* $a = new NullableField(new TextField("Field1", "Field 1", "abc")); * $a = new NullableField(new TextField("Field1", "Field 1", "abc"));
* *
* It displays the field that is wrapped followed by a checkbox that is used to specify if the value is null or not. * It displays the field that is wrapped followed by a checkbox that is used to specify if the
* It uses the Title of the wrapped field for its title. * value is null or not. It uses the Title of the wrapped field for its title.
* When a form is submitted the field tests the value of the "is null" checkbox and sets its value accordingly. *
* You can retrieve the value of the wrapped field from the NullableField as follows: * When a form is submitted the field tests the value of the "is null" checkbox and sets its value
* accordingly. You can retrieve the value of the wrapped field from the NullableField as follows:
*
* $field->Value() or $field->dataValue() * $field->Value() or $field->dataValue()
* *
* You can specify the label to use for the "is null" checkbox. If you want to use I8N for this label then specify it * You can specify the label to use for the "is null" checkbox. If you want to use i18n for this
* like this: * label then specify it like this:
* *
* $field->setIsNullLabel(_T(SOME_MODULE_ISNULL_LABEL, "Is Null"); * $field->setIsNullLabel(_T(SOME_MODULE_ISNULL_LABEL, "Is Null"));
* *
* @author Pete Bacon Darwin * @author Pete Bacon Darwin
*
* @package forms * @package forms
* @subpackage fields-basic * @subpackage fields-basic
*/ */
class NullableField extends FormField { class NullableField extends FormField {
/** /**
* The field that holds the value of this field * The field that holds the value of this field
*
* @var FormField * @var FormField
*/ */
protected $valueField; protected $valueField;
/** /**
* The label to show next to the is null check box. * The label to show next to the is null check box.
*
* @var string * @var string
*/ */
protected $isNullLabel; protected $isNullLabel;
@ -37,39 +45,55 @@ class NullableField extends FormField {
/** /**
* Create a new nullable field * Create a new nullable field
* @param $valueField *
* @return NullableField * @param FormField $valueField
* @param null|string $isNullLabel
*/ */
public function __construct(FormField $valueField, $isNullLabel = null) { public function __construct(FormField $valueField, $isNullLabel = null) {
$this->valueField = $valueField; $this->valueField = $valueField;
$this->isNullLabel = $isNullLabel;
if ( is_null($this->isNullLabel) ) { if(isset($isNullLabel)) {
// Set a default label if one is not provided. $this->setIsNullLabel($isNullLabel);
} else {
$this->isNullLabel = _t('NullableField.IsNullLabel', 'Is Null'); $this->isNullLabel = _t('NullableField.IsNullLabel', 'Is Null');
} }
parent::__construct($valueField->getName(), $valueField->Title(), $valueField->Value(),
$valueField->getForm(), $valueField->RightTitle()); parent::__construct(
$this->readonly = $valueField->isReadonly(); $valueField->getName(),
$valueField->Title(),
$valueField->Value()
);
$this->setForm($valueField->getForm());
$this->setRightTitle($valueField->RightTitle());
$this->setReadonly($valueField->isReadonly());
} }
/** /**
* Get the label used for the Is Null checkbox. * Get the label used for the Is Null checkbox.
*
* @return string * @return string
*/ */
public function getIsNullLabel() { public function getIsNullLabel() {
return $this->isNullLabel; return $this->isNullLabel;
} }
/** /**
* Set the label used for the Is Null checkbox. * Set the label used for the Is Null checkbox.
*
* @param $isNulLabel string * @param $isNulLabel string
*
* @return $this
*/ */
public function setIsNullLabel(string $isNulLabel){ public function setIsNullLabel($isNulLabel) {
$this->isNullLabel = $isNulLabel; $this->isNullLabel = $isNulLabel;
return $this; return $this;
} }
/** /**
* Get the id used for the Is Null check box. * Get the id used for the Is Null check box.
*
* @return string * @return string
*/ */
public function getIsNullId() { public function getIsNullId() {
@ -77,54 +101,81 @@ class NullableField extends FormField {
} }
/** /**
* (non-PHPdoc) * @param array $properties
* @see framework/forms/FormField#Field() *
* @return string
*/ */
public function Field($properties = array()) { public function Field($properties = array()) {
if ( $this->isReadonly()) { if($this->isReadonly()) {
$nullableCheckbox = new CheckboxField_Readonly($this->getIsNullId()); $nullableCheckbox = new CheckboxField_Readonly($this->getIsNullId());
} else { } else {
$nullableCheckbox = new CheckboxField($this->getIsNullId()); $nullableCheckbox = new CheckboxField($this->getIsNullId());
} }
$nullableCheckbox->setValue(is_null($this->dataValue())); $nullableCheckbox->setValue(is_null($this->dataValue()));
return $this->valueField->Field() . ' ' . $nullableCheckbox->Field() return sprintf(
. '&nbsp;<span>' . $this->getIsNullLabel().'</span>'; '%s %s&nbsp;<span>%s</span>',
$this->valueField->Field(),
$nullableCheckbox->Field(),
$this->getIsNullLabel()
);
} }
/** /**
* Value is sometimes an array, and sometimes a single value, so we need to handle both cases * Value is sometimes an array, and sometimes a single value, so we need to handle both cases
*
* @param mixed $value
* @param null|array $data
*
* @return $this
*/ */
public function setValue($value, $data = null) { public function setValue($value, $data = null) {
if ( is_array($data) && array_key_exists($this->getIsNullId(), $data) && $data[$this->getIsNullId()] ) { $id = $this->getIsNullId();
if(is_array($data) && array_key_exists($id, $data) && $data[$id]) {
$value = null; $value = null;
} }
$this->valueField->setValue($value); $this->valueField->setValue($value);
parent::setValue($value); parent::setValue($value);
return $this; return $this;
} }
/** /**
* (non-PHPdoc) * @param string $name
* @see forms/FormField#setName($name) *
* @return $this
*/ */
public function setName($name) { public function setName($name) {
// We need to pass through the name change to the underlying value field.
$this->valueField->setName($name); $this->valueField->setName($name);
parent::setName($name); parent::setName($name);
return $this; return $this;
} }
/** /**
* (non-PHPdoc) * @return string
* @see framework/forms/FormField#debug()
*/ */
public function debug() { public function debug() {
$result = "$this->class ($this->name: $this->title : <font style='color:red;'>$this->message</font>) = "; $result = sprintf(
$result .= (is_null($this->value)) ? "<<null>>" : $this->value; '%s (%s: $s : <span style="color: red">%s</span>) = ',
return result; $this->class,
$this->name,
$this->title,
$this->message
);
if($this->value === null) {
$result .= "<<null>>";
} else {
$result .= (string) $this->value;
}
return $result;
} }
} }

View File

@ -9,56 +9,75 @@
* @subpackage fields-formattedinput * @subpackage fields-formattedinput
*/ */
class NumericField extends TextField { class NumericField extends TextField {
/** /**
* Override locale for this field * Override locale for this field.
* *
* @var string * @var string
*/ */
protected $locale = null; protected $locale = null;
/**
* @param mixed $value
* @param array $data
*
* @return $this
*
* @throws Zend_Locale_Exception
*/
public function setValue($value, $data = array()) { public function setValue($value, $data = array()) {
require_once "Zend/Locale/Format.php"; require_once "Zend/Locale/Format.php";
// If passing in a non-string number, or a value // If passing in a non-string number, or a value
// directly from a dataobject then localise this number // directly from a DataObject then localise this number
if ((is_numeric($value) && !is_string($value)) ||
($value && $data instanceof DataObject) if(is_int($value) || is_float($value) || $data instanceof DataObject) {
){
$locale = new Zend_Locale($this->getLocale()); $locale = new Zend_Locale($this->getLocale());
$this->value = Zend_Locale_Format::toNumber($value, array('locale' => $locale));
$this->value = Zend_Locale_Format::toNumber(
$value,
array('locale' => $locale)
);
} else { } else {
// If an invalid number, store it anyway, but validate() will fail
$this->value = $this->clean($value); $this->value = $this->clean($value);
} }
return $this; return $this;
} }
/** /**
* In some cases and locales, validation expects non-breaking spaces * In some cases and locales, validation expects non-breaking spaces.
*
* Returns the value, with all spaces replaced with non-breaking spaces.
* *
* @param string $input * @param string $input
* @return string The input value, with all spaces replaced with non-breaking spaces *
* @return string
*/ */
protected function clean($input) { protected function clean($input) {
$nbsp = html_entity_decode('&nbsp;', null, 'UTF-8'); $replacement = html_entity_decode('&nbsp;', null, 'UTF-8');
return str_replace(' ', $nbsp, trim($input));
return str_replace(' ', $replacement, trim($input));
} }
/** /**
* Determine if the current value is a valid number in the current locale * Determine if the current value is a valid number in the current locale.
* *
* @return bool * @return bool
*/ */
protected function isNumeric() { protected function isNumeric() {
require_once "Zend/Locale/Format.php"; require_once "Zend/Locale/Format.php";
$locale = new Zend_Locale($this->getLocale()); $locale = new Zend_Locale($this->getLocale());
return Zend_Locale_Format::isNumber( return Zend_Locale_Format::isNumber(
$this->clean($this->value), $this->clean($this->value),
array('locale' => $locale) array('locale' => $locale)
); );
} }
/**
* {@inheritdoc}
*/
public function Type() { public function Type() {
return 'numeric text'; return 'numeric text';
} }
@ -81,74 +100,111 @@ class NumericField extends TextField {
return true; return true;
} }
if($this->isNumeric()) return true; if($this->isNumeric()) {
return true;
}
$validator->validationError( $validator->validationError(
$this->name, $this->name,
_t( _t(
'NumericField.VALIDATION', "'{value}' is not a number, only numbers can be accepted for this field", 'NumericField.VALIDATION',
"'{value}' is not a number, only numbers can be accepted for this field",
array('value' => $this->value) array('value' => $this->value)
), ),
"validation" "validation"
); );
return false; return false;
} }
/** /**
* Extracts the number value from the localised string value * Extracts the number value from the localised string value.
* *
* @return string number value * @return string
*/ */
public function dataValue() { public function dataValue() {
require_once "Zend/Locale/Format.php"; require_once "Zend/Locale/Format.php";
if(!$this->isNumeric()) return 0;
if(!$this->isNumeric()) {
return 0;
}
$locale = new Zend_Locale($this->getLocale()); $locale = new Zend_Locale($this->getLocale());
$number = Zend_Locale_Format::getNumber( $number = Zend_Locale_Format::getNumber(
$this->clean($this->value), $this->clean($this->value),
array('locale' => $locale) array('locale' => $locale)
); );
return $number; return $number;
} }
/** /**
* Returns a readonly version of this field * Creates a read-only version of the field.
*
* @return NumericField_Readonly
*/ */
public function performReadonlyTransformation() { public function performReadonlyTransformation() {
$field = new NumericField_Readonly($this->name, $this->title, $this->value); $field = new NumericField_Readonly(
$this->name,
$this->title,
$this->value
);
$field->setForm($this->form); $field->setForm($this->form);
return $field; return $field;
} }
/** /**
* Gets the current locale this field is set to * Gets the current locale this field is set to.
* *
* @return string * @return string
*/ */
public function getLocale() { public function getLocale() {
return $this->locale ?: i18n::get_locale(); if($this->locale) {
return $this->locale;
}
return i18n::get_locale();
} }
/** /**
* Override the locale for this field * Override the locale for this field.
* *
* @param string $locale * @param string $locale
*
* @return $this * @return $this
*/ */
public function setLocale($locale) { public function setLocale($locale) {
$this->locale = $locale; $this->locale = $locale;
return $this; return $this;
} }
} }
/**
* Readonly version of a numeric field.
*
* @package forms
* @subpackage fields-basic
*/
class NumericField_Readonly extends ReadonlyField { class NumericField_Readonly extends ReadonlyField {
/**
* @return static
*/
public function performReadonlyTransformation() { public function performReadonlyTransformation() {
return clone $this; return clone $this;
} }
/**
* @return string
*/
public function Value() { public function Value() {
return Convert::raw2xml($this->value ? "$this->value" : "0"); if($this->value) {
} return Convert::raw2xml((string) $this->value);
}
return '0';
}
} }

View File

@ -3,12 +3,11 @@
/** /**
* Displays a {@link SS_List} in a grid format. * Displays a {@link SS_List} in a grid format.
* *
* GridField is a field that takes an SS_List and displays it in an table with rows * GridField is a field that takes an SS_List and displays it in an table with rows and columns.
* and columns. It reminds of the old TableFields but works with SS_List types * It reminds of the old TableFields but works with SS_List types and only loads the necessary
* and only loads the necessary rows from the list. * rows from the list.
* *
* The minimum configuration is to pass in name and title of the field and a * The minimum configuration is to pass in name and title of the field and a SS_List.
* SS_List.
* *
* <code> * <code>
* $gridField = new GridField('ExampleGrid', 'Example grid', new DataList('Page')); * $gridField = new GridField('ExampleGrid', 'Example grid', new DataList('Page'));
@ -21,45 +20,44 @@
* @property GridState_Data $State The gridstate of this object * @property GridState_Data $State The gridstate of this object
*/ */
class GridField extends FormField { class GridField extends FormField {
/** /**
*
* @var array * @var array
*/ */
private static $allowed_actions = array( private static $allowed_actions = array(
'index', 'index',
'gridFieldAlterAction' 'gridFieldAlterAction',
); );
/** /**
* The datasource * Data source.
* *
* @var SS_List * @var SS_List
*/ */
protected $list = null; protected $list = null;
/** /**
* The classname of the DataObject that the GridField will display. Defaults to the value of $this->list->dataClass * Class name of the DataObject that the GridField will display.
*
* Defaults to the value of $this->list->dataClass.
* *
* @var string * @var string
*/ */
protected $modelClassName = ''; protected $modelClassName = '';
/** /**
* the current state of the GridField * Current state of the GridField.
* *
* @var GridState * @var GridState
*/ */
protected $state = null; protected $state = null;
/** /**
*
* @var GridFieldConfig * @var GridFieldConfig
*/ */
protected $config = null; protected $config = null;
/** /**
* The components list * Components list.
* *
* @var array * @var array
*/ */
@ -67,14 +65,15 @@ class GridField extends FormField {
/** /**
* Internal dispatcher for column handlers. * Internal dispatcher for column handlers.
* Keys are column names and values are GridField_ColumnProvider objects *
* Keys are column names and values are GridField_ColumnProvider objects.
* *
* @var array * @var array
*/ */
protected $columnDispatch = null; protected $columnDispatch = null;
/** /**
* Map of callbacks for custom data fields * Map of callbacks for custom data fields.
* *
* @var array * @var array
*/ */
@ -86,8 +85,6 @@ class GridField extends FormField {
protected $name = ''; protected $name = '';
/** /**
* Creates a new GridField field
*
* @param string $name * @param string $name
* @param string $title * @param string $title
* @param SS_List $dataList * @param SS_List $dataList
@ -95,13 +92,18 @@ class GridField extends FormField {
*/ */
public function __construct($name, $title = null, SS_List $dataList = null, GridFieldConfig $config = null) { public function __construct($name, $title = null, SS_List $dataList = null, GridFieldConfig $config = null) {
parent::__construct($name, $title, null); parent::__construct($name, $title, null);
$this->name = $name; $this->name = $name;
if($dataList) { if($dataList) {
$this->setList($dataList); $this->setList($dataList);
} }
$this->setConfig($config ?: GridFieldConfig_Base::create()); if(!$config) {
$config = GridFieldConfig_Base::create();
}
$this->setConfig($config);
$this->config->addComponent(new GridState_Component()); $this->config->addComponent(new GridState_Component());
$this->state = new GridState($this); $this->state = new GridState($this);
@ -109,44 +111,58 @@ class GridField extends FormField {
$this->addExtraClass('ss-gridfield'); $this->addExtraClass('ss-gridfield');
} }
/**
* @param SS_HTTPRequest $request
*
* @return string
*/
public function index($request) { public function index($request) {
return $this->gridFieldAlterAction(array(), $this->getForm(), $request); return $this->gridFieldAlterAction(array(), $this->getForm(), $request);
} }
/** /**
* Set the modelClass (dataobject) that this field will get it column headers from. * Set the modelClass (data object) that this field will get it column headers from.
* If no $displayFields has been set, the displayfields will be fetched from *
* this modelclass $summary_fields * If no $displayFields has been set, the display fields will be $summary_fields.
*
* @see GridFieldDataColumns::getDisplayFields()
* *
* @param string $modelClassName * @param string $modelClassName
* *
* @see GridFieldDataColumns::getDisplayFields() * @return $this
*/ */
public function setModelClass($modelClassName) { public function setModelClass($modelClassName) {
$this->modelClassName = $modelClassName; $this->modelClassName = $modelClassName;
return $this; return $this;
} }
/** /**
* Returns a dataclass that is a DataObject type that this GridField should look like. * Returns a data class that is a DataObject type that this GridField should look like.
* *
* @throws Exception
* @return string * @return string
*
* @throws LogicException
*/ */
public function getModelClass() { public function getModelClass() {
if($this->modelClassName) return $this->modelClassName; if($this->modelClassName) {
if($this->list && method_exists($this->list, 'dataClass')) { return $this->modelClassName;
$class = $this->list->dataClass();
if($class) return $class;
} }
throw new LogicException('GridField doesn\'t have a modelClassName,' if($this->list && method_exists($this->list, 'dataClass')) {
. ' so it doesn\'t know the columns of this grid.'); $class = $this->list->dataClass();
if($class) {
return $class;
}
}
throw new LogicException(
'GridField doesn\'t have a modelClassName, so it doesn\'t know the columns of this grid.'
);
} }
/** /**
* Get the GridFieldConfig
*
* @return GridFieldConfig * @return GridFieldConfig
*/ */
public function getConfig() { public function getConfig() {
@ -156,61 +172,69 @@ class GridField extends FormField {
/** /**
* @param GridFieldConfig $config * @param GridFieldConfig $config
* *
* @return GridField * @return $this
*/ */
public function setConfig(GridFieldConfig $config) { public function setConfig(GridFieldConfig $config) {
$this->config = $config; $this->config = $config;
return $this; return $this;
} }
/**
* @return ArrayList
*/
public function getComponents() { public function getComponents() {
return $this->config->getComponents(); return $this->config->getComponents();
} }
/** /**
* Cast a arbitrary value with the help of a castingDefintion * Cast an arbitrary value with the help of a $castingDefinition.
*
* @param $value
* @param $castingDefinition
* *
* @todo refactor this into GridFieldComponent * @todo refactor this into GridFieldComponent
*
* @param mixed $value
* @param string|array $castingDefinition
*
* @return mixed
*/ */
public function getCastedValue($value, $castingDefinition) { public function getCastedValue($value, $castingDefinition) {
$castingParams = array();
if(is_array($castingDefinition)) { if(is_array($castingDefinition)) {
$castingParams = $castingDefinition; $castingParams = $castingDefinition;
array_shift($castingParams); array_shift($castingParams);
$castingDefinition = array_shift($castingDefinition); $castingDefinition = array_shift($castingDefinition);
} else {
$castingParams = array();
} }
if(strpos($castingDefinition, '->') === false) { if(strpos($castingDefinition, '->') === false) {
$castingFieldType = $castingDefinition; $castingFieldType = $castingDefinition;
$castingField = DBField::create_field($castingFieldType, $value); $castingField = DBField::create_field($castingFieldType, $value);
$value = call_user_func_array(array($castingField, 'XML'), $castingParams);
} else { return call_user_func_array(array($castingField, 'XML'), $castingParams);
$fieldTypeParts = explode('->', $castingDefinition);
$castingFieldType = $fieldTypeParts[0];
$castingMethod = $fieldTypeParts[1];
$castingField = DBField::create_field($castingFieldType, $value);
$value = call_user_func_array(array($castingField, $castingMethod), $castingParams);
} }
return $value; list($castingFieldType, $castingMethod) = explode('->', $castingDefinition);
$castingField = DBField::create_field($castingFieldType, $value);
return call_user_func_array(array($castingField, $castingMethod), $castingParams);
} }
/** /**
* Set the datasource * Set the data source.
* *
* @param SS_List $list * @param SS_List $list
*
* @return $this
*/ */
public function setList(SS_List $list) { public function setList(SS_List $list) {
$this->list = $list; $this->list = $list;
return $this; return $this;
} }
/** /**
* Get the datasource * Get the data source.
* *
* @return SS_List * @return SS_List
*/ */
@ -219,26 +243,28 @@ class GridField extends FormField {
} }
/** /**
* Get the datasource after applying the {@link GridField_DataManipulator}s to it. * Get the data source after applying every {@link GridField_DataManipulator} to it.
* *
* @return SS_List * @return SS_List
*/ */
public function getManipulatedList() { public function getManipulatedList() {
$list = $this->getList(); $list = $this->getList();
foreach($this->getComponents() as $item) { foreach($this->getComponents() as $item) {
if($item instanceof GridField_DataManipulator) { if($item instanceof GridField_DataManipulator) {
$list = $item->getManipulatedData($this, $list); $list = $item->getManipulatedData($this, $list);
} }
} }
return $list; return $list;
} }
/** /**
* Get the current GridState_Data or the GridState * Get the current GridState_Data or the GridState.
* *
* @param bool $getData - flag for returning the GridState_Data or the GridState * @param bool $getData
* *
* @return GridState_data|GridState * @return GridState_Data|GridState
*/ */
public function getState($getData = true) { public function getState($getData = true) {
if($getData) { if($getData) {
@ -249,7 +275,9 @@ class GridField extends FormField {
} }
/** /**
* Returns the whole gridfield rendered with all the attached components * Returns the whole gridfield rendered with all the attached components.
*
* @param array $properties
* *
* @return string * @return string
*/ */
@ -265,88 +293,115 @@ class GridField extends FormField {
Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js'); Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js');
Requirements::javascript(FRAMEWORK_DIR . '/javascript/GridField.js'); Requirements::javascript(FRAMEWORK_DIR . '/javascript/GridField.js');
// Get columns
$columns = $this->getColumns(); $columns = $this->getColumns();
// Get data
$list = $this->getManipulatedList(); $list = $this->getManipulatedList();
// Render headers, footers, etc
$content = array( $content = array(
"before" => "", 'before' => '',
"after" => "", 'after' => '',
"header" => "", 'header' => '',
"footer" => "", 'footer' => '',
); );
foreach($this->getComponents() as $item) { foreach($this->getComponents() as $item) {
if($item instanceof GridField_HTMLProvider) { if($item instanceof GridField_HTMLProvider) {
$fragments = $item->getHTMLFragments($this); $fragments = $item->getHTMLFragments($this);
if($fragments) foreach($fragments as $k => $v) {
$k = strtolower($k);
if(!isset($content[$k])) $content[$k] = "";
$content[$k] .= $v . "\n";
}
}
}
foreach($content as $k => $v) { if($fragments) {
$content[$k] = trim($v); foreach($fragments as $fragmentKey => $fragmentValue) {
} $fragmentKey = strtolower($fragmentKey);
// Replace custom fragments and check which fragments are defined if(!isset($content[$fragmentKey])) {
// Nested dependencies are handled by deferring the rendering of any content item that $content[$fragmentKey] = '';
// Circular dependencies are detected by disallowing any item to be deferred more than 5 times
// It's a fairly crude algorithm but it works
$fragmentDefined = array('header' => true, 'footer' => true, 'before' => true, 'after' => true);
reset($content);
while(list($k, $v) = each($content)) {
if(preg_match_all('/\$DefineFragment\(([a-z0-9\-_]+)\)/i', $v, $matches)) {
foreach($matches[1] as $match) {
$fragmentName = strtolower($match);
$fragmentDefined[$fragmentName] = true;
$fragment = isset($content[$fragmentName]) ? $content[$fragmentName] : "";
// If the fragment still has a fragment definition in it, when we should defer this item until
// later.
if(preg_match('/\$DefineFragment\(([a-z0-9\-_]+)\)/i', $fragment, $matches)) {
// If we've already deferred this fragment, then we have a circular dependency
if(isset($fragmentDeferred[$k]) && $fragmentDeferred[$k] > 5) {
throw new LogicException("GridField HTML fragment '$fragmentName' and '$matches[1]' " .
"appear to have a circular dependency.");
} }
// Otherwise we can push to the end of the content array $content[$fragmentKey] .= $fragmentValue . "\n";
unset($content[$k]);
$content[$k] = $v;
if(!isset($fragmentDeferred[$k])) {
$fragmentDeferred[$k] = 1;
} else {
$fragmentDeferred[$k]++;
}
break;
} else {
$content[$k] = preg_replace('/\$DefineFragment\(' . $fragmentName . '\)/i', $fragment,
$content[$k]);
} }
} }
} }
} }
// Check for any undefined fragments, and if so throw an exception foreach($content as $contentKey => $contentValue) {
// While we're at it, trim whitespace off the elements $content[$contentKey] = trim($contentValue);
foreach($content as $k => $v) { }
if(empty($fragmentDefined[$k])) {
throw new LogicException("GridField HTML fragment '$k' was given content," // Replace custom fragments and check which fragments are defined. Circular dependencies
. " but not defined. Perhaps there is a supporting GridField component you need to add?"); // are detected by disallowing any item to be deferred more than 5 times.
$fragmentDefined = array(
'header' => true,
'footer' => true,
'before' => true,
'after' => true,
);
reset($content);
while(list($contentKey, $contentValue) = each($content)) {
if(preg_match_all('/\$DefineFragment\(([a-z0-9\-_]+)\)/i', $contentValue, $matches)) {
foreach($matches[1] as $match) {
$fragmentName = strtolower($match);
$fragmentDefined[$fragmentName] = true;
$fragment = '';
if(isset($content[$fragmentName])) {
$fragment = $content[$fragmentName];
}
// If the fragment still has a fragment definition in it, when we should defer
// this item until later.
if(preg_match('/\$DefineFragment\(([a-z0-9\-_]+)\)/i', $fragment, $matches)) {
if(isset($fragmentDeferred[$contentKey]) && $fragmentDeferred[$contentKey] > 5) {
throw new LogicException(sprintf(
'GridField HTML fragment "%s" and "%s" appear to have a circular dependency.',
$fragmentName,
$matches[1]
));
}
unset($content[$contentKey]);
$content[$contentKey] = $contentValue;
if(!isset($fragmentDeferred[$contentKey])) {
$fragmentDeferred[$contentKey] = 0;
}
$fragmentDeferred[$contentKey]++;
break;
} else {
$content[$contentKey] = preg_replace(
sprintf('/\$DefineFragment\(%s\)/i', $fragmentName),
$fragment,
$content[$contentKey]
);
}
}
}
}
// Check for any undefined fragments, and if so throw an exception.
// While we're at it, trim whitespace off the elements.
foreach($content as $contentKey => $contentValue) {
if(empty($fragmentDefined[$contentKey])) {
throw new LogicException(sprintf(
'GridField HTML fragment "%s" was given content, but not defined. Perhaps there is a supporting GridField component you need to add?',
$contentKey
));
} }
} }
$total = count($list); $total = count($list);
if($total > 0) { if($total > 0) {
$rows = array(); $rows = array();
foreach($list as $idx => $record) {
foreach($list as $index => $record) {
if($record->hasMethod('canView') && !$record->canView()) { if($record->hasMethod('canView') && !$record->canView()) {
continue; continue;
} }
@ -356,58 +411,80 @@ class GridField extends FormField {
foreach($this->getColumns() as $column) { foreach($this->getColumns() as $column) {
$colContent = $this->getColumnContent($record, $column); $colContent = $this->getColumnContent($record, $column);
// A return value of null means this columns should be skipped altogether. // Null means this columns should be skipped altogether.
if($colContent === null) { if($colContent === null) {
continue; continue;
} }
$colAttributes = $this->getColumnAttributes($record, $column); $colAttributes = $this->getColumnAttributes($record, $column);
$rowContent .= $this->newCell($total, $idx, $record, $colAttributes, $colContent); $rowContent .= $this->newCell(
$total,
$index,
$record,
$colAttributes,
$colContent
);
} }
$rowAttributes = $this->getRowAttributes($total, $idx, $record); $rowAttributes = $this->getRowAttributes($total, $index, $record);
$rows[] = $this->newRow($total, $idx, $record, $rowAttributes, $rowContent); $rows[] = $this->newRow($total, $index, $record, $rowAttributes, $rowContent);
} }
$content['body'] = implode("\n", $rows); $content['body'] = implode("\n", $rows);
} }
// Display a message when the grid field is empty // Display a message when the grid field is empty.
if(!(isset($content['body']) && $content['body'])) {
$content['body'] = FormField::create_tag( if(empty($content['body'])) {
'tr', $cell = FormField::create_tag(
array("class" => 'ss-gridfield-item ss-gridfield-no-items'), 'td',
FormField::create_tag( array(
'td', 'colspan' => count($columns),
array('colspan' => count($columns)), ),
_t('GridField.NoItemsFound', 'No items found') _t('GridField.NoItemsFound', 'No items found')
)
); );
$row = FormField::create_tag(
'tr',
array(
'class' => 'ss-gridfield-item ss-gridfield-no-items',
),
$cell
);
$content['body'] = $row;
} }
// Turn into the relevant parts of a table $header = $this->getOptionalTableHeader($content);
$head = $content['header'] $body = $this->getOptionalTableBody($content);
? FormField::create_tag('thead', array(), $content['header']) $footer = $this->getOptionalTableFooter($content);
: '';
$body = $content['body']
? FormField::create_tag('tbody', array('class' => 'ss-gridfield-items'), $content['body'])
: '';
$foot = $content['footer']
? FormField::create_tag('tfoot', array(), $content['footer'])
: '';
$this->addExtraClass('ss-gridfield field'); $this->addExtraClass('ss-gridfield field');
$attrs = array_diff_key(
$fieldsetAttributes = array_diff_key(
$this->getAttributes(), $this->getAttributes(),
array('value' => false, 'type' => false, 'name' => false) array(
'value' => false,
'type' => false,
'name' => false,
)
); );
$attrs['data-name'] = $this->getName();
$tableAttrs = array( $fieldsetAttributes['data-name'] = $this->getName();
'id' => isset($this->id) ? $this->id : null,
$tableId = null;
if($this->id) {
$tableId = $this->id;
}
$tableAttributes = array(
'id' => $tableId,
'class' => 'ss-gridfield-table', 'class' => 'ss-gridfield-table',
'cellpadding' => '0', 'cellpadding' => '0',
'cellspacing' => '0' 'cellspacing' => '0',
); );
if($this->getDescription()) { if($this->getDescription()) {
@ -418,12 +495,17 @@ class GridField extends FormField {
); );
} }
return $table = FormField::create_tag(
FormField::create_tag('fieldset', $attrs, 'table',
$content['before'] . $tableAttributes,
FormField::create_tag('table', $tableAttrs, $head . "\n" . $foot . "\n" . $body) . $header . "\n" . $footer . "\n" . $body
$content['after'] );
);
return FormField::create_tag(
'fieldset',
$fieldsetAttributes,
$content['before'] . $table . $content['after']
);
} }
/** /**
@ -495,27 +577,44 @@ class GridField extends FormField {
$classes[] = 'last'; $classes[] = 'last';
} }
$classes[] = ($index % 2) ? 'even' : 'odd'; if($index % 2) {
$classes[] = 'even';
} else {
$classes[] = 'odd';
}
return $classes; return $classes;
} }
/**
* @param array $properties
*
* @return string
*/
public function Field($properties = array()) { public function Field($properties = array()) {
return $this->FieldHolder($properties); return $this->FieldHolder($properties);
} }
/**
* {@inheritdoc}
*/
public function getAttributes() { public function getAttributes() {
return array_merge(parent::getAttributes(), array('data-url' => $this->Link())); return array_merge(
parent::getAttributes(),
array(
'data-url' => $this->Link(),
)
);
} }
/** /**
* Get the columns of this GridField, they are provided by attached GridField_ColumnProvider * Get the columns of this GridField, they are provided by attached GridField_ColumnProvider.
* *
* @return array * @return array
*/ */
public function getColumns() { public function getColumns() {
// Get column list
$columns = array(); $columns = array();
foreach($this->getComponents() as $item) { foreach($this->getComponents() as $item) {
if($item instanceof GridField_ColumnProvider) { if($item instanceof GridField_ColumnProvider) {
$item->augmentColumns($this, $columns); $item->augmentColumns($this, $columns);
@ -526,28 +625,36 @@ class GridField extends FormField {
} }
/** /**
* Get the value from a column * Get the value from a column.
* *
* @param DataObject $record * @param DataObject $record
* @param string $column * @param string $column
* *
* @return string * @return string
*
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
public function getColumnContent($record, $column) { public function getColumnContent($record, $column) {
// Build the column dispatch
if(!$this->columnDispatch) { if(!$this->columnDispatch) {
$this->buildColumnDispatch(); $this->buildColumnDispatch();
} }
if(!empty($this->columnDispatch[$column])) { if(!empty($this->columnDispatch[$column])) {
$content = ""; $content = '';
foreach($this->columnDispatch[$column] as $handler) { foreach($this->columnDispatch[$column] as $handler) {
/**
* @var GridField_ColumnProvider $handler
*/
$content .= $handler->getColumnContent($this, $record, $column); $content .= $handler->getColumnContent($this, $record, $column);
} }
return $content; return $content;
} else { } else {
throw new InvalidArgumentException("Bad column '$column'"); throw new InvalidArgumentException(sprintf(
'Bad column "%s"',
$column
));
} }
} }
@ -567,111 +674,139 @@ class GridField extends FormField {
/** /**
* Get the value of a named field on the given record. * Get the value of a named field on the given record.
* Use of this method ensures that any special rules around the data for this gridfield are followed. *
* Use of this method ensures that any special rules around the data for this gridfield are
* followed.
*
* @param DataObject $record
* @param string $fieldName
*
* @return mixed
*/ */
public function getDataFieldValue($record, $fieldName) { public function getDataFieldValue($record, $fieldName) {
// Custom callbacks
if(isset($this->customDataFields[$fieldName])) { if(isset($this->customDataFields[$fieldName])) {
$callback = $this->customDataFields[$fieldName]; $callback = $this->customDataFields[$fieldName];
return $callback($record); return $callback($record);
} }
// Default implementation
if($record->hasMethod('relField')) { if($record->hasMethod('relField')) {
return $record->relField($fieldName); return $record->relField($fieldName);
} elseif($record->hasMethod($fieldName)) {
return $record->$fieldName();
} else {
return $record->$fieldName;
} }
if($record->hasMethod($fieldName)) {
return $record->$fieldName();
}
return $record->$fieldName;
} }
/** /**
* Get extra columns attributes used as HTML attributes * Get extra columns attributes used as HTML attributes.
* *
* @param DataObject $record * @param DataObject $record
* @param string $column * @param string $column
* *
* @return array * @return array
*
* @throws LogicException * @throws LogicException
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
public function getColumnAttributes($record, $column) { public function getColumnAttributes($record, $column) {
// Build the column dispatch
if(!$this->columnDispatch) { if(!$this->columnDispatch) {
$this->buildColumnDispatch(); $this->buildColumnDispatch();
} }
if(!empty($this->columnDispatch[$column])) { if(!empty($this->columnDispatch[$column])) {
$attrs = array(); $attributes = array();
foreach($this->columnDispatch[$column] as $handler) { foreach($this->columnDispatch[$column] as $handler) {
$column_attrs = $handler->getColumnAttributes($this, $record, $column); /**
* @var GridField_ColumnProvider $handler
*/
$columnAttributes = $handler->getColumnAttributes($this, $record, $column);
if(is_array($column_attrs)) { if(is_array($columnAttributes)) {
$attrs = array_merge($attrs, $column_attrs); $attributes = array_merge($attributes, $columnAttributes);
} elseif($column_attrs) { continue;
$methodSignature = get_class($handler) . "::getColumnAttributes()";
throw new LogicException("Non-array response from $methodSignature.");
} }
throw new LogicException(sprintf(
'Non-array response from %s::getColumnAttributes().',
get_class($handler)
));
} }
return $attrs; return $attributes;
} else {
throw new InvalidArgumentException("Bad column '$column'");
} }
throw new InvalidArgumentException(sprintf(
'Bad column "%s"',
$column
));
} }
/** /**
* Get metadata for a column, example array('Title'=>'Email address') * Get metadata for a column.
*
* @example "array('Title'=>'Email address')"
* *
* @param string $column * @param string $column
* *
* @return array * @return array
*
* @throws LogicException * @throws LogicException
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
public function getColumnMetadata($column) { public function getColumnMetadata($column) {
// Build the column dispatch
if(!$this->columnDispatch) { if(!$this->columnDispatch) {
$this->buildColumnDispatch(); $this->buildColumnDispatch();
} }
if(!empty($this->columnDispatch[$column])) { if(!empty($this->columnDispatch[$column])) {
$metadata = array(); $metaData = array();
foreach($this->columnDispatch[$column] as $handler) { foreach($this->columnDispatch[$column] as $handler) {
$column_metadata = $handler->getColumnMetadata($this, $column); /**
* @var GridField_ColumnProvider $handler
*/
$columnMetaData = $handler->getColumnMetadata($this, $column);
if(is_array($column_metadata)) { if(is_array($columnMetaData)) {
$metadata = array_merge($metadata, $column_metadata); $metaData = array_merge($metaData, $columnMetaData);
} else { continue;
$methodSignature = get_class($handler) . "::getColumnMetadata()";
throw new LogicException("Non-array response from $methodSignature.");
} }
throw new LogicException(sprintf(
'Non-array response from %s::getColumnMetadata().',
get_class($handler)
));
} }
return $metadata; return $metaData;
} }
throw new InvalidArgumentException("Bad column '$column'");
throw new InvalidArgumentException(sprintf(
'Bad column "%s"',
$column
));
} }
/** /**
* Return how many columns the grid will have * Return how many columns the grid will have.
* *
* @return int * @return int
*/ */
public function getColumnCount() { public function getColumnCount() {
// Build the column dispatch if(!$this->columnDispatch) {
if(!$this->columnDispatch) $this->buildColumnDispatch(); $this->buildColumnDispatch();
}
return count($this->columnDispatch); return count($this->columnDispatch);
} }
/** /**
* Build an columnDispatch that maps a GridField_ColumnProvider to a column * Build an columnDispatch that maps a GridField_ColumnProvider to a column for reference later.
* for reference later
*
*/ */
protected function buildColumnDispatch() { protected function buildColumnDispatch() {
$this->columnDispatch = array(); $this->columnDispatch = array();
@ -691,140 +826,172 @@ class GridField extends FormField {
* This is the action that gets executed when a GridField_AlterAction gets clicked. * This is the action that gets executed when a GridField_AlterAction gets clicked.
* *
* @param array $data * @param array $data
* @param Form $form
* @param SS_HTTPRequest $request
* *
* @return string * @return string
*/ */
public function gridFieldAlterAction($data, $form, SS_HTTPRequest $request) { public function gridFieldAlterAction($data, $form, SS_HTTPRequest $request) {
$html = '';
$data = $request->requestVars(); $data = $request->requestVars();
$name = $this->getName(); $name = $this->getName();
$fieldData = isset($data[$name]) ? $data[$name] : null;
// Update state from client $fieldData = null;
if(isset($data[$name])) {
$fieldData = $data[$name];
}
$state = $this->getState(false); $state = $this->getState(false);
if(isset($fieldData['GridState'])) { if(isset($fieldData['GridState'])) {
$state->setValue($fieldData['GridState']); $state->setValue($fieldData['GridState']);
} }
// Try to execute alter action foreach($data as $dataKey => $dataValue) {
foreach($data as $k => $v) { if(preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $dataKey, $matches)) {
if(preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $k, $matches)) { $stateChange = Session::get($matches[1]);
$id = $matches[1];
$stateChange = Session::get($id);
$actionName = $stateChange['actionName']; $actionName = $stateChange['actionName'];
$args = isset($stateChange['args']) ? $stateChange['args'] : array(); $arguments = array();
$html = $this->handleAlterAction($actionName, $args, $data);
// A field can optionally return its own HTML if(isset($stateChange['args'])) {
if($html) return $html; $arguments = $stateChange['args'];
};
$html = $this->handleAlterAction($actionName, $arguments, $data);
if($html) {
return $html;
}
} }
} }
switch($request->getHeader('X-Pjax')) { if($request->getHeader('X-Pjax') === 'CurrentField') {
case 'CurrentField': return $this->FieldHolder();
return $this->FieldHolder();
break;
case 'CurrentForm':
return $form->forTemplate();
break;
default:
return $form->forTemplate();
break;
} }
return $form->forTemplate();
} }
/** /**
* Pass an action on the first GridField_ActionProvider that matches the $actionName * Pass an action on the first GridField_ActionProvider that matches the $actionName.
* *
* @param string $actionName * @param string $actionName
* @param mixed $args * @param mixed $arguments
* @param array $data - send data from a form * @param array $data
* *
* @return mixed * @return mixed
*
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
public function handleAlterAction($actionName, $args, $data) { public function handleAlterAction($actionName, $arguments, $data) {
$actionName = strtolower($actionName); $actionName = strtolower($actionName);
foreach($this->getComponents() as $component) {
if(!($component instanceof GridField_ActionProvider)) {
continue;
}
if(in_array($actionName, array_map('strtolower', (array) $component->getActions($this)))) { foreach($this->getComponents() as $component) {
return $component->handleAction($this, $actionName, $args, $data); if($component instanceof GridField_ActionProvider) {
$actions = array_map('strtolower', (array) $component->getActions($this));
if(in_array($actionName, $actions)) {
return $component->handleAction($this, $actionName, $arguments, $data);
}
} }
} }
throw new InvalidArgumentException("Can't handle action '$actionName'");
throw new InvalidArgumentException(sprintf(
'Can\'t handle action "%s"',
$actionName
));
} }
/** /**
* Custom request handler that will check component handlers before proceeding to the default implementation. * Custom request handler that will check component handlers before proceeding to the default
* implementation.
* *
* @todo There is too much code copied from RequestHandler here. * @todo copy less code from RequestHandler.
*
* @param SS_HTTPRequest $request
* @param DataModel $model
*
* @return array|RequestHandler|SS_HTTPResponse|string|void
*
* @throws SS_HTTPResponse_Exception
*/ */
public function handleRequest(SS_HTTPRequest $request, DataModel $model) { public function handleRequest(SS_HTTPRequest $request, DataModel $model) {
if($this->brokenOnConstruct) { if($this->brokenOnConstruct) {
user_error("parent::__construct() needs to be called on {$handlerClass}::__construct()", E_USER_WARNING); user_error(
sprintf(
"parent::__construct() needs to be called on %s::__construct()",
__CLASS__
),
E_USER_WARNING
);
} }
$this->setRequest($request); $this->setRequest($request);
$this->setDataModel($model); $this->setDataModel($model);
$fieldData = $this->getRequest()->requestVar($this->getName()); $fieldData = $this->getRequest()->requestVar($this->getName());
if($fieldData && isset($fieldData['GridState'])) $this->getState(false)->setValue($fieldData['GridState']);
if($fieldData && isset($fieldData['GridState'])) {
$this->getState(false)->setValue($fieldData['GridState']);
}
foreach($this->getComponents() as $component) { foreach($this->getComponents() as $component) {
if(!($component instanceof GridField_URLHandler)) { if($component instanceof GridField_URLHandler && $urlHandlers = $component->getURLHandlers($this)) {
continue; foreach($urlHandlers as $rule => $action) {
} if($params = $request->match($rule, true)) {
// Actions can reference URL parameters.
// e.g. '$Action/$ID/$OtherID' → '$Action'
$urlHandlers = $component->getURLHandlers($this); if($action[0] == '$') {
$action = $params[substr($action, 1)];
if($urlHandlers) foreach($urlHandlers as $rule => $action) {
if($params = $request->match($rule, true)) {
// Actions can reference URL parameters, eg, '$Action/$ID/$OtherID' => '$Action',
if($action[0] == '$') $action = $params[substr($action, 1)];
if(!method_exists($component, 'checkAccessAction') || $component->checkAccessAction($action)) {
if(!$action) {
$action = "index";
} else if(!is_string($action)) {
throw new LogicException("Non-string method name: " . var_export($action, true));
} }
try { if(!method_exists($component, 'checkAccessAction') || $component->checkAccessAction($action)) {
$result = $component->$action($this, $request); if(!$action) {
} catch(SS_HTTPResponse_Exception $responseException) { $action = "index";
$result = $responseException->getResponse();
}
if($result instanceof SS_HTTPResponse && $result->isError()) {
return $result;
}
if($this !== $result && !$request->isEmptyPattern($rule) && is_object($result)
&& $result instanceof RequestHandler
) {
$returnValue = $result->handleRequest($request, $model);
if(is_array($returnValue)) {
throw new LogicException("GridField_URLHandler handlers can't return arrays");
} }
return $returnValue; if(!is_string($action)) {
throw new LogicException(sprintf(
'Non-string method name: %s',
var_export($action, true)
));
}
// If we return some other data, and all the URL is parsed, then return that try {
} else if($request->allParsed()) { $result = $component->$action($this, $request);
return $result; } catch(SS_HTTPResponse_Exception $responseException) {
$result = $responseException->getResponse();
}
// But if we have more content on the URL and we don't know what to do with it, return an error if($result instanceof SS_HTTPResponse && $result->isError()) {
} else { return $result;
return $this->httpError(404, }
"I can't handle sub-URLs of a " . get_class($result) . " object.");
if($this !== $result && !$request->isEmptyPattern($rule) && is_object($result) && $result instanceof RequestHandler) {
$returnValue = $result->handleRequest($request, $model);
if(is_array($returnValue)) {
throw new LogicException(
'GridField_URLHandler handlers can\'t return arrays'
);
}
return $returnValue;
}
if($request->allParsed()) {
return $result;
}
return $this->httpError(
404,
sprintf(
'I can\'t handle sub-URLs of a %s object.',
get_class($result)
)
);
} }
} }
} }
@ -834,6 +1001,9 @@ class GridField extends FormField {
return parent::handleRequest($request, $model); return parent::handleRequest($request, $model);
} }
/**
* {@inheritdoc}
*/
public function saveInto(DataObjectInterface $record) { public function saveInto(DataObjectInterface $record) {
foreach($this->getComponents() as $component) { foreach($this->getComponents() as $component) {
if($component instanceof GridField_SaveHandler) { if($component instanceof GridField_SaveHandler) {
@ -842,18 +1012,61 @@ class GridField extends FormField {
} }
} }
/**
* @param array $content
*
* @return string
*/
protected function getOptionalTableHeader(array $content) {
if($content['header']) {
return FormField::create_tag(
'thead', array(), $content['header']
);
}
return '';
}
/**
* @param array $content
*
* @return string
*/
protected function getOptionalTableBody(array $content) {
if($content['body']) {
return FormField::create_tag(
'tbody', array('class' => 'ss-gridfield-items'), $content['body']
);
}
return '';
}
/**
* @param $content
*
* @return string
*/
protected function getOptionalTableFooter($content) {
if($content['footer']) {
return FormField::create_tag(
'tfoot', array(), $content['footer']
);
}
return '';
}
} }
/** /**
* This class is the base class when you want to have an action that alters * This class is the base class when you want to have an action that alters the state of the
* the state of the {@link GridField}, rendered as a button element. * {@link GridField}, rendered as a button element.
* *
* @package forms * @package forms
* @subpackage fields-gridfield * @subpackage fields-gridfield
*/ */
class GridField_FormAction extends FormAction { class GridField_FormAction extends FormAction {
/** /**
* @var GridField * @var GridField
*/ */
@ -882,7 +1095,7 @@ class GridField_FormAction extends FormAction {
/** /**
* @param GridField $gridField * @param GridField $gridField
* @param string $name * @param string $name
* @param string $label * @param string $title
* @param string $actionName * @param string $actionName
* @param array $args * @param array $args
*/ */
@ -895,19 +1108,20 @@ class GridField_FormAction extends FormAction {
} }
/** /**
* urlencode encodes less characters in percent form than we need - we * Encode all non-word characters.
* need everything that isn't a \w.
* *
* @param string $val * @param string $value
*
* @return string
*/ */
public function nameEncode($val) { public function nameEncode($value) {
return preg_replace_callback('/[^\w]/', array($this, '_nameEncode'), $val); return (string) preg_replace_callback('/[^\w]/', array($this, '_nameEncode'), $value);
} }
/** /**
* The callback for nameEncode * @param array $match
* *
* @param string $val * @return string
*/ */
public function _nameEncode($match) { public function _nameEncode($match) {
return '%' . dechex(ord($match[0])); return '%' . dechex(ord($match[0]));
@ -941,9 +1155,7 @@ class GridField_FormAction extends FormAction {
} }
/** /**
* Calculate the name of the gridfield relative to the Form * Calculate the name of the gridfield relative to the form.
*
* @param GridField $base
* *
* @return string * @return string
*/ */

View File

@ -18,19 +18,17 @@ class DirectorTest extends SapphireTest {
public function setUp() { public function setUp() {
parent::setUp(); parent::setUp();
// Required for testRequestFilterInDirectorTest
Injector::nest();
// Hold the original request URI once so it doesn't get overwritten // Hold the original request URI once so it doesn't get overwritten
if(!self::$originalRequestURI) { if(!self::$originalRequestURI) {
self::$originalRequestURI = $_SERVER['REQUEST_URI']; self::$originalRequestURI = $_SERVER['REQUEST_URI'];
} }
$_SERVER['REQUEST_URI'] = 'http://www.mysite.com'; $_SERVER['REQUEST_URI'] = 'http://www.mysite.com';
$this->originalGet = $_GET; $this->originalGet = $_GET;
$this->originalSession = $_SESSION; $this->originalSession = $_SESSION;
$_SESSION = array(); $_SESSION = array();
Config::inst()->update('Director', 'rules', array( Config::inst()->update('Director', 'rules', array(
'DirectorTestRule/$Action/$ID/$OtherID' => 'DirectorTestRequest_Controller', 'DirectorTestRule/$Action/$ID/$OtherID' => 'DirectorTestRequest_Controller',
'en-nz/$Action/$ID/$OtherID' => array( 'en-nz/$Action/$ID/$OtherID' => array(
@ -53,9 +51,6 @@ class DirectorTest extends SapphireTest {
public function tearDown() { public function tearDown() {
// TODO Remove director rule, currently API doesnt allow this // TODO Remove director rule, currently API doesnt allow this
// Remove base URL override (setting to false reverts to default behaviour)
Config::inst()->update('Director', 'alternate_base_url', false);
$_GET = $this->originalGet; $_GET = $this->originalGet;
$_SESSION = $this->originalSession; $_SESSION = $this->originalSession;
@ -68,7 +63,6 @@ class DirectorTest extends SapphireTest {
} }
} }
Injector::unnest();
parent::tearDown(); parent::tearDown();
} }
@ -140,7 +134,7 @@ class DirectorTest extends SapphireTest {
public function testAlternativeBaseURL() { public function testAlternativeBaseURL() {
// Get original protocol and hostname // Get original protocol and hostname
$rootURL = Director::protocolAndHost(); $rootURL = Director::protocolAndHost();
// relative base URLs - you should end them in a / // relative base URLs - you should end them in a /
Config::inst()->update('Director', 'alternate_base_url', '/relativebase/'); Config::inst()->update('Director', 'alternate_base_url', '/relativebase/');
$_SERVER['REQUEST_URI'] = "$rootURL/relativebase/sub-page/"; $_SERVER['REQUEST_URI'] = "$rootURL/relativebase/sub-page/";

View File

@ -189,8 +189,7 @@ class ConfigTest extends SapphireTest {
// But it won't affect subclasses - this is *uninherited* static // But it won't affect subclasses - this is *uninherited* static
$this->assertNotContains('test_2b', $this->assertNotContains('test_2b',
Config::inst()->get('ConfigStaticTest_Third', 'first', Config::UNINHERITED)); Config::inst()->get('ConfigStaticTest_Third', 'first', Config::UNINHERITED));
$this->assertNotContains('test_2b', $this->assertNull(Config::inst()->get('ConfigStaticTest_Fourth', 'first', Config::UNINHERITED));
Config::inst()->get('ConfigStaticTest_Fourth', 'first', Config::UNINHERITED));
// Subclasses that don't have the static explicitly defined should allow definition, also // Subclasses that don't have the static explicitly defined should allow definition, also
// This also checks that set can be called after the first uninherited get() // This also checks that set can be called after the first uninherited get()

View File

@ -391,7 +391,7 @@ class ConfigManifestTest extends SapphireTest {
public function testEnvironmentRules() { public function testEnvironmentRules() {
foreach (array('dev', 'test', 'live') as $env) { foreach (array('dev', 'test', 'live') as $env) {
Config::inst()->nest(); Config::nest();
Config::inst()->update('Director', 'environment_type', $env); Config::inst()->update('Director', 'environment_type', $env);
$config = $this->getConfigFixtureValue('Environment'); $config = $this->getConfigFixtureValue('Environment');
@ -403,13 +403,11 @@ class ConfigManifestTest extends SapphireTest {
); );
} }
Config::inst()->unnest(); Config::unnest();
} }
} }
public function testDynamicEnvironmentRules() { public function testDynamicEnvironmentRules() {
Config::inst()->nest();
// First, make sure environment_type is live // First, make sure environment_type is live
Config::inst()->update('Director', 'environment_type', 'live'); Config::inst()->update('Director', 'environment_type', 'live');
$this->assertEquals('live', Config::inst()->get('Director', 'environment_type')); $this->assertEquals('live', Config::inst()->get('Director', 'environment_type'));
@ -423,8 +421,6 @@ class ConfigManifestTest extends SapphireTest {
// And that the dynamic rule was calculated correctly // And that the dynamic rule was calculated correctly
$this->assertEquals('dev', Config::inst()->get('ConfigManifestTest', 'DynamicEnvironment')); $this->assertEquals('dev', Config::inst()->get('ConfigManifestTest', 'DynamicEnvironment'));
Config::inst()->unnest();
} }
public function testMultipleRules() { public function testMultipleRules() {

View File

@ -1,17 +1,17 @@
<?php <?php
/** /**
* Note: the running of this test is handled by the thing it's testing (DevelopmentAdmin controller). * Note: the running of this test is handled by the thing it's testing (DevelopmentAdmin controller).
* *
* @package framework * @package framework
* @package tests * @package tests
*/ */
class DevAdminControllerTest extends FunctionalTest { class DevAdminControllerTest extends FunctionalTest {
public function setUp(){ public function setUp(){
parent::setUp(); parent::setUp();
Config::nest()->update('DevelopmentAdmin', 'registered_controllers', array( Config::inst()->update('DevelopmentAdmin', 'registered_controllers', array(
'x1' => array( 'x1' => array(
'controller' => 'DevAdminControllerTest_Controller1', 'controller' => 'DevAdminControllerTest_Controller1',
'links' => array( 'links' => array(
@ -27,45 +27,40 @@ class DevAdminControllerTest extends FunctionalTest {
) )
)); ));
} }
public function tearDown(){
parent::tearDown();
Config::unnest();
}
public function testGoodRegisteredControllerOutput(){ public function testGoodRegisteredControllerOutput(){
// Check for the controller running from the registered url above // Check for the controller running from the registered url above
// (we use contains rather than equals because sometimes you get Warning: You probably want to define an entry in $_FILE_TO_URL_MAPPING) // (we use contains rather than equals because sometimes you get Warning: You probably want to define an entry in $_FILE_TO_URL_MAPPING)
$this->assertContains(DevAdminControllerTest_Controller1::OK_MSG, $this->getCapture('/dev/x1')); $this->assertContains(DevAdminControllerTest_Controller1::OK_MSG, $this->getCapture('/dev/x1'));
$this->assertContains(DevAdminControllerTest_Controller1::OK_MSG, $this->getCapture('/dev/x1/y1')); $this->assertContains(DevAdminControllerTest_Controller1::OK_MSG, $this->getCapture('/dev/x1/y1'));
} }
public function testGoodRegisteredControllerStatus(){ public function testGoodRegisteredControllerStatus(){
// Check response code is 200/OK // Check response code is 200/OK
$this->assertEquals(false, $this->getAndCheckForError('/dev/x1')); $this->assertEquals(false, $this->getAndCheckForError('/dev/x1'));
$this->assertEquals(false, $this->getAndCheckForError('/dev/x1/y1')); $this->assertEquals(false, $this->getAndCheckForError('/dev/x1/y1'));
// Check response code is 500/ some sort of error // Check response code is 500/ some sort of error
$this->assertEquals(true, $this->getAndCheckForError('/dev/x2')); $this->assertEquals(true, $this->getAndCheckForError('/dev/x2'));
} }
protected function getCapture($url){ protected function getCapture($url){
$this->logInWithPermission('ADMIN'); $this->logInWithPermission('ADMIN');
ob_start(); ob_start();
$this->get($url); $this->get($url);
$r = ob_get_contents(); $r = ob_get_contents();
ob_end_clean(); ob_end_clean();
return $r; return $r;
} }
protected function getAndCheckForError($url){ protected function getAndCheckForError($url){
$this->logInWithPermission('ADMIN'); $this->logInWithPermission('ADMIN');
if(Director::is_cli()){ if(Director::is_cli()){
// when in CLI the admin controller throws exceptions // when in CLI the admin controller throws exceptions
ob_start(); ob_start();
@ -75,10 +70,10 @@ class DevAdminControllerTest extends FunctionalTest {
ob_end_clean(); ob_end_clean();
return true; return true;
} }
ob_end_clean(); ob_end_clean();
return false; return false;
}else{ }else{
// when in http the admin controller sets a response header // when in http the admin controller sets a response header
ob_start(); ob_start();
@ -87,30 +82,30 @@ class DevAdminControllerTest extends FunctionalTest {
return $resp->isError(); return $resp->isError();
} }
} }
} }
class DevAdminControllerTest_Controller1 extends Controller { class DevAdminControllerTest_Controller1 extends Controller {
const OK_MSG = 'DevAdminControllerTest_Controller1 TEST OK'; const OK_MSG = 'DevAdminControllerTest_Controller1 TEST OK';
private static $url_handlers = array( private static $url_handlers = array(
'' => 'index', '' => 'index',
'y1' => 'y1Action' 'y1' => 'y1Action'
); );
private static $allowed_actions = array( private static $allowed_actions = array(
'index', 'index',
'y1Action', 'y1Action',
); );
public function index(){ public function index(){
echo self::OK_MSG; echo self::OK_MSG;
} }
public function y1Action(){ public function y1Action(){
echo self::OK_MSG; echo self::OK_MSG;
} }
} }

View File

@ -214,6 +214,24 @@ class DatetimeFieldTest extends SapphireTest {
); );
} }
public function testGetName() {
$field = new DatetimeField('Datetime');
$this->assertEquals('Datetime', $field->getName());
$this->assertEquals('Datetime[date]', $field->getDateField()->getName());
$this->assertEquals('Datetime[time]', $field->getTimeField()->getName());
$this->assertEquals('Datetime[timezone]', $field->getTimezoneField()->getName());
}
public function testSetName() {
$field = new DatetimeField('Datetime', 'Datetime');
$field->setName('CustomDatetime');
$this->assertEquals('CustomDatetime', $field->getName());
$this->assertEquals('CustomDatetime[date]', $field->getDateField()->getName());
$this->assertEquals('CustomDatetime[time]', $field->getTimeField()->getName());
$this->assertEquals('CustomDatetime[timezone]', $field->getTimezoneField()->getName());
}
protected function getMockForm() { protected function getMockForm() {
return new Form( return new Form(
new Controller(), new Controller(),

View File

@ -91,6 +91,14 @@ class TimeFieldTest extends SapphireTest {
$f = new TimeField('Time', 'Time'); $f = new TimeField('Time', 'Time');
$f->setValue('23:59:38'); $f->setValue('23:59:38');
$this->assertEquals($f->dataValue(), '23:59:38'); $this->assertEquals($f->dataValue(), '23:59:38');
$f = new TimeField('Time', 'Time');
$f->setValue('12:00 am');
$this->assertEquals($f->dataValue(), '00:00:00');
$f = new TimeField('Time', 'Time');
$f->setValue('12:00:01 am');
$this->assertEquals($f->dataValue(), '00:00:01');
} }
public function testOverrideWithNull() { public function testOverrideWithNull() {

View File

@ -46,7 +46,6 @@ class DataObjectSchemaGenerationTest extends SapphireTest {
// Table will have been initially created by the $extraDataObjects setting // Table will have been initially created by the $extraDataObjects setting
// Let's insert a new field here // Let's insert a new field here
Config::nest();
Config::inst()->update('DataObjectSchemaGenerationTest_DO', 'db', array( Config::inst()->update('DataObjectSchemaGenerationTest_DO', 'db', array(
'SecretField' => 'Varchar(100)' 'SecretField' => 'Varchar(100)'
)); ));
@ -59,9 +58,6 @@ class DataObjectSchemaGenerationTest extends SapphireTest {
$schema->cancelSchemaUpdate(); $schema->cancelSchemaUpdate();
$test->assertTrue($needsUpdating); $test->assertTrue($needsUpdating);
}); });
// Restore db configuration
Config::unnest();
} }
/** /**
@ -84,7 +80,6 @@ class DataObjectSchemaGenerationTest extends SapphireTest {
}); });
// Test with alternate index format, although these indexes are the same // Test with alternate index format, although these indexes are the same
Config::nest();
Config::inst()->remove('DataObjectSchemaGenerationTest_IndexDO', 'indexes'); Config::inst()->remove('DataObjectSchemaGenerationTest_IndexDO', 'indexes');
Config::inst()->update('DataObjectSchemaGenerationTest_IndexDO', 'indexes', Config::inst()->update('DataObjectSchemaGenerationTest_IndexDO', 'indexes',
Config::inst()->get('DataObjectSchemaGenerationTest_IndexDO', 'indexes_alt') Config::inst()->get('DataObjectSchemaGenerationTest_IndexDO', 'indexes_alt')
@ -98,9 +93,6 @@ class DataObjectSchemaGenerationTest extends SapphireTest {
$schema->cancelSchemaUpdate(); $schema->cancelSchemaUpdate();
$test->assertFalse($needsUpdating); $test->assertFalse($needsUpdating);
}); });
// Restore old index format
Config::unnest();
} }
/** /**
@ -114,7 +106,6 @@ class DataObjectSchemaGenerationTest extends SapphireTest {
// Table will have been initially created by the $extraDataObjects setting // Table will have been initially created by the $extraDataObjects setting
// Update the SearchFields index here // Update the SearchFields index here
Config::nest();
Config::inst()->update('DataObjectSchemaGenerationTest_IndexDO', 'indexes', array( Config::inst()->update('DataObjectSchemaGenerationTest_IndexDO', 'indexes', array(
'SearchFields' => array( 'SearchFields' => array(
'value' => 'Title' 'value' => 'Title'
@ -129,9 +120,6 @@ class DataObjectSchemaGenerationTest extends SapphireTest {
$schema->cancelSchemaUpdate(); $schema->cancelSchemaUpdate();
$test->assertTrue($needsUpdating); $test->assertTrue($needsUpdating);
}); });
// Restore old indexes
Config::unnest();
} }
/** /**
@ -218,4 +206,4 @@ class DataObjectSchemaGenerationTest_IndexDO extends DataObjectSchemaGenerationT
), ),
'SearchFields' => 'fulltext ("Title","Content")' 'SearchFields' => 'fulltext ("Title","Content")'
); );
} }

View File

@ -1,16 +1,6 @@
<?php <?php
class OembedTest extends SapphireTest { class OembedTest extends SapphireTest {
public function setUp() {
parent::setUp();
Config::nest();
}
public function tearDown() {
Config::unnest();
parent::tearDown();
}
public function testGetOembedFromUrl() { public function testGetOembedFromUrl() {
Config::inst()->update('Oembed', 'providers', array( Config::inst()->update('Oembed', 'providers', array(
'http://*.silverstripe.com/watch*'=>'http://www.silverstripe.com/oembed/' 'http://*.silverstripe.com/watch*'=>'http://www.silverstripe.com/oembed/'

View File

@ -14,17 +14,11 @@ class BasicAuthTest extends FunctionalTest {
parent::setUp(); parent::setUp();
// Fixtures assume Email is the field used to identify the log in identity // Fixtures assume Email is the field used to identify the log in identity
Config::nest();
Member::config()->unique_identifier_field = 'Email'; Member::config()->unique_identifier_field = 'Email';
Security::$force_database_is_ready = true; // Prevents Member test subclasses breaking ready test Security::$force_database_is_ready = true; // Prevents Member test subclasses breaking ready test
Member::config()->lock_out_after_incorrect_logins = 10; Member::config()->lock_out_after_incorrect_logins = 10;
} }
public function tearDown() {
Config::unnest();
parent::tearDown();
}
public function testBasicAuthEnabledWithoutLogin() { public function testBasicAuthEnabledWithoutLogin() {
$origUser = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : null; $origUser = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : null;
$origPw = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : null; $origPw = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : null;