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 .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 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 #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 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.cms-batch-actions { float: left; }
.cms-content-batchactions.inactive form { display: none; }

View File

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

View File

@ -5,12 +5,12 @@ require_once 'TestRunner.php';
* 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
* to work with.
*
*
* @package framework
* @subpackage testing
*/
class SapphireTest extends PHPUnit_Framework_TestCase {
/** @config */
private static $dependencies = array(
'fixtureFactory' => '%$FixtureFactory',
@ -21,7 +21,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
* If passed as an array, multiple fixture files will be loaded.
* Please note that you won't be able to refer with "=>" notation
* between the fixtures, they act independent of each other.
*
*
* @var string|array
*/
protected static $fixture_file = null;
@ -30,19 +30,19 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
* @var FixtureFactory
*/
protected $fixtureFactory;
/**
* @var bool Set whether to include this test in the TestRunner or to skip this.
*/
protected $skipTest = false;
/**
* @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.
*/
protected $usesDatabase = null;
protected $originalMailer;
protected $originalMemberPasswordValidator;
protected $originalRequirements;
@ -50,33 +50,33 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
protected $originalTheme;
protected $originalNestedURLsState;
protected $originalMemoryLimit;
protected $mailer;
/**
* Pointer to the manifest that isn't a test manifest
*/
protected static $regular_manifest;
/**
* @var boolean
*/
protected static $is_running_test = false;
protected static $test_class_manifest;
/**
* By default, setUp() does not require default records. Pass
* class names in here, and the require/augment default records
* function will be called on them.
*/
protected $requireDefaultRecordsFrom = array();
/**
* 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.
*
*
* 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.
*/
@ -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
* 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
* of required extensions on that class.
*
*
* Example:
* <code>
* array("MyTreeDataObject" => array("Versioned", "Hierarchy"))
@ -97,35 +97,35 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
*/
protected $requiredExtensions = array(
);
/**
* 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.
* Set it to an array of DataObject subclass names.
*/
protected $extraDataObjects = array();
/**
* We need to disabling backing up of globals to avoid overriding
* the few globals SilverStripe relies on, like $lang for the i18n subsystem.
*
*
* @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html
*/
protected $backupGlobals = FALSE;
/**
/**
* Helper arrays for illegalExtensions/requiredExtensions code
*/
private $extensionsToReapply = array(), $extensionsToRemove = array();
/**
* Determines if unit tests are currently run (via {@link TestRunner}).
* This is used as a cheap replacement for fully mockable state
* in certain contiditions (e.g. access checks).
* Caution: When set to FALSE, certain controllers might bypass
* access checks, so this is a very security sensitive setting.
*
*
* @return boolean
*/
public static function is_running_test() {
@ -133,7 +133,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
}
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;
}
/**
* @var array $fixtures Array of {@link YamlFixture} instances
* @deprecated 3.1 Use $fixtureFactory instad
*/
protected $fixtures = array();
protected $model;
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.
if(get_class($this) == "SapphireTest") $this->skipTest = true;
if($this->skipTest) {
$this->markTestSkipped(sprintf(
'Skipping %s ', get_class($this)
));
return;
}
// Mark test as being run
$this->originalIsRunningTest = self::$is_running_test;
self::$is_running_test = true;
@ -179,16 +190,16 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
i18n::set_locale(i18n::default_locale());
i18n::config()->date_format = null;
i18n::config()->time_format = null;
// Set default timezone consistently to avoid NZ-specific dependencies
date_default_timezone_set('UTC');
// Remove password validation
$this->originalMemberPasswordValidator = Member::password_validator();
$this->originalRequirements = Requirements::backend();
Member::set_password_validator(null);
Config::inst()->update('Cookie', 'report_errors', false);
if(class_exists('RootURLController')) RootURLController::reset();
if(class_exists('Translatable')) Translatable::reset();
Versioned::reset();
@ -212,7 +223,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$this->mailer = new TestMailer();
Email::set_mailer($this->mailer);
Config::inst()->remove('Email', 'send_all_emails_to');
// Todo: this could be a special test model
$this->model = DataModel::inst();
@ -227,9 +238,9 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
}
singleton('DataObject')->flushCache();
self::empty_temp_db();
foreach($this->requireDefaultRecordsFrom as $className) {
$instance = singleton($className);
if (method_exists($instance, 'requireDefaultRecords')) $instance->requireDefaultRecords();
@ -244,14 +255,14 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
foreach($fixtureFiles as $fixtureFilePath) {
// Support fixture paths relative to the test class, rather than relative to webroot
// String checking is faster than file_exists() calls.
$isRelativeToFile = (strpos('/', $fixtureFilePath) === false
$isRelativeToFile = (strpos('/', $fixtureFilePath) === false
|| preg_match('/^\.\./', $fixtureFilePath));
if($isRelativeToFile) {
$resolvedPath = realpath($pathForClass . '/' . $fixtureFilePath);
if($resolvedPath) $fixtureFilePath = $resolvedPath;
}
$fixture = Injector::inst()->create('YamlFixture', $fixtureFilePath);
$fixture->writeInto($this->getFixtureFactory());
$this->fixtures[] = $fixture;
@ -261,20 +272,20 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$i++;
}
}
$this->logInWithPermission("ADMIN");
}
// Preserve memory settings
$this->originalMemoryLimit = ini_get('memory_limit');
// turn off template debugging
Config::inst()->update('SSViewer', 'source_file_comments', false);
// Clear requirements
Requirements::clear();
}
/**
* Called once per test case ({@link SapphireTest} subclass).
* 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.
*/
public function setUpOnce() {
//nest config and injector for each suite so they are effectively sandboxed
Config::nest();
Injector::nest();
$isAltered = false;
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($isAltered || $this->extensionsToReapply || $this->extensionsToRemove || $this->extraDataObjects) {
if(!self::using_temp_db()) self::create_temp_db();
$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()
Injector::inst()->unregisterAllObjects();
// Set default timezone consistently to avoid NZ-specific dependencies
date_default_timezone_set('UTC');
}
/**
* tearDown method that's called once per test class rather once per test method.
*/
public function tearDownOnce() {
// If we have made changes to the extensions present, then migrate the database schema.
if($this->extensionsToReapply || $this->extensionsToRemove) {
// Remove extensions added for testing
foreach($this->extensionsToRemove as $class => $extensions) {
foreach($extensions as $extension) {
$class::remove_extension($extension);
}
}
//unnest injector / config now that the test suite is over
// this will reset all the extensions on the object too (see setUpOnce)
Injector::unnest();
Config::unnest();
// Reapply ones removed
foreach($this->extensionsToReapply as $class => $extensions) {
foreach($extensions as $extension) {
$class::add_extension($extension);
}
}
}
if($this->extensionsToReapply || $this->extensionsToRemove || $this->extraDataObjects) {
if(!empty($this->extensionsToReapply) || !empty($this->extensionsToRemove) || !empty($this->extraDataObjects)) {
$this->resetDBSchema();
}
}
/**
* @return FixtureFactory
*/
@ -366,10 +369,10 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$this->fixtureFactory = $factory;
return $this;
}
/**
* 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 $identifier The identifier string, as provided in your fixture file
* @return int
@ -415,12 +418,12 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
"Couldn't find object '%s' (class: %s)",
$identifier,
$className
), E_USER_ERROR);
), E_USER_ERROR);
}
return $obj;
}
/**
* Load a YAML fixture file into the database.
* 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());
$this->fixtures[] = $fixture;
}
/**
* Clear all fixtures which were previously loaded through
* {@link loadFixture()}
* {@link loadFixture()}
*/
public function clearFixtures() {
$this->getFixtureFactory()->clear();
}
/**
* Useful for writing unit tests without hardcoding folder structures.
*
*
* @return String Absolute path to current class.
*/
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));
return dirname($filename);
}
/**
* @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)));
return $path;
}
public function tearDown() {
// Preserve memory settings
ini_set('memory_limit', ($this->originalMemoryLimit) ? $this->originalMemoryLimit : -1);
@ -472,16 +475,16 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
Email::set_mailer($this->originalMailer);
$this->originalMailer = null;
}
$this->mailer = null;
$this->mailer = null;
// Restore password validation
if($this->originalMemberPasswordValidator) {
Member::set_password_validator($this->originalMemberPasswordValidator);
Member::set_password_validator($this->originalMemberPasswordValidator);
}
// Restore requirements
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
@ -490,15 +493,18 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
// Reset mocked datetime
SS_Datetime::clear_mock_now();
// 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.
$controller = Controller::has_curr() ? Controller::curr() : null;
if ( $controller && $controller->response && $controller->response->getHeader('Location') ) {
$controller->response->setStatusCode(200);
$controller->response->removeHeader('Location');
}
//unnest injector / config now that tests are over
Injector::unnest();
Config::unnest();
}
public static function assertContains(
@ -545,7 +551,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
public function findEmail($to, $from = null, $subject = null, $content = null) {
return $this->mailer->findEmail($to, $from, $subject, $content);
}
/**
* 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.
@ -577,7 +583,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
/**
* Assert that the given {@link SS_List} includes DataObjects matching the given 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
* either pass a single pattern or an array of patterns.
* @param $dataObjectSet The {@link SS_List} to test.
@ -585,19 +591,19 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
* Examples
* --------
* Check that $members includes an entry with Email = sam@example.com:
* $this->assertDOSContains(array('Email' => '...@example.com'), $members);
*
* Check that $members includes entries with Email = sam@example.com and with
* $this->assertDOSContains(array('Email' => '...@example.com'), $members);
*
* Check that $members includes entries with Email = sam@example.com and with
* Email = ingo@example.com:
* $this->assertDOSContains(array(
* array('Email' => '...@example.com'),
* array('Email' => 'i...@example.com'),
* ), $members);
* $this->assertDOSContains(array(
* array('Email' => '...@example.com'),
* array('Email' => 'i...@example.com'),
* ), $members);
*/
public function assertDOSContains($matches, $dataObjectSet) {
$extracted = array();
foreach($dataObjectSet as $item) $extracted[] = $item->toMap();
foreach($matches as $match) {
$matched = false;
foreach($extracted as $i => $item) {
@ -613,35 +619,35 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$this->assertTrue(
$matched,
"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)
);
}
}
}
/**
* 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.
*
*
* @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.
* @param $dataObjectSet The {@link SS_List} to test.
*
* 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:
* $this->assertDOSEquals(array(
* array('FirstName' =>'Sam', 'Surname' => 'Minnee'),
* array('FirstName' => 'Ingo', 'Surname' => 'Schommer'),
* ), $members);
* $this->assertDOSEquals(array(
* array('FirstName' =>'Sam', 'Surname' => 'Minnee'),
* array('FirstName' => 'Ingo', 'Surname' => 'Schommer'),
* ), $members);
*/
public function assertDOSEquals($matches, $dataObjectSet) {
if(!$dataObjectSet) return false;
$extracted = array();
foreach($dataObjectSet as $item) $extracted[] = $item->toMap();
foreach($matches as $match) {
$matched = false;
foreach($extracted as $i => $item) {
@ -657,11 +663,11 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$this->assertTrue(
$matched,
"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)
);
}
// If we have leftovers than the DOS has extra data that shouldn't be there
$this->assertTrue(
(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 "
. "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
* pairs.
*
*
* @param $match The pattern to match. The pattern is a map of key-value pairs.
* @param $dataObjectSet The {@link SS_List} to test.
*
* Example
* --------
* 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) {
$extracted = array();
@ -690,7 +696,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
foreach($extracted as $i => $item) {
$this->assertTrue(
$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)
);
}
@ -763,7 +769,6 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$this->assertNotContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
}
/**
* 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)
== strtolower(sprintf('%stmpdb', $prefix)));
}
public static function kill_temp_db() {
// Delete our temporary database
if(self::using_temp_db()) {
$dbConn = DB::get_conn();
$dbName = $dbConn->getSelectedDatabase();
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
foreach(ClassInfo::subclassesFor('DataExtension') as $class) {
$toCall = array($class, 'on_db_reset');
@ -811,15 +816,15 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
}
}
}
/**
* Remove all content from the temporary database.
*/
public static function empty_temp_db() {
if(self::using_temp_db()) {
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
$classes = array_merge(ClassInfo::subclassesFor('DataExtension'), ClassInfo::subclassesFor('DataObject'));
foreach($classes as $class) {
@ -828,7 +833,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
}
}
}
public static function create_temp_db() {
// Disable PHPUnit error handling
restore_error_handler();
@ -848,13 +853,13 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$st = Injector::inst()->create('SapphireTest');
$st->resetDBSchema();
// Reinstate PHPUnit error handling
set_error_handler(array('PHPUnit_Util_ErrorHandler', 'handleError'));
return $dbname;
}
public static function delete_all_temp_dbs() {
$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
foreach(DB::get_schema()->databaseList() as $dbName) {
@ -869,7 +874,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
}
}
}
/**
* Reset the testing database's schema.
* @param $includeExtraDataObjects If true, the extraDataObjects tables will also be included
@ -909,7 +914,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
singleton('DataObject')->flushCache();
}
}
/**
* Create a member and group with the given permission code, and log in with it.
* Returns the member ID.
@ -924,25 +929,25 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$permission->Code = $permCode;
$permission->write();
$group->Permissions()->add($permission);
$member = DataObject::get_one('Member', array(
'"Member"."Email"' => "$permCode@example.org"
));
if(!$member) $member = Injector::inst()->create('Member');
$member->FirstName = $permCode;
$member->Surname = "User";
$member->Email = "$permCode@example.org";
$member->write();
$group->Members()->add($member);
$this->cache_generatedMembers[$permCode] = $member;
}
$this->cache_generatedMembers[$permCode]->logIn();
return $this->cache_generatedMembers[$permCode]->ID;
}
/**
* 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:SSViewer]
* [api:SS_TemplateManifest]
* [api:SS_TemplateManifest]

View File

@ -182,18 +182,10 @@ end of each test.
$page->publish('Stage', 'Live');
}
// reset configuration for the test.
Config::nest();
// set custom configuration for the test.
Config::inst()->update('Foo', 'bar', 'Hello!');
}
public function tearDown() {
// restores the config variables
Config::unnest();
parent::tearDown();
}
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
@ -266,4 +284,4 @@ some `thirdparty/` directories add the following to the `phpunit.xml` configurat
* [api:TestRunner]
* [api:SapphireTest]
* [api:FunctionalTest]
* [api:FunctionalTest]

View File

@ -1,11 +1,11 @@
title: Changelogs
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).
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.
[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-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 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 are for and then submit the form. Your changes will be sent to the core committers for approval.
* 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.
<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*.
@ -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.
* 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.
* 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`).
## 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).
</div>
Code:
Code for a Tip box:
<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.
</div>
Code:
Code for a Notification box:
<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.
</div>
Code:
Code for a Warning box:
<div class="warning" markdown='1'>
...

View File

@ -1,12 +1,12 @@
title: Implement Internationalization
summary: Implement SilverStripe's internationalization system in your own modules.
title: Implement Internationalisation
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).
## Set up your own module for localization
## Set up your own module for localisation
### Collecting translatable text
@ -33,7 +33,7 @@ source_lang = en
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.
### 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
(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
[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
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
[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
* [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 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($oldName && $oldName != $name) {
$base = pathinfo($name, PATHINFO_BASENAME);
$base = pathinfo($name, PATHINFO_FILENAME);
$ext = self::get_file_extension($name);
$suffix = 1;
@ -645,7 +645,7 @@ class File extends DataObject {
))->first()
) {
$suffix++;
$name = "$base-$suffix$ext";
$name = "$base-$suffix.$ext";
}
}

View File

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

View File

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

View File

@ -1,35 +1,43 @@
<?php
/**
* NullableField is a field that wraps other fields when you want to allow the user to specify whether the value of
* the field is null or not.
* NullableField is a field that wraps other fields when you want to allow the user to specify
* 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"));
*
* It displays the field that is wrapped followed by a checkbox that is used to specify if the 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:
* It displays the field that is wrapped followed by a checkbox that is used to specify if the
* 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:
*
* $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
* like this:
* You can specify the label to use for the "is null" checkbox. If you want to use i18n for 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
*
* @package forms
* @subpackage fields-basic
*/
class NullableField extends FormField {
/**
* The field that holds the value of this field
*
* @var FormField
*/
protected $valueField;
/**
* The label to show next to the is null check box.
*
* @var string
*/
protected $isNullLabel;
@ -37,39 +45,55 @@ class NullableField extends FormField {
/**
* Create a new nullable field
* @param $valueField
* @return NullableField
*
* @param FormField $valueField
* @param null|string $isNullLabel
*/
public function __construct(FormField $valueField, $isNullLabel = null) {
$this->valueField = $valueField;
$this->isNullLabel = $isNullLabel;
if ( is_null($this->isNullLabel) ) {
// Set a default label if one is not provided.
if(isset($isNullLabel)) {
$this->setIsNullLabel($isNullLabel);
} else {
$this->isNullLabel = _t('NullableField.IsNullLabel', 'Is Null');
}
parent::__construct($valueField->getName(), $valueField->Title(), $valueField->Value(),
$valueField->getForm(), $valueField->RightTitle());
$this->readonly = $valueField->isReadonly();
parent::__construct(
$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.
*
* @return string
*/
public function getIsNullLabel() {
return $this->isNullLabel;
}
/**
* Set the label used for the Is Null checkbox.
*
* @param $isNulLabel string
*
* @return $this
*/
public function setIsNullLabel(string $isNulLabel){
public function setIsNullLabel($isNulLabel) {
$this->isNullLabel = $isNulLabel;
return $this;
}
/**
* Get the id used for the Is Null check box.
*
* @return string
*/
public function getIsNullId() {
@ -77,54 +101,81 @@ class NullableField extends FormField {
}
/**
* (non-PHPdoc)
* @see framework/forms/FormField#Field()
* @param array $properties
*
* @return string
*/
public function Field($properties = array()) {
if ( $this->isReadonly()) {
if($this->isReadonly()) {
$nullableCheckbox = new CheckboxField_Readonly($this->getIsNullId());
} else {
$nullableCheckbox = new CheckboxField($this->getIsNullId());
}
$nullableCheckbox->setValue(is_null($this->dataValue()));
return $this->valueField->Field() . ' ' . $nullableCheckbox->Field()
. '&nbsp;<span>' . $this->getIsNullLabel().'</span>';
return sprintf(
'%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
*
* @param mixed $value
* @param null|array $data
*
* @return $this
*/
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;
}
$this->valueField->setValue($value);
parent::setValue($value);
return $this;
}
/**
* (non-PHPdoc)
* @see forms/FormField#setName($name)
* @param string $name
*
* @return $this
*/
public function setName($name) {
// We need to pass through the name change to the underlying value field.
$this->valueField->setName($name);
parent::setName($name);
return $this;
}
/**
* (non-PHPdoc)
* @see framework/forms/FormField#debug()
* @return string
*/
public function debug() {
$result = "$this->class ($this->name: $this->title : <font style='color:red;'>$this->message</font>) = ";
$result .= (is_null($this->value)) ? "<<null>>" : $this->value;
return result;
$result = sprintf(
'%s (%s: $s : <span style="color: red">%s</span>) = ',
$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
*/
class NumericField extends TextField {
/**
* Override locale for this field
*
* Override locale for this field.
*
* @var string
*/
protected $locale = null;
/**
* @param mixed $value
* @param array $data
*
* @return $this
*
* @throws Zend_Locale_Exception
*/
public function setValue($value, $data = array()) {
require_once "Zend/Locale/Format.php";
// If passing in a non-string number, or a value
// directly from a dataobject then localise this number
if ((is_numeric($value) && !is_string($value)) ||
($value && $data instanceof DataObject)
){
// directly from a DataObject then localise this number
if(is_int($value) || is_float($value) || $data instanceof DataObject) {
$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 {
// If an invalid number, store it anyway, but validate() will fail
$this->value = $this->clean($value);
}
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
* @return string The input value, with all spaces replaced with non-breaking spaces
*
* @return string
*/
protected function clean($input) {
$nbsp = html_entity_decode('&nbsp;', null, 'UTF-8');
return str_replace(' ', $nbsp, trim($input));
$replacement = html_entity_decode('&nbsp;', null, 'UTF-8');
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
*/
protected function isNumeric() {
require_once "Zend/Locale/Format.php";
$locale = new Zend_Locale($this->getLocale());
return Zend_Locale_Format::isNumber(
$this->clean($this->value),
array('locale' => $locale)
);
}
/**
* {@inheritdoc}
*/
public function Type() {
return 'numeric text';
}
@ -81,74 +100,111 @@ class NumericField extends TextField {
return true;
}
if($this->isNumeric()) return true;
if($this->isNumeric()) {
return true;
}
$validator->validationError(
$this->name,
_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)
),
"validation"
);
return false;
}
/**
* Extracts the number value from the localised string value
*
* @return string number value
* Extracts the number value from the localised string value.
*
* @return string
*/
public function dataValue() {
require_once "Zend/Locale/Format.php";
if(!$this->isNumeric()) return 0;
if(!$this->isNumeric()) {
return 0;
}
$locale = new Zend_Locale($this->getLocale());
$number = Zend_Locale_Format::getNumber(
$this->clean($this->value),
array('locale' => $locale)
);
return $number;
}
/**
* Returns a readonly version of this field
* Creates a read-only version of the field.
*
* @return NumericField_Readonly
*/
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);
return $field;
}
/**
* Gets the current locale this field is set to
*
* Gets the current locale this field is set to.
*
* @return string
*/
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
*
* @return $this
*/
public function setLocale($locale) {
$this->locale = $locale;
return $this;
}
}
/**
* Readonly version of a numeric field.
*
* @package forms
* @subpackage fields-basic
*/
class NumericField_Readonly extends ReadonlyField {
/**
* @return static
*/
public function performReadonlyTransformation() {
return clone $this;
}
/**
* @return string
*/
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.
*
* GridField is a field that takes an SS_List and displays it in an table with rows
* and columns. It reminds of the old TableFields but works with SS_List types
* and only loads the necessary rows from the list.
* GridField is a field that takes an SS_List and displays it in an table with rows and columns.
* It reminds of the old TableFields but works with SS_List types and only loads the necessary
* rows from the list.
*
* The minimum configuration is to pass in name and title of the field and a
* SS_List.
* The minimum configuration is to pass in name and title of the field and a SS_List.
*
* <code>
* $gridField = new GridField('ExampleGrid', 'Example grid', new DataList('Page'));
@ -21,45 +20,44 @@
* @property GridState_Data $State The gridstate of this object
*/
class GridField extends FormField {
/**
*
* @var array
*/
private static $allowed_actions = array(
'index',
'gridFieldAlterAction'
'gridFieldAlterAction',
);
/**
* The datasource
* Data source.
*
* @var SS_List
*/
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
*/
protected $modelClassName = '';
/**
* the current state of the GridField
* Current state of the GridField.
*
* @var GridState
*/
protected $state = null;
/**
*
* @var GridFieldConfig
*/
protected $config = null;
/**
* The components list
* Components list.
*
* @var array
*/
@ -67,14 +65,15 @@ class GridField extends FormField {
/**
* 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
*/
protected $columnDispatch = null;
/**
* Map of callbacks for custom data fields
* Map of callbacks for custom data fields.
*
* @var array
*/
@ -86,8 +85,6 @@ class GridField extends FormField {
protected $name = '';
/**
* Creates a new GridField field
*
* @param string $name
* @param string $title
* @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) {
parent::__construct($name, $title, null);
$this->name = $name;
if($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->state = new GridState($this);
@ -109,44 +111,58 @@ class GridField extends FormField {
$this->addExtraClass('ss-gridfield');
}
/**
* @param SS_HTTPRequest $request
*
* @return string
*/
public function index($request) {
return $this->gridFieldAlterAction(array(), $this->getForm(), $request);
}
/**
* Set the modelClass (dataobject) 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
* Set the modelClass (data object) that this field will get it column headers from.
*
* If no $displayFields has been set, the display fields will be $summary_fields.
*
* @see GridFieldDataColumns::getDisplayFields()
*
* @param string $modelClassName
*
* @see GridFieldDataColumns::getDisplayFields()
* @return $this
*/
public function setModelClass($modelClassName) {
$this->modelClassName = $modelClassName;
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
*
* @throws LogicException
*/
public function getModelClass() {
if($this->modelClassName) return $this->modelClassName;
if($this->list && method_exists($this->list, 'dataClass')) {
$class = $this->list->dataClass();
if($class) return $class;
if($this->modelClassName) {
return $this->modelClassName;
}
throw new LogicException('GridField doesn\'t have a modelClassName,'
. ' so it doesn\'t know the columns of this grid.');
if($this->list && method_exists($this->list, 'dataClass')) {
$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
*/
public function getConfig() {
@ -156,61 +172,69 @@ class GridField extends FormField {
/**
* @param GridFieldConfig $config
*
* @return GridField
* @return $this
*/
public function setConfig(GridFieldConfig $config) {
$this->config = $config;
return $this;
}
/**
* @return ArrayList
*/
public function getComponents() {
return $this->config->getComponents();
}
/**
* Cast a arbitrary value with the help of a castingDefintion
*
* @param $value
* @param $castingDefinition
* Cast an arbitrary value with the help of a $castingDefinition.
*
* @todo refactor this into GridFieldComponent
*
* @param mixed $value
* @param string|array $castingDefinition
*
* @return mixed
*/
public function getCastedValue($value, $castingDefinition) {
$castingParams = array();
if(is_array($castingDefinition)) {
$castingParams = $castingDefinition;
array_shift($castingParams);
$castingDefinition = array_shift($castingDefinition);
} else {
$castingParams = array();
}
if(strpos($castingDefinition, '->') === false) {
$castingFieldType = $castingDefinition;
$castingField = DBField::create_field($castingFieldType, $value);
$value = call_user_func_array(array($castingField, 'XML'), $castingParams);
} else {
$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 call_user_func_array(array($castingField, 'XML'), $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
*
* @return $this
*/
public function setList(SS_List $list) {
$this->list = $list;
return $this;
}
/**
* Get the datasource
* Get the data source.
*
* @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
*/
public function getManipulatedList() {
$list = $this->getList();
foreach($this->getComponents() as $item) {
if($item instanceof GridField_DataManipulator) {
$list = $item->getManipulatedData($this, $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) {
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
*/
@ -265,88 +293,115 @@ class GridField extends FormField {
Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js');
Requirements::javascript(FRAMEWORK_DIR . '/javascript/GridField.js');
// Get columns
$columns = $this->getColumns();
// Get data
$list = $this->getManipulatedList();
// Render headers, footers, etc
$content = array(
"before" => "",
"after" => "",
"header" => "",
"footer" => "",
'before' => '',
'after' => '',
'header' => '',
'footer' => '',
);
foreach($this->getComponents() as $item) {
if($item instanceof GridField_HTMLProvider) {
$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) {
$content[$k] = trim($v);
}
if($fragments) {
foreach($fragments as $fragmentKey => $fragmentValue) {
$fragmentKey = strtolower($fragmentKey);
// Replace custom fragments and check which fragments are defined
// Nested dependencies are handled by deferring the rendering of any content item that
// 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.");
if(!isset($content[$fragmentKey])) {
$content[$fragmentKey] = '';
}
// Otherwise we can push to the end of the content array
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]);
$content[$fragmentKey] .= $fragmentValue . "\n";
}
}
}
}
// Check for any undefined fragments, and if so throw an exception
// While we're at it, trim whitespace off the elements
foreach($content as $k => $v) {
if(empty($fragmentDefined[$k])) {
throw new LogicException("GridField HTML fragment '$k' was given content,"
. " but not defined. Perhaps there is a supporting GridField component you need to add?");
foreach($content as $contentKey => $contentValue) {
$content[$contentKey] = trim($contentValue);
}
// Replace custom fragments and check which fragments are defined. Circular dependencies
// 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);
if($total > 0) {
$rows = array();
foreach($list as $idx => $record) {
foreach($list as $index => $record) {
if($record->hasMethod('canView') && !$record->canView()) {
continue;
}
@ -356,58 +411,80 @@ class GridField extends FormField {
foreach($this->getColumns() as $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) {
continue;
}
$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);
}
// Display a message when the grid field is empty
if(!(isset($content['body']) && $content['body'])) {
$content['body'] = FormField::create_tag(
'tr',
array("class" => 'ss-gridfield-item ss-gridfield-no-items'),
FormField::create_tag(
'td',
array('colspan' => count($columns)),
_t('GridField.NoItemsFound', 'No items found')
)
// Display a message when the grid field is empty.
if(empty($content['body'])) {
$cell = FormField::create_tag(
'td',
array(
'colspan' => count($columns),
),
_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
$head = $content['header']
? FormField::create_tag('thead', array(), $content['header'])
: '';
$body = $content['body']
? FormField::create_tag('tbody', array('class' => 'ss-gridfield-items'), $content['body'])
: '';
$foot = $content['footer']
? FormField::create_tag('tfoot', array(), $content['footer'])
: '';
$header = $this->getOptionalTableHeader($content);
$body = $this->getOptionalTableBody($content);
$footer = $this->getOptionalTableFooter($content);
$this->addExtraClass('ss-gridfield field');
$attrs = array_diff_key(
$fieldsetAttributes = array_diff_key(
$this->getAttributes(),
array('value' => false, 'type' => false, 'name' => false)
array(
'value' => false,
'type' => false,
'name' => false,
)
);
$attrs['data-name'] = $this->getName();
$tableAttrs = array(
'id' => isset($this->id) ? $this->id : null,
$fieldsetAttributes['data-name'] = $this->getName();
$tableId = null;
if($this->id) {
$tableId = $this->id;
}
$tableAttributes = array(
'id' => $tableId,
'class' => 'ss-gridfield-table',
'cellpadding' => '0',
'cellspacing' => '0'
'cellspacing' => '0',
);
if($this->getDescription()) {
@ -418,12 +495,17 @@ class GridField extends FormField {
);
}
return
FormField::create_tag('fieldset', $attrs,
$content['before'] .
FormField::create_tag('table', $tableAttrs, $head . "\n" . $foot . "\n" . $body) .
$content['after']
);
$table = FormField::create_tag(
'table',
$tableAttributes,
$header . "\n" . $footer . "\n" . $body
);
return FormField::create_tag(
'fieldset',
$fieldsetAttributes,
$content['before'] . $table . $content['after']
);
}
/**
@ -495,27 +577,44 @@ class GridField extends FormField {
$classes[] = 'last';
}
$classes[] = ($index % 2) ? 'even' : 'odd';
if($index % 2) {
$classes[] = 'even';
} else {
$classes[] = 'odd';
}
return $classes;
}
/**
* @param array $properties
*
* @return string
*/
public function Field($properties = array()) {
return $this->FieldHolder($properties);
}
/**
* {@inheritdoc}
*/
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
*/
public function getColumns() {
// Get column list
$columns = array();
foreach($this->getComponents() as $item) {
if($item instanceof GridField_ColumnProvider) {
$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 string $column
*
* @return string
*
* @throws InvalidArgumentException
*/
public function getColumnContent($record, $column) {
// Build the column dispatch
if(!$this->columnDispatch) {
$this->buildColumnDispatch();
}
if(!empty($this->columnDispatch[$column])) {
$content = "";
$content = '';
foreach($this->columnDispatch[$column] as $handler) {
/**
* @var GridField_ColumnProvider $handler
*/
$content .= $handler->getColumnContent($this, $record, $column);
}
return $content;
} 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.
* 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) {
// Custom callbacks
if(isset($this->customDataFields[$fieldName])) {
$callback = $this->customDataFields[$fieldName];
return $callback($record);
}
// Default implementation
if($record->hasMethod('relField')) {
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 string $column
*
* @return array
*
* @throws LogicException
* @throws InvalidArgumentException
*/
public function getColumnAttributes($record, $column) {
// Build the column dispatch
if(!$this->columnDispatch) {
$this->buildColumnDispatch();
}
if(!empty($this->columnDispatch[$column])) {
$attrs = array();
$attributes = array();
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)) {
$attrs = array_merge($attrs, $column_attrs);
} elseif($column_attrs) {
$methodSignature = get_class($handler) . "::getColumnAttributes()";
throw new LogicException("Non-array response from $methodSignature.");
if(is_array($columnAttributes)) {
$attributes = array_merge($attributes, $columnAttributes);
continue;
}
throw new LogicException(sprintf(
'Non-array response from %s::getColumnAttributes().',
get_class($handler)
));
}
return $attrs;
} else {
throw new InvalidArgumentException("Bad column '$column'");
return $attributes;
}
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
*
* @return array
*
* @throws LogicException
* @throws InvalidArgumentException
*/
public function getColumnMetadata($column) {
// Build the column dispatch
if(!$this->columnDispatch) {
$this->buildColumnDispatch();
}
if(!empty($this->columnDispatch[$column])) {
$metadata = array();
$metaData = array();
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)) {
$metadata = array_merge($metadata, $column_metadata);
} else {
$methodSignature = get_class($handler) . "::getColumnMetadata()";
throw new LogicException("Non-array response from $methodSignature.");
if(is_array($columnMetaData)) {
$metaData = array_merge($metaData, $columnMetaData);
continue;
}
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
*/
public function getColumnCount() {
// Build the column dispatch
if(!$this->columnDispatch) $this->buildColumnDispatch();
if(!$this->columnDispatch) {
$this->buildColumnDispatch();
}
return count($this->columnDispatch);
}
/**
* Build an columnDispatch that maps a GridField_ColumnProvider to a column
* for reference later
*
* Build an columnDispatch that maps a GridField_ColumnProvider to a column for reference later.
*/
protected function buildColumnDispatch() {
$this->columnDispatch = array();
@ -691,140 +826,172 @@ class GridField extends FormField {
* This is the action that gets executed when a GridField_AlterAction gets clicked.
*
* @param array $data
* @param Form $form
* @param SS_HTTPRequest $request
*
* @return string
*/
public function gridFieldAlterAction($data, $form, SS_HTTPRequest $request) {
$html = '';
$data = $request->requestVars();
$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);
if(isset($fieldData['GridState'])) {
$state->setValue($fieldData['GridState']);
}
// Try to execute alter action
foreach($data as $k => $v) {
if(preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $k, $matches)) {
$id = $matches[1];
$stateChange = Session::get($id);
foreach($data as $dataKey => $dataValue) {
if(preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $dataKey, $matches)) {
$stateChange = Session::get($matches[1]);
$actionName = $stateChange['actionName'];
$args = isset($stateChange['args']) ? $stateChange['args'] : array();
$html = $this->handleAlterAction($actionName, $args, $data);
// A field can optionally return its own HTML
if($html) return $html;
$arguments = array();
if(isset($stateChange['args'])) {
$arguments = $stateChange['args'];
};
$html = $this->handleAlterAction($actionName, $arguments, $data);
if($html) {
return $html;
}
}
}
switch($request->getHeader('X-Pjax')) {
case 'CurrentField':
return $this->FieldHolder();
break;
case 'CurrentForm':
return $form->forTemplate();
break;
default:
return $form->forTemplate();
break;
if($request->getHeader('X-Pjax') === 'CurrentField') {
return $this->FieldHolder();
}
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 mixed $args
* @param array $data - send data from a form
* @param mixed $arguments
* @param array $data
*
* @return mixed
*
* @throws InvalidArgumentException
*/
public function handleAlterAction($actionName, $args, $data) {
public function handleAlterAction($actionName, $arguments, $data) {
$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)))) {
return $component->handleAction($this, $actionName, $args, $data);
foreach($this->getComponents() as $component) {
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) {
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->setDataModel($model);
$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) {
if(!($component instanceof GridField_URLHandler)) {
continue;
}
if($component instanceof GridField_URLHandler && $urlHandlers = $component->getURLHandlers($this)) {
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($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));
if($action[0] == '$') {
$action = $params[substr($action, 1)];
}
try {
$result = $component->$action($this, $request);
} catch(SS_HTTPResponse_Exception $responseException) {
$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");
if(!method_exists($component, 'checkAccessAction') || $component->checkAccessAction($action)) {
if(!$action) {
$action = "index";
}
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
} else if($request->allParsed()) {
return $result;
try {
$result = $component->$action($this, $request);
} 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
} else {
return $this->httpError(404,
"I can't handle sub-URLs of a " . get_class($result) . " object.");
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($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);
}
/**
* {@inheritdoc}
*/
public function saveInto(DataObjectInterface $record) {
foreach($this->getComponents() as $component) {
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
* the state of the {@link GridField}, rendered as a button element.
* This class is the base class when you want to have an action that alters the state of the
* {@link GridField}, rendered as a button element.
*
* @package forms
* @package forms
* @subpackage fields-gridfield
*/
class GridField_FormAction extends FormAction {
/**
* @var GridField
*/
@ -882,7 +1095,7 @@ class GridField_FormAction extends FormAction {
/**
* @param GridField $gridField
* @param string $name
* @param string $label
* @param string $title
* @param string $actionName
* @param array $args
*/
@ -895,19 +1108,20 @@ class GridField_FormAction extends FormAction {
}
/**
* urlencode encodes less characters in percent form than we need - we
* need everything that isn't a \w.
* Encode all non-word characters.
*
* @param string $val
* @param string $value
*
* @return string
*/
public function nameEncode($val) {
return preg_replace_callback('/[^\w]/', array($this, '_nameEncode'), $val);
public function nameEncode($value) {
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) {
return '%' . dechex(ord($match[0]));
@ -941,9 +1155,7 @@ class GridField_FormAction extends FormAction {
}
/**
* Calculate the name of the gridfield relative to the Form
*
* @param GridField $base
* Calculate the name of the gridfield relative to the form.
*
* @return string
*/

View File

@ -18,19 +18,17 @@ class DirectorTest extends SapphireTest {
public function setUp() {
parent::setUp();
// Required for testRequestFilterInDirectorTest
Injector::nest();
// Hold the original request URI once so it doesn't get overwritten
if(!self::$originalRequestURI) {
self::$originalRequestURI = $_SERVER['REQUEST_URI'];
}
$_SERVER['REQUEST_URI'] = 'http://www.mysite.com';
$this->originalGet = $_GET;
$this->originalSession = $_SESSION;
$_SESSION = array();
Config::inst()->update('Director', 'rules', array(
'DirectorTestRule/$Action/$ID/$OtherID' => 'DirectorTestRequest_Controller',
'en-nz/$Action/$ID/$OtherID' => array(
@ -53,9 +51,6 @@ class DirectorTest extends SapphireTest {
public function tearDown() {
// 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;
$_SESSION = $this->originalSession;
@ -68,7 +63,6 @@ class DirectorTest extends SapphireTest {
}
}
Injector::unnest();
parent::tearDown();
}
@ -140,7 +134,7 @@ class DirectorTest extends SapphireTest {
public function testAlternativeBaseURL() {
// Get original protocol and hostname
$rootURL = Director::protocolAndHost();
// relative base URLs - you should end them in a /
Config::inst()->update('Director', 'alternate_base_url', '/relativebase/');
$_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
$this->assertNotContains('test_2b',
Config::inst()->get('ConfigStaticTest_Third', 'first', Config::UNINHERITED));
$this->assertNotContains('test_2b',
Config::inst()->get('ConfigStaticTest_Fourth', 'first', Config::UNINHERITED));
$this->assertNull(Config::inst()->get('ConfigStaticTest_Fourth', 'first', Config::UNINHERITED));
// 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()

View File

@ -391,7 +391,7 @@ class ConfigManifestTest extends SapphireTest {
public function testEnvironmentRules() {
foreach (array('dev', 'test', 'live') as $env) {
Config::inst()->nest();
Config::nest();
Config::inst()->update('Director', 'environment_type', $env);
$config = $this->getConfigFixtureValue('Environment');
@ -403,13 +403,11 @@ class ConfigManifestTest extends SapphireTest {
);
}
Config::inst()->unnest();
Config::unnest();
}
}
public function testDynamicEnvironmentRules() {
Config::inst()->nest();
// First, make sure environment_type is live
Config::inst()->update('Director', 'environment_type', 'live');
$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
$this->assertEquals('dev', Config::inst()->get('ConfigManifestTest', 'DynamicEnvironment'));
Config::inst()->unnest();
}
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).
*
*
* @package framework
* @package tests
*/
class DevAdminControllerTest extends FunctionalTest {
public function setUp(){
parent::setUp();
Config::nest()->update('DevelopmentAdmin', 'registered_controllers', array(
Config::inst()->update('DevelopmentAdmin', 'registered_controllers', array(
'x1' => array(
'controller' => 'DevAdminControllerTest_Controller1',
'links' => array(
@ -27,45 +27,40 @@ class DevAdminControllerTest extends FunctionalTest {
)
));
}
public function tearDown(){
parent::tearDown();
Config::unnest();
}
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)
$this->assertContains(DevAdminControllerTest_Controller1::OK_MSG, $this->getCapture('/dev/x1'));
$this->assertContains(DevAdminControllerTest_Controller1::OK_MSG, $this->getCapture('/dev/x1/y1'));
}
public function testGoodRegisteredControllerStatus(){
// Check response code is 200/OK
$this->assertEquals(false, $this->getAndCheckForError('/dev/x1'));
$this->assertEquals(false, $this->getAndCheckForError('/dev/x1/y1'));
// Check response code is 500/ some sort of error
$this->assertEquals(true, $this->getAndCheckForError('/dev/x2'));
}
protected function getCapture($url){
$this->logInWithPermission('ADMIN');
ob_start();
$this->get($url);
$r = ob_get_contents();
ob_end_clean();
return $r;
}
protected function getAndCheckForError($url){
$this->logInWithPermission('ADMIN');
if(Director::is_cli()){
// when in CLI the admin controller throws exceptions
ob_start();
@ -75,10 +70,10 @@ class DevAdminControllerTest extends FunctionalTest {
ob_end_clean();
return true;
}
ob_end_clean();
return false;
}else{
// when in http the admin controller sets a response header
ob_start();
@ -87,30 +82,30 @@ class DevAdminControllerTest extends FunctionalTest {
return $resp->isError();
}
}
}
class DevAdminControllerTest_Controller1 extends Controller {
const OK_MSG = 'DevAdminControllerTest_Controller1 TEST OK';
private static $url_handlers = array(
'' => 'index',
'y1' => 'y1Action'
);
private static $allowed_actions = array(
'index',
'y1Action',
);
public function index(){
echo self::OK_MSG;
}
public function y1Action(){
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() {
return new Form(
new Controller(),

View File

@ -91,6 +91,14 @@ class TimeFieldTest extends SapphireTest {
$f = new TimeField('Time', 'Time');
$f->setValue('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() {

View File

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

View File

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

View File

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