mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
(merged from branches/roa. use "svn log -c <changeset> -g <module-svn-path>" for detailed commit message)
git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@60264 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
parent
46bbde18e7
commit
95d4634e2e
2
Makefile
2
Makefile
@ -10,7 +10,7 @@ URL=`./cli-script.php SapphireInfo/baseurl`
|
||||
test: phpunit
|
||||
|
||||
phpunit:
|
||||
php5 ./cli-script.php TestRunner flush=1
|
||||
php5 ./cli-script.php dev/tests/all flush=1
|
||||
|
||||
windmill:
|
||||
functest ../cms/tests/test_windmill url=${URL}admin browser=firefox
|
||||
|
@ -376,7 +376,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
foreach($rightData as $key=>$rightVal) {
|
||||
// don't merge conflicting values if priority is 'left'
|
||||
if($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) continue;
|
||||
|
||||
|
||||
// don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
|
||||
if($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) continue;
|
||||
|
||||
@ -623,7 +623,12 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
// (used mainly for has_one/has_many)
|
||||
if(!$fieldObj) $fieldObj = DBField::create('Varchar', $this->record[$fieldName], $fieldName);
|
||||
|
||||
$fieldObj->setValue($this->record[$fieldName], $this->record);
|
||||
// CompositeDBFields handle their own value storage; regular fields need to be
|
||||
// re-populated from the database
|
||||
if(!$fieldObj instanceof CompositeDBField) {
|
||||
$fieldObj->setValue($this->record[$fieldName], $this->record);
|
||||
}
|
||||
|
||||
$fieldObj->writeToManipulation($manipulation[$class]);
|
||||
}
|
||||
}
|
||||
@ -829,7 +834,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
user_error("DataObject::getComponent(): Unknown 1-to-1 component '$componentName' on class '$this->class'", E_USER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A cache used by component getting classes
|
||||
* @var array
|
||||
@ -878,10 +883,10 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the query object for a $has_many Component.
|
||||
*
|
||||
*
|
||||
* Use {@link DataObjectSet->setComponentInfo()} to attach metadata to the
|
||||
* resultset you're building with this query.
|
||||
* Use {@link DataObject->buildDataObjectSet()} to build a set out of the {@link SQLQuery}
|
||||
@ -900,7 +905,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
}
|
||||
|
||||
$joinField = $this->getComponentJoinField($componentName);
|
||||
|
||||
|
||||
$id = $this->getField("ID");
|
||||
|
||||
// get filter
|
||||
@ -977,9 +982,9 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
// Join expression is done on SiteTree.ID even if we link to Page; it helps work around
|
||||
// database inconsistencies
|
||||
$componentBaseClass = ClassInfo::baseDataClass($componentClass);
|
||||
|
||||
|
||||
if($this->ID && is_numeric($this->ID)) {
|
||||
|
||||
|
||||
if($componentClass) {
|
||||
$query = $this->getManyManyComponentsQuery($componentName, $filter, $sort, $join, $limit);
|
||||
|
||||
@ -1003,7 +1008,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the query object for a $many_many Component.
|
||||
* Use {@link DataObjectSet->setComponentInfo()} to attach metadata to the
|
||||
@ -1020,7 +1025,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
*/
|
||||
public function getManyManyComponentsQuery($componentName, $filter = "", $sort = "", $join = "", $limit = "") {
|
||||
list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName);
|
||||
|
||||
|
||||
$componentObj = singleton($componentClass);
|
||||
|
||||
// Join expression is done on SiteTree.ID even if we link to Page; it helps work around
|
||||
@ -1037,7 +1042,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
|
||||
if($filter) $query->where[] = $filter;
|
||||
if($join) $query->from[] = $join;
|
||||
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
@ -1065,23 +1070,23 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
if ($this->has_many() || $this->many_many()) {
|
||||
$oldFields = $fieldSet;
|
||||
$fieldSet = new FieldSet(
|
||||
new TabSet("Root", new Tab("Main"))
|
||||
new TabSet("Root", new Tab("Main"))
|
||||
);
|
||||
foreach($oldFields as $field) {
|
||||
$fieldSet->addFieldToTab("Root.Main", $field);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if($this->has_many()) {
|
||||
// Add each relation as a separate tab
|
||||
foreach($this->has_many() as $relationship => $component) {
|
||||
$relationshipFields = singleton($component)->summaryFields();
|
||||
$foreignKey = $this->getComponentJoinField($relationship);
|
||||
$ctf = new ComplexTableField(
|
||||
$this,
|
||||
$relationship,
|
||||
$component,
|
||||
$relationshipFields,
|
||||
$this,
|
||||
$relationship,
|
||||
$component,
|
||||
$relationshipFields,
|
||||
"getCMSFields",
|
||||
"$foreignKey = $this->ID"
|
||||
);
|
||||
@ -1089,18 +1094,18 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
$fieldSet->addFieldToTab("Root.$relationship", $ctf);
|
||||
}
|
||||
}
|
||||
if ($this->many_many()) {
|
||||
if ($this->many_many()) {
|
||||
foreach($this->many_many() as $relationship => $component) {
|
||||
$relationshipFields = singleton($component)->summaryFields();
|
||||
$filterWhere = $this->getManyManyFilter($relationship, $component);
|
||||
$filterJoin = $this->getManyManyJoin($relationship, $component);
|
||||
$ctf = new ComplexTableField(
|
||||
$this,
|
||||
$relationship,
|
||||
$component,
|
||||
$relationshipFields,
|
||||
$this,
|
||||
$relationship,
|
||||
$component,
|
||||
$relationshipFields,
|
||||
"getCMSFields",
|
||||
$filterWhere,
|
||||
$filterWhere,
|
||||
'',
|
||||
$filterJoin
|
||||
);
|
||||
@ -1126,17 +1131,17 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
$classes = array_reverse(ClassInfo::ancestry($this->class));
|
||||
|
||||
list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName);
|
||||
|
||||
|
||||
if($baseTable == $parentClass) {
|
||||
return "LEFT JOIN `$table` ON (`$parentField` = `$parentClass`.`ID` AND `$componentField` = '{$this->ID}')";
|
||||
} else {
|
||||
return "LEFT JOIN `$table` ON (`$componentField` = `$componentClass`.`ID` AND `$parentField` = '{$this->ID}')";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getManyManyFilter($componentName, $baseTable) {
|
||||
list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName);
|
||||
|
||||
|
||||
return "`$table`.`$parentField` = '{$this->ID}'";
|
||||
}
|
||||
|
||||
@ -1294,14 +1299,14 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
* @return SearchContext
|
||||
*/
|
||||
public function getDefaultSearchContext() {
|
||||
return new SearchContext($this->class, $this->searchable_fields(), $this->defaultSearchFilters());
|
||||
return new SearchContext($this->class, new FieldSet($this->searchable_fields()), $this->defaultSearchFilters());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine which properties on the DataObject are
|
||||
* searchable, and map them to their default {@link FormField}
|
||||
* representations. Used for scaffolding a searchform for {@link ModelAdmin}.
|
||||
*
|
||||
*
|
||||
* Some additional logic is included for switching field labels, based on
|
||||
* how generic or specific the field type is.
|
||||
*
|
||||
@ -1349,7 +1354,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
$fieldClass = $fieldClasses[$fieldName];
|
||||
$fieldObject = new $fieldClass($fieldName);
|
||||
} else {
|
||||
$fieldObject = $this->dbObject($fieldName)->scaffoldFormField();
|
||||
$fieldObject = $this->dbObject($fieldName)->scaffoldFormField();
|
||||
}
|
||||
$fields->push($fieldObject);
|
||||
}
|
||||
@ -1434,7 +1439,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
$fieldObj = eval($constructor);
|
||||
if(isset($this->record[$field])) $fieldObj->setValue($this->record[$field], $this->record);
|
||||
$this->record[$field] = $fieldObj;
|
||||
|
||||
|
||||
return $this->record[$field];
|
||||
}
|
||||
|
||||
@ -1492,7 +1497,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
if($val instanceof DBField) {
|
||||
$val->Name = $fieldName;
|
||||
$this->record[$fieldName] = $val;
|
||||
|
||||
|
||||
// Situation 2: Passing a literal
|
||||
} else {
|
||||
$defaults = $this->stat('defaults');
|
||||
@ -1660,17 +1665,17 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
public function canDelete($member = null) {
|
||||
return Permission::check('ADMIN');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @todo Should canCreate be a static method?
|
||||
*
|
||||
*
|
||||
* @param Member $member
|
||||
* @return boolean
|
||||
*/
|
||||
public function canCreate($member = null) {
|
||||
return Permission::check('ADMIN');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Debugging used by Debug::show()
|
||||
*
|
||||
@ -1723,7 +1728,11 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
* @return DBField The field as a DBField object
|
||||
*/
|
||||
public function dbObject($fieldName) {
|
||||
return $this->obj($fieldName);
|
||||
if($fieldName == 'ID') {
|
||||
return new PrimaryKey($fieldName, $this);
|
||||
} else {
|
||||
return $this->obj($fieldName);
|
||||
}
|
||||
/*
|
||||
$helperPair = $this->castingHelperPair($fieldName);
|
||||
$constructor = $helperPair['castingHelper'];
|
||||
@ -1744,9 +1753,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
* @return DBField
|
||||
*/
|
||||
public function relObject($fieldPath) {
|
||||
//Debug::message("relObject:$fieldPath");
|
||||
$parts = explode('.', $fieldPath);
|
||||
//Debug::dump($parts);
|
||||
$fieldName = array_pop($parts);
|
||||
$component = $this;
|
||||
foreach($parts as $relation) {
|
||||
@ -1754,30 +1761,30 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
$component = singleton($rel);
|
||||
} elseif ($rel = $component->has_many($relation)) {
|
||||
$component = singleton($rel);
|
||||
//Debug::dump($component);
|
||||
} elseif ($rel = $component->many_many($relation)) {
|
||||
//Debug::dump($rel);
|
||||
$component = singleton($rel[1]);
|
||||
//Debug::dump($component);
|
||||
}
|
||||
}
|
||||
|
||||
$object = $component->dbObject($fieldName);
|
||||
//Debug::message("$component->class.$fieldName");
|
||||
//Debug::show($component);
|
||||
//Debug::message("$component->class.$fieldName == $object->class");
|
||||
|
||||
if (!($object instanceof DBField)) {
|
||||
user_error("Unable to traverse to related object field [$fieldPath]", E_USER_ERROR);
|
||||
//Debug::dump($object);
|
||||
}
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary hack to return an association name, based on class, toget around the mangle
|
||||
* Temporary hack to return an association name, based on class, to get around the mangle
|
||||
* of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
|
||||
*
|
||||
* @return String
|
||||
*/
|
||||
public function getReverseAssociation($className) {
|
||||
public function getReverseAssociation($className) {
|
||||
if (is_array($this->many_many())) {
|
||||
$many_many = array_flip($this->many_many());
|
||||
if (array_key_exists($className, $many_many)) return $many_many[$className];
|
||||
}
|
||||
if (is_array($this->has_many())) {
|
||||
$has_many = array_flip($this->has_many());
|
||||
if (array_key_exists($className, $has_many)) return $has_many[$className];
|
||||
@ -1786,6 +1793,8 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
$has_one = array_flip($this->has_one());
|
||||
if (array_key_exists($className, $has_one)) return $has_one[$className];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1821,7 +1830,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
$query->where($filter);
|
||||
$query->orderby($sort);
|
||||
$query->limit($limit);
|
||||
|
||||
|
||||
// Add SQL for multi-value fields on the base table
|
||||
$databaseFields = $this->databaseFields();
|
||||
if($databaseFields) foreach($databaseFields as $k => $v) {
|
||||
@ -2284,6 +2293,13 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
*/
|
||||
public function searchable_fields() {
|
||||
$fields = $this->stat('searchable_fields');
|
||||
|
||||
// if fields were passed in numeric array,
|
||||
// convert to an associative array
|
||||
if($fields && array_key_exists(0, $fields)) {
|
||||
$fields = array_fill_keys(array_values($fields), 'TextField');
|
||||
}
|
||||
|
||||
if (!$fields) {
|
||||
$fields = array_fill_keys(array_keys($this->summaryFields()), 'TextField');
|
||||
} else {
|
||||
@ -2297,54 +2313,49 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
}
|
||||
return $fields;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get any user defined searchable fields labels that
|
||||
* exist. Allows overriding of default field names in the form
|
||||
* interface actually presented to the user.
|
||||
*
|
||||
*
|
||||
* The reason for keeping this separate from searchable_fields,
|
||||
* which would be a logical place for this functionality, is to
|
||||
* which would be a logical place for this functionality, is to
|
||||
* avoid bloating and complicating the configuration array. Currently
|
||||
* much of this system is based on sensible defaults, and this property
|
||||
* would generally only be set in the case of more complex relationships
|
||||
* between data object being required in the search interface.
|
||||
*
|
||||
* Generates labels based on name of the field itself, if no static property exists.
|
||||
*
|
||||
*
|
||||
* Generates labels based on name of the field itself, if no static property
|
||||
* {@link self::searchable_fields_labels} exists.
|
||||
*
|
||||
* @todo fix bad code
|
||||
*
|
||||
*
|
||||
* @param $fieldName name of the field to retrieve
|
||||
* @return array of all element labels if no argument given
|
||||
* @return string of label if field
|
||||
*/
|
||||
public function searchable_fields_labels($fieldName=false) {
|
||||
$labels = $this->stat('searchable_fields_labels');
|
||||
if (is_array($labels)) {
|
||||
if ($fieldName) {
|
||||
if (isset($labels[$fieldName])) {
|
||||
return $labels[$fieldName];
|
||||
}
|
||||
$custom_labels = $this->stat('searchable_fields_labels');
|
||||
|
||||
$fields = array_keys($this->searchable_fields());
|
||||
$labels = array_combine($fields, $fields);
|
||||
if(is_array($custom_labels)) $labels = array_merge($labels, $custom_labels);
|
||||
if ($fieldName) {
|
||||
if(array_key_exists($fieldName, $labels)) {
|
||||
return $labels[$fieldName];
|
||||
} elseif (strstr($fieldName, '.')) {
|
||||
$parts = explode('.', $fieldName);
|
||||
$label = $parts[count($parts)-2] . ' ' . $parts[count($parts)-1];
|
||||
return $this->toLabel($label);
|
||||
} else {
|
||||
return $labels;
|
||||
return $this->toLabel($fieldName);
|
||||
}
|
||||
} else {
|
||||
$fields = array_keys($this->searchable_fields());
|
||||
$labels = array_combine($fields, $fields);
|
||||
if ($fieldName) {
|
||||
if (strstr($fieldName, '.')) {
|
||||
$parts = explode('.', $fieldName);
|
||||
$label = $parts[count($parts)-2] . ' ' . $parts[count($parts)-1];
|
||||
return $this->toLabel($label);
|
||||
} else {
|
||||
return $this->toLabel($fieldName);
|
||||
}
|
||||
} else {
|
||||
return $labels;
|
||||
}
|
||||
return $labels;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the default summary fields for this object.
|
||||
*
|
||||
@ -2354,13 +2365,13 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
*/
|
||||
public function summaryFields() {
|
||||
$fields = $this->stat('summary_fields');
|
||||
|
||||
|
||||
// if fields were passed in numeric array,
|
||||
// convert to an associative array
|
||||
if($fields && array_key_exists(0, $fields)) {
|
||||
$fields = array_combine(array_values($fields), array_values($fields));
|
||||
}
|
||||
|
||||
|
||||
if (!$fields) {
|
||||
$fields = array();
|
||||
// try to scaffold a couple of usual suspects
|
||||
@ -2397,11 +2408,11 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
if (is_int($name)) {
|
||||
$filters[$type] = $this->relObject($type)->defaultSearchFilter();
|
||||
} else {
|
||||
if (is_array($type)) {
|
||||
if(is_array($type)) {
|
||||
$filter = current($type);
|
||||
$filters[$name] = new $filter($name);
|
||||
} else {
|
||||
if (is_subclass_of($type, 'SearchFilter')) {
|
||||
if(is_subclass_of($type, 'SearchFilter')) {
|
||||
$filters[$name] = new $type($name);
|
||||
} else {
|
||||
$filters[$name] = $this->relObject($name)->defaultSearchFilter($name);
|
||||
@ -2596,7 +2607,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
* default display in the search form.
|
||||
*/
|
||||
public static $searchable_fields_labels = null;
|
||||
|
||||
|
||||
/**
|
||||
* Provides a default list of fields to be used by a 'summary'
|
||||
* view of this object.
|
||||
|
169
core/model/YamlFixture.php
Normal file
169
core/model/YamlFixture.php
Normal file
@ -0,0 +1,169 @@
|
||||
<?php
|
||||
/**
|
||||
* Uses the Spyc library to parse a YAML document (see http://yaml.org).
|
||||
* YAML is a simple markup languages that uses tabs and colons instead of the more verbose XML tags,
|
||||
* and because of this much better for developers creating files by hand.
|
||||
*
|
||||
* The contents of the YAML file are broken into three levels:
|
||||
* - Top level: class names - Page and ErrorPage. This is the name of the dataobject class that should be created.
|
||||
* The fact that ErrorPage is actually a subclass is irrelevant to the system populating the database.
|
||||
* Each identifier you specify delimits a new database record.
|
||||
* This means that every record needs to have an identifier, whether you use it or not.
|
||||
* - Third level: fields - each field for the record is listed as a 3rd level entry.
|
||||
* In most cases, the fieldÕs raw content is provided.
|
||||
* However, if you want to define a relationship, you can do so using "=>"
|
||||
*
|
||||
* There are a couple of lines like this:
|
||||
* @example Parent: =>Page.about
|
||||
* This will tell the system to set the ParentID database field to the ID of the Page object with the identifier ÒaboutÓ.
|
||||
* This can be used on any has-one or many-many relationship.
|
||||
* Note that we use the name of the relationship (Parent), and not the name of the database field (ParentID)
|
||||
*
|
||||
* On many-many relationships, you should specify a comma separated list of values.
|
||||
* @example MyRelation: =>Class.inst1,=>Class.inst2,=>Class.inst3
|
||||
* An crucial thing to note is that the YAML file specifies DataObjects, not database records.
|
||||
* The database is populated by instantiating DataObject objects, setting the fields listed, and calling write().
|
||||
* This means that any onBeforeWrite() or default value logic will be executed as part of the test.
|
||||
* This forms the basis of our testURLGeneration() test above.
|
||||
*
|
||||
* For example, the URLSegment value of Page.staffduplicate is the same as the URLSegment value of Page.staff.
|
||||
* When the fixture is set up, the URLSegment value of Page.staffduplicate will actually be my-staff-2.
|
||||
*
|
||||
* Finally, be aware that requireDefaultRecords() is not called by the database populator -
|
||||
* so you will need to specify standard pages such as 404 and home in your YAML file.
|
||||
*
|
||||
* <code>
|
||||
* Page:
|
||||
* home:
|
||||
* Title: Home
|
||||
* about:
|
||||
* Title: About Us
|
||||
* staff:
|
||||
* Title: Staff
|
||||
* URLSegment: my-staff
|
||||
* Parent: =>Page.about
|
||||
* staffduplicate:
|
||||
* Title: Staff
|
||||
* URLSegment: my-staff
|
||||
* Parent: =>Page.about
|
||||
* products:
|
||||
* Title: Products
|
||||
* ErrorPage:
|
||||
* 404:
|
||||
* Title: Page not Found
|
||||
* ErrorCode: 404
|
||||
* </code>
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage core
|
||||
*
|
||||
* @see http://spyc.sourceforge.net/
|
||||
*
|
||||
* @todo Write unit test for YamlFixture
|
||||
*
|
||||
* @param $fixtureFile The location of the .yml fixture file, relative to the site base dir
|
||||
*/
|
||||
class YamlFixture extends Object {
|
||||
|
||||
/**
|
||||
* The location of the .yml fixture file, relative to the site base dir
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $fixtureFile;
|
||||
|
||||
/**
|
||||
* Array of fixture items
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fixtureDictionary;
|
||||
|
||||
function __construct($fixtureFile) {
|
||||
$this->fixtureFile = $fixtureFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
protected function idFromFixture($className, $identifier) {
|
||||
return $this->fixtureDictionary[$className][$identifier];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all of the IDs in the fixture of a particular class name.
|
||||
*
|
||||
* @return A map of fixture-identifier => object-id
|
||||
*/
|
||||
protected function allFixtureIDs($className) {
|
||||
return $this->fixtureDictionary[$className];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get 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
|
||||
*/
|
||||
protected function objFromFixture($className, $identifier) {
|
||||
return DataObject::get_by_id($className, $this->idFromFixture($className, $identifier));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a YAML fixture file into the database.
|
||||
* Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture
|
||||
*/
|
||||
function saveIntoDatabase() {
|
||||
$parser = new Spyc();
|
||||
$fixtureContent = $parser->load(Director::baseFolder().'/'.$this->fixtureFile);
|
||||
|
||||
$this->fixtureDictionary = array();
|
||||
|
||||
foreach($fixtureContent as $dataClass => $items) {
|
||||
foreach($items as $identifier => $fields) {
|
||||
$obj = new $dataClass();
|
||||
foreach($fields as $fieldName => $fieldVal) {
|
||||
if($obj->many_many($fieldName) || $obj->has_many($fieldName)) {
|
||||
$parsedItems = array();
|
||||
$items = split(' *, *',trim($fieldVal));
|
||||
foreach($items as $item) {
|
||||
$parsedItems[] = $this->parseFixtureVal($item);
|
||||
}
|
||||
$obj->write();
|
||||
if($obj->has_many($fieldName)) {
|
||||
$obj->getComponents($fieldName)->setByIDList($parsedItems);
|
||||
} elseif($obj->many_many($fieldName)) {
|
||||
$obj->getManyManyComponents($fieldName)->setByIDList($parsedItems);
|
||||
}
|
||||
} elseif($obj->has_one($fieldName)) {
|
||||
$obj->{$fieldName . 'ID'} = $this->parseFixtureVal($fieldVal);
|
||||
} else {
|
||||
$obj->$fieldName = $this->parseFixtureVal($fieldVal);
|
||||
}
|
||||
}
|
||||
$obj->write();
|
||||
|
||||
// Populate the dictionary with the ID
|
||||
$this->fixtureDictionary[$dataClass][$identifier] = $obj->ID;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a value from a fixture file. If it starts with => it will get an ID from the fixture dictionary
|
||||
*/
|
||||
protected function parseFixtureVal($fieldVal) {
|
||||
// Parse a dictionary reference - used to set foreign keys
|
||||
if(substr($fieldVal,0,2) == '=>') {
|
||||
list($a, $b) = explode('.', substr($fieldVal,2), 2);
|
||||
return $this->fixtureDictionary[$a][$b];
|
||||
|
||||
// Regular field value setting
|
||||
} else {
|
||||
return $fieldVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
@ -21,6 +21,13 @@ abstract class DBField extends ViewableData {
|
||||
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* Subclass of {@link SearchFilter} for usage in {@link defaultSearchFilter()}.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $default_search_filter_class = 'PartialMatchFilter';
|
||||
|
||||
/**
|
||||
* @var $default mixed Default-value in the database.
|
||||
* Might be overridden on DataObject-level, but still useful for setting defaults on
|
||||
@ -223,7 +230,8 @@ abstract class DBField extends ViewableData {
|
||||
*/
|
||||
public function defaultSearchFilter($name = false) {
|
||||
$name = ($name) ? $name : $this->name;
|
||||
return new PartialMatchFilter($name);
|
||||
$filterClass = $this->stat('default_search_filter_class');
|
||||
return new $filterClass($name);
|
||||
}
|
||||
|
||||
/**
|
||||
|
30
core/model/fieldtypes/PrimaryKey.php
Normal file
30
core/model/fieldtypes/PrimaryKey.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* A special type Int field used for primary keys.
|
||||
*
|
||||
* @todo Allow for custom limiting/filtering of scaffoldFormField dropdown
|
||||
*
|
||||
* @param string $name
|
||||
* @param DataOject $object The object that this is primary key for (should have a relation with $name)
|
||||
*/
|
||||
class PrimaryKey extends Int {
|
||||
/**
|
||||
* @var DataObject
|
||||
*/
|
||||
protected $object;
|
||||
|
||||
function __construct($name, $object) {
|
||||
$this->object = $object;
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
||||
public function scaffoldFormField($title = null) {
|
||||
$objs = DataObject::get($this->object->class);
|
||||
$map = ($objs) ? $objs->toDropdownMap() : false;
|
||||
|
||||
return new DropdownField($this->name, $title, $map, null, null, ' ');
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@ -1,10 +1,4 @@
|
||||
<?php
|
||||
/**
|
||||
* @package sapphire
|
||||
* @subpackage testing
|
||||
*/
|
||||
|
||||
|
||||
require_once 'TestRunner.php';
|
||||
if(hasPhpUnit()) {
|
||||
require_once 'PHPUnit/Framework.php';
|
||||
@ -13,18 +7,27 @@ require_once 'PHPUnit/Framework.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 sapphire
|
||||
* @subpackage testing
|
||||
*/
|
||||
class SapphireTest extends PHPUnit_Framework_TestCase {
|
||||
/**
|
||||
* Path to fixture data for this test run
|
||||
* Path to fixture data for this test run.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $fixture_file = null;
|
||||
|
||||
protected $originalMailer;
|
||||
|
||||
protected $mailer;
|
||||
|
||||
/**
|
||||
* @var YamlFixture
|
||||
*/
|
||||
protected $fixture;
|
||||
|
||||
function setUp() {
|
||||
$className = get_class($this);
|
||||
$fixtureFile = eval("return {$className}::\$fixture_file;");
|
||||
@ -47,10 +50,9 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
|
||||
|
||||
$dbadmin = new DatabaseAdmin();
|
||||
$dbadmin->doBuild(true, false, true);
|
||||
|
||||
// Load the fixture into the database
|
||||
$className = get_class($this);
|
||||
$this->loadFixture($fixtureFile);
|
||||
|
||||
$this->fixture = new YamlFixture($fixtureFile);
|
||||
$this->fixture->saveIntoDatabase();
|
||||
}
|
||||
|
||||
// Set up email
|
||||
@ -100,51 +102,8 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
|
||||
$parser = new Spyc();
|
||||
$fixtureContent = $parser->load(Director::baseFolder().'/'.$fixtureFile);
|
||||
|
||||
$this->fixtureDictionary = array();
|
||||
|
||||
foreach($fixtureContent as $dataClass => $items) {
|
||||
foreach($items as $identifier => $fields) {
|
||||
$obj = new $dataClass();
|
||||
foreach($fields as $fieldName => $fieldVal) {
|
||||
if($obj->many_many($fieldName) || $obj->has_many($fieldName)) {
|
||||
$parsedItems = array();
|
||||
$items = split(' *, *',trim($fieldVal));
|
||||
foreach($items as $item) {
|
||||
$parsedItems[] = $this->parseFixtureVal($item);
|
||||
}
|
||||
$obj->write();
|
||||
if($obj->has_many($fieldName)) {
|
||||
$obj->getComponents($fieldName)->setByIDList($parsedItems);
|
||||
} elseif($obj->many_many($fieldName)) {
|
||||
$obj->getManyManyComponents($fieldName)->setByIDList($parsedItems);
|
||||
}
|
||||
} elseif($obj->has_one($fieldName)) {
|
||||
$obj->{$fieldName . 'ID'} = $this->parseFixtureVal($fieldVal);
|
||||
} else {
|
||||
$obj->$fieldName = $this->parseFixtureVal($fieldVal);
|
||||
}
|
||||
}
|
||||
$obj->write();
|
||||
|
||||
// Populate the dictionary with the ID
|
||||
$this->fixtureDictionary[$dataClass][$identifier] = $obj->ID;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a value from a fixture file. If it starts with => it will get an ID from the fixture dictionary
|
||||
*/
|
||||
protected function parseFixtureVal($fieldVal) {
|
||||
// Parse a dictionary reference - used to set foreign keys
|
||||
if(substr($fieldVal,0,2) == '=>') {
|
||||
list($a, $b) = explode('.', substr($fieldVal,2), 2);
|
||||
return $this->fixtureDictionary[$a][$b];
|
||||
|
||||
// Regular field value setting
|
||||
} else {
|
||||
return $fieldVal;
|
||||
}
|
||||
$this->fixture = new YamlFixture($fixtureFile);
|
||||
$this->fixture->saveIntoDatabase();
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
|
@ -454,6 +454,29 @@ HTML;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a fieldname and converts camelcase to spaced
|
||||
* words. Also resolves combined fieldnames with dot syntax
|
||||
* to spaced words.
|
||||
*
|
||||
* @example 'TotalAmount' will return 'Total Amount'
|
||||
* @example 'Organisation.ZipCode' will return 'Organisation Zip Code'
|
||||
*
|
||||
* @param string $fieldName
|
||||
* @return string
|
||||
*/
|
||||
public function name_to_label($fieldName) {
|
||||
if(strpos('.', $fieldName) !== false) {
|
||||
$parts = explode('.', $fieldName);
|
||||
$label = $parts[count($parts)-2] . ' ' . $parts[count($parts)-1];
|
||||
} else {
|
||||
$label = $fieldName;
|
||||
}
|
||||
$label = preg_replace("/([a-z]+)([A-Z])/","$1 $2", $label);
|
||||
|
||||
return $label;
|
||||
}
|
||||
|
||||
// ###################
|
||||
// DEPRECATED
|
||||
// ###################
|
||||
|
@ -176,6 +176,11 @@ class TableListField extends FormField {
|
||||
*/
|
||||
public $groupByField = null;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $extraLinkParams;
|
||||
|
||||
function __construct($name, $sourceClass, $fieldList = null, $sourceFilter = null,
|
||||
$sourceSort = null, $sourceJoin = null) {
|
||||
|
||||
@ -603,11 +608,7 @@ JS
|
||||
function getPermissions($arr) {
|
||||
return $this->permissions;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* #################################
|
||||
* Pagination
|
||||
@ -639,14 +640,29 @@ JS
|
||||
return $_REQUEST['ctf'][$this->Name()]['start'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array
|
||||
*/
|
||||
function setExtraLinkParams($params){
|
||||
$this->extraLinkParams = $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
function getExtraLinkParams(){
|
||||
return $this->extraLinkParams;
|
||||
}
|
||||
|
||||
function FirstLink() {
|
||||
$start = 0;
|
||||
|
||||
if(!isset($_REQUEST['ctf'][$this->Name()]['start']) || !is_numeric($_REQUEST['ctf'][$this->Name()]['start']) || $_REQUEST['ctf'][$this->Name()]['start'] == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->Link() . "/ajax_refresh?ctf[{$this->Name()}][start]={$start}{$this->filterString()}";
|
||||
$link = $this->Link() . "/ajax_refresh?ctf[{$this->Name()}][start]={$start}";
|
||||
if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
|
||||
return $link;
|
||||
}
|
||||
|
||||
function PrevLink() {
|
||||
@ -658,7 +674,9 @@ JS
|
||||
|
||||
$start = ($_REQUEST['ctf'][$this->Name()]['start'] - $this->pageSize < 0) ? 0 : $_REQUEST['ctf'][$this->Name()]['start'] - $this->pageSize;
|
||||
|
||||
return $this->Link() . "/ajax_refresh?ctf[{$this->Name()}][start]=$start{$this->filterString()}";
|
||||
$link = $this->Link() . "/ajax_refresh?ctf[{$this->Name()}][start]={$start}";
|
||||
if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
|
||||
return $link;
|
||||
}
|
||||
|
||||
function NextLink() {
|
||||
@ -667,20 +685,22 @@ JS
|
||||
if($currentStart >= $start-1) {
|
||||
return null;
|
||||
}
|
||||
return $this->Link() . "/ajax_refresh?ctf[{$this->Name()}][start]={$start}{$this->filterString()}";
|
||||
$link = $this->Link() . "/ajax_refresh?ctf[{$this->Name()}][start]={$start}";
|
||||
if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
|
||||
return $link;
|
||||
}
|
||||
|
||||
function LastLink() {
|
||||
$pageSize = ($this->TotalCount() % $this->pageSize > 0) ? $this->TotalCount() % $this->pageSize : $this->pageSize;
|
||||
$start = $this->totalCount - $pageSize;
|
||||
|
||||
|
||||
$start = $this->TotalCount() - $pageSize;
|
||||
// Check if there is only one page, or if we are on last page
|
||||
if($this->totalCount <= $pageSize || (isset($_REQUEST['ctf'][$this->Name()]['start']) && $_REQUEST['ctf'][$this->Name()]['start'] >= $start)) {
|
||||
if($this->TotalCount() <= $pageSize || (isset($_REQUEST['ctf'][$this->Name()]['start']) && $_REQUEST['ctf'][$this->Name()]['start'] >= $start)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->Link() . "/ajax_refresh?ctf[{$this->Name()}][start]=$start{$this->filterString()}";
|
||||
$link = $this->Link() . "/ajax_refresh?ctf[{$this->Name()}][start]={$start}";
|
||||
if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
|
||||
return $link;
|
||||
}
|
||||
|
||||
function FirstItem() {
|
||||
@ -739,7 +759,7 @@ JS
|
||||
*
|
||||
* @return String URL-parameters
|
||||
*/
|
||||
function filterString() {
|
||||
function filterString() {
|
||||
|
||||
}
|
||||
|
||||
@ -1004,9 +1024,9 @@ class TableListField_Item extends ViewableData {
|
||||
$relationMethod = $fieldNameParts[$j];
|
||||
$idField = $relationMethod . 'ID';
|
||||
if($j == sizeof($fieldNameParts)-1) {
|
||||
$value = $tmpItem->$relationMethod;
|
||||
if($tmpItem) $value = $tmpItem->$relationMethod;
|
||||
} else {
|
||||
$tmpItem = $tmpItem->$relationMethod();
|
||||
if($tmpItem) $tmpItem = $tmpItem->$relationMethod();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -113,7 +113,7 @@ TableListField.prototype = {
|
||||
new Ajax.Request(
|
||||
el.href,
|
||||
{
|
||||
postBody: 'update=1',
|
||||
postBody: 'update=1&paginate=1',
|
||||
onComplete: Ajax.Evaluator,
|
||||
onFailure: this.ajaxErrorHandler.bind(this)
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
<% if ShowPagination %>
|
||||
<div class="PageControls">
|
||||
<% if LastLink %><a class="Last" href="$LastLink" title="<% _t('VIEWLAST', 'View last') %> $PageSize"><img src="cms/images/pagination/record-last.png" alt="<% _t('VIEWLAST', 'View last') %> $PageSize" /></a>
|
||||
<% else %><span class="Last"><img src="cms/images/pagination/record-last-g.png" alt="<% _t('VIEWLAST', 'View last') %> $PageSize" /></span><% end_if %>
|
||||
<% if FirstLink %><a class="First" href="$FirstLink" title="<% _t('VIEWFIRST', 'View first') %> $PageSize"><img src="cms/images/pagination/record-first.png" alt="<% _t('VIEWFIRST', 'View first') %> $PageSize" /></a>
|
||||
<% else %><span class="First"><img src="cms/images/pagination/record-first-g.png" alt="<% _t('VIEWFIRST', 'View first') %> $PageSize" /></span><% end_if %>
|
||||
<% if PrevLink %><a class="Prev" href="$PrevLink" title="<% _t('VIEWPREVIOUS', 'View previous') %> $PageSize"><img src="cms/images/pagination/record-prev.png" alt="<% _t('VIEWPREVIOUS', 'View previous') %> $PageSize" /></a>
|
||||
@ -11,5 +9,7 @@
|
||||
</span>
|
||||
<% if NextLink %><a class="Next" href="$NextLink" title="<% _t('VIEWNEXT', 'View next') %> $PageSize"><img src="cms/images/pagination/record-next.png" alt="<% _t('VIEWNEXT', 'View next') %> $PageSize" /></a>
|
||||
<% else %><img class="Next" src="cms/images/pagination/record-next-g.png" alt="<% _t('VIEWNEXT', 'View next') %> $PageSize" /><% end_if %>
|
||||
<% if LastLink %><a class="Last" href="$LastLink" title="<% _t('VIEWLAST', 'View last') %> $PageSize"><img src="cms/images/pagination/record-last.png" alt="<% _t('VIEWLAST', 'View last') %> $PageSize" /></a>
|
||||
<% else %><span class="Last"><img src="cms/images/pagination/record-last-g.png" alt="<% _t('VIEWLAST', 'View last') %> $PageSize" /></span><% end_if %>
|
||||
</div>
|
||||
<% end_if %>
|
@ -14,25 +14,25 @@ class DataObjectTest extends SapphireTest {
|
||||
function testDelete() {
|
||||
// Test deleting using delete() on the DataObject
|
||||
// Get the first page
|
||||
$page = $this->objFromFixture('Page', 'page1');
|
||||
$page = $this->fixture->objFromFixture('Page', 'page1');
|
||||
// Check the page exists before deleting
|
||||
$this->assertTrue(is_object($page) && $page->exists());
|
||||
// Delete the page
|
||||
$page->delete();
|
||||
// Check that page does not exist after deleting
|
||||
$page = $this->objFromFixture('Page', 'page1');
|
||||
$page = $this->fixture->objFromFixture('Page', 'page1');
|
||||
$this->assertTrue(!$page || !$page->exists());
|
||||
|
||||
|
||||
// Test deleting using DataObject::delete_by_id()
|
||||
// Get the second page
|
||||
$page2 = $this->objFromFixture('Page', 'page2');
|
||||
$page2 = $this->fixture->objFromFixture('Page', 'page2');
|
||||
// Check the page exists before deleting
|
||||
$this->assertTrue(is_object($page2) && $page2->exists());
|
||||
// Delete the page
|
||||
DataObject::delete_by_id('Page', $page2->ID);
|
||||
// Check that page does not exist after deleting
|
||||
$page2 = $this->objFromFixture('Page', 'page2');
|
||||
$page2 = $this->fixture->objFromFixture('Page', 'page2');
|
||||
$this->assertTrue(!$page2 || !$page2->exists());
|
||||
}
|
||||
|
||||
@ -91,7 +91,7 @@ class DataObjectTest extends SapphireTest {
|
||||
|
||||
|
||||
// Test get_by_id()
|
||||
$homepage = $this->objFromFixture('Page', 'home');
|
||||
$homepage = $this->fixture->objFromFixture('Page', 'home');
|
||||
$page = DataObject::get_by_id('Page', $homepage->ID);
|
||||
$this->assertTrue($page->Title == 'Home');
|
||||
|
||||
@ -130,7 +130,7 @@ class DataObjectTest extends SapphireTest {
|
||||
*
|
||||
*/
|
||||
function testWritePropertyWithoutDBField() {
|
||||
$page = $this->objFromFixture('Page', 'page1');
|
||||
$page = $this->fixture->objFromFixture('Page', 'page1');
|
||||
$page->ParentID = 99;
|
||||
$page->write();
|
||||
// reload the page from the database
|
||||
@ -144,7 +144,7 @@ class DataObjectTest extends SapphireTest {
|
||||
* - Test the IDs on the DataObjects are set correctly
|
||||
*/
|
||||
function testHasManyRelationships() {
|
||||
$page = $this->objFromFixture('Page', 'home');
|
||||
$page = $this->fixture->objFromFixture('Page', 'home');
|
||||
|
||||
// Test getComponents() gets the ComponentSet of the other side of the relation
|
||||
$this->assertTrue($page->getComponents('Comments')->Count() == 2);
|
||||
|
@ -26,7 +26,7 @@ class SiteTreeTest extends SapphireTest {
|
||||
);
|
||||
|
||||
foreach($expectedURLs as $fixture => $urlSegment) {
|
||||
$obj = $this->objFromFixture('Page', $fixture);
|
||||
$obj = $this->fixture->objFromFixture('Page', $fixture);
|
||||
$this->assertEquals($urlSegment, $obj->URLSegment);
|
||||
}
|
||||
}
|
||||
@ -35,7 +35,7 @@ class SiteTreeTest extends SapphireTest {
|
||||
* Test that publication copies data to SiteTree_Live
|
||||
*/
|
||||
function testPublishCopiesToLiveTable() {
|
||||
$obj = $this->objFromFixture('Page','about');
|
||||
$obj = $this->fixture->objFromFixture('Page','about');
|
||||
$obj->publish('Stage', 'Live');
|
||||
|
||||
$createdID = DB::query("SELECT ID FROM SiteTree_Live WHERE URLSegment = '$obj->URLSegment'")->value();
|
||||
|
Loading…
Reference in New Issue
Block a user