#3927 ENHANCEMENT Added support for many-many auto-setting relations with a standard ComplexTableField

MINOR Added tests for ComplexTableField - ComplexTableFieldTest


git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/branches/2.3@75738 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Sean Harvey 2009-04-30 05:54:54 +00:00 committed by Sam Minnee
parent 7861a8f740
commit faeed904dc
7 changed files with 422 additions and 32 deletions

View File

@ -60,6 +60,35 @@ class ComponentSet extends DataObjectSet {
$this->joinField = $joinField;
}
/**
* Find the extra field data for a single row of the relationship
* join table, given the known child ID.
*
* @param string $componentName The name of the component
* @param int $childID The ID of the child for the relationship
* @return array Map of fieldName => fieldValue
*/
function getExtraData($componentName, $childID) {
$ownerObj = $this->ownerObj;
$parentField = $this->ownerClass . 'ID';
$childField = ($this->childClass == $this->ownerClass) ? 'ChildID' : ($this->childClass . 'ID');
$result = array();
if(!$componentName) return false;
// @todo Optimize into a single query instead of one per extra field
$extraFields = $ownerObj->many_many_extraFields($componentName);
if($extraFields) {
foreach($extraFields as $fieldName => $dbFieldSpec) {
$query = DB::query("SELECT $fieldName FROM {$this->tableName} WHERE $parentField = '{$this->ownerObj->ID}' AND $childField = '{$childID}'");
$value = $query->value();
$result[$fieldName] = $value;
}
}
return $result;
}
/**
* Get an array of all the IDs in this component set, where the keys are the same as the
* values.
@ -95,7 +124,6 @@ class ComponentSet extends DataObjectSet {
}
$item = DataObject::get_by_id($this->childClass, $item);
if(!$item) return;
}
@ -292,4 +320,4 @@ OUT;
}
}
?>
?>

View File

@ -1421,6 +1421,90 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return isset($items) ? $items : null;
}
/**
* Return the many-to-many extra fields specification.
*
* If you don't specify a component name, it returns all
* extra fields for all components available.
*
* @param string $component Name of component
* @return array
*/
public function many_many_extraFields($component = null) {
$classes = ClassInfo::ancestry($this);
foreach($classes as $class) {
if(in_array($class, array('ViewableData', 'Object', 'DataObject'))) continue;
// Find extra fields for one component
if($component) {
$SNG_class = singleton($class);
$extraFields = $SNG_class->stat('many_many_extraFields');
// Extra fields are immediately available on this class
if(isset($extraFields[$component])) {
return $extraFields[$component];
}
$manyMany = $SNG_class->stat('many_many');
$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
if($candidate) {
$SNG_candidate = singleton($candidate);
$candidateManyMany = $SNG_candidate->stat('belongs_many_many');
// Find the relation given the class
if($candidateManyMany) foreach($candidateManyMany as $relation => $relatedClass) {
if($relatedClass == $class) {
$relationName = $relation;
}
}
$extraFields = $SNG_candidate->stat('many_many_extraFields');
if(isset($extraFields[$relationName])) {
return $extraFields[$relationName];
}
}
$manyMany = $SNG_class->stat('belongs_many_many');
$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
if($candidate) {
$SNG_candidate = singleton($candidate);
$candidateManyMany = $SNG_candidate->stat('many_many');
// Find the relation given the class
if($candidateManyMany) foreach($candidateManyMany as $relation => $relatedClass) {
if($relatedClass == $class) {
$relationName = $relation;
}
}
$extraFields = $SNG_candidate->stat('many_many_extraFields');
if(isset($extraFields[$relationName])) {
return $extraFields[$relationName];
}
}
} else {
// Find all the extra fields for all components
$newItems = eval("return (array){$class}::\$many_many_extraFields;");
foreach($newItems as $k => $v) {
if(!is_array($v)) {
user_error(
"$class::\$many_many_extraFields has a bad entry: "
. var_export($k, true) . " => " . var_export($v, true)
. ". Each many_many_extraFields entry should map to a field specification array.",
E_USER_ERROR
);
}
}
return isset($items) ? array_merge($newItems, $items) : $newItems;
}
}
}
/**
* Return information about a many-to-many component.
* The return value is an array of (parentclass, childclass). If $component is null, then all many-many

View File

@ -455,61 +455,109 @@ JS;
* this method.
*/
function getCustomFieldsFor($childData) {
// If the fieldset is passed, use it
if(is_a($this->detailFormFields,"Fieldset")) {
if($this->detailFormFields instanceof Fieldset) {
return $this->detailFormFields;
// Else use the formfields returned from the object via a string method call.
} else {
if(!is_string($this->detailFormFields)) $this->detailFormFields = "getCMSFields";
$functioncall = $this->detailFormFields;
if(!$childData->hasMethod($functioncall)) $functioncall = "getCMSFields";
return $childData->$functioncall();
}
}
$fieldsMethod = $this->detailFormFields;
if(!is_string($fieldsMethod)) {
$this->detailFormFields = 'getCMSFields';
$fieldsMethod = 'getCMSFields';
}
if(!$childData->hasMethod($fieldsMethod)) {
$fieldsMethod = 'getCMSFields';
}
$fields = $childData->$fieldsMethod();
}
if(!$this->relationAutoSetting) {
return $fields;
}
if($this->sourceID()) {
$parentClass = DataObject::get_by_id($this->getParentClass(), $this->sourceID());
} else {
$parentClass = singleton($this->getParentClass());
}
$manyManyExtraFields = $parentClass->many_many_extraFields($this->name);
if($manyManyExtraFields) {
foreach($manyManyExtraFields as $fieldName => $fieldSpec) {
$dbField = new Varchar('ctf[extraFields][' . $fieldName . ']');
$fields->addFieldToTab('Root.Main', $dbField->scaffoldFormField($fieldName));
}
}
return $fields;
}
function getFieldsFor($childData) {
// See if our parent class has any many_many relations by this source class
if($this->sourceID()) {
$parentClass = DataObject::get_by_id($this->getParentClass(), $this->sourceID());
} else {
$parentClass = singleton($this->getParentClass());
}
$manyManyRelations = $parentClass->many_many();
$manyManyRelationName = null;
$manyManyComponentSet = null;
if($manyManyRelations) foreach($manyManyRelations as $relation => $class) {
if($class == $this->sourceClass()) {
$manyManyRelationName = $relation;
}
}
// Add the relation value to related records
if(!$childData->ID && $this->getParentClass()) {
// make sure the relation-link is existing, even if we just add the sourceClass and didn't save it
$parentIDName = $this->getParentIdName( $this->getParentClass(), $this->sourceClass() );
$childData->$parentIDName = $this->sourceID();
}
$detailFields = $this->getCustomFieldsFor($childData);
// Loading of extra field values for editing an existing record
if($manyManyRelationName && $childData->ID) {
$manyManyComponentSet = $parentClass->getManyManyComponents($manyManyRelationName);
$extraData = $manyManyComponentSet->getExtraData($manyManyRelationName, $childData->ID);
if($extraData) foreach($extraData as $fieldName => $fieldValue) {
$field = $detailFields->dataFieldByName('ctf[extraFields][' . $fieldName . ']');
$field->setValue($fieldValue);
}
}
// the ID field confuses the Controller-logic in finding the right view for ReferencedField
$detailFields->removeByName('ID');
// only add childID if we're not adding a record
if($childData->ID) {
$detailFields->push(new HiddenField("ctf[childID]","",$childData->ID));
$detailFields->push(new HiddenField('ctf[childID]', '', $childData->ID));
}
// add a namespaced ID instead thats "converted" by saveComplexTableField()
$detailFields->push(new HiddenField("ctf[ClassName]","",$this->sourceClass()));
$detailFields->push(new HiddenField('ctf[ClassName]', '', $this->sourceClass()));
if($this->getParentClass()) {
$parentIdName = $this->getParentIdName($this->getParentClass(), $this->sourceClass());
/*
if(!$parentIdName) {
user_error("ComplexTableField::DetailForm() Cannot automatically
determine 'has-one'-relationship to parent class " . $this->ctf->getParentClass() . ",
please use setParentClass() to set it manually",
E_USER_WARNING);
return;
if($manyManyRelationName && $this->relationAutoSetting) {
$detailFields->push(new HiddenField('ctf[manyManyRelation]', '', $manyManyRelationName));
$detailFields->push(new HiddenField('ctf[parentClass]', '', $this->getParentClass()));
$detailFields->push(new HiddenField('ctf[sourceID]', '', $this->sourceID()));
}
*/
$parentIdName = $this->getParentIdName($this->getParentClass(), $this->sourceClass());
if($parentIdName) {
// add relational fields
$detailFields->push(new HiddenField("ctf[parentClass]"," ",$this->getParentClass()));
if( $this->relationAutoSetting ) {
$detailFields->push(new HiddenField('ctf[parentClass]', '', $this->getParentClass()));
if($this->relationAutoSetting) {
// Hack for model admin: model admin will have included a dropdown for the relation itself
$detailFields->removeByName($parentIdName);
$detailFields->push(new HiddenField("$parentIdName"," ",$this->sourceID()));
$detailFields->push(new HiddenField($parentIdName, '', $this->sourceID()));
}
}
}
@ -574,6 +622,8 @@ JS;
* even if there is no action relevant for the main controller (to provide the instance of ComplexTableField
* which in turn saves the record.
*
* This is for adding new item records. {@link ComplexTableField_ItemRequest::saveComplexTableField()}
*
* @see Form::ReferencedField
*/
function saveComplexTableField($data, $form, $params) {
@ -581,17 +631,38 @@ JS;
$childData = new $className();
$form->saveInto($childData);
$childData->write();
// Save the many many relationship if it's available
if(isset($data['ctf']['manyManyRelation'])) {
$parentRecord = DataObject::get_by_id($data['ctf']['parentClass'], (int) $data['ctf']['sourceID']);
$relationName = $data['ctf']['manyManyRelation'];
$extraFields = array();
if(isset($data['ctf']['extraFields'])) {
foreach($data['ctf']['extraFields'] as $field => $value) {
$extraFields[$field] = $value;
}
}
$componentSet = $parentRecord->getManyManyComponents($relationName);
$componentSet->add($childData, $extraFields);
}
$referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null;
$closeLink = sprintf(
'<small><a href="' . $_SERVER['HTTP_REFERER'] . '" onclick="javascript:window.top.GB_hide(); return false;">(%s)</a></small>',
'<small><a href="' . $referrer . '" onclick="javascript:window.top.GB_hide(); return false;">(%s)</a></small>',
_t('ComplexTableField.CLOSEPOPUP', 'Close Popup')
);
$message = sprintf(
_t('ComplexTableField.SUCCESSADD', 'Added %s %s %s'),
$childData->singular_name(),
'<a href="' . $this->Link() . '/item/' . $childData->ID . '/edit">' . $childData->Title . '</a>',
$closeLink
);
$form->sessionMessage($message, 'good');
Director::redirectBack();
@ -724,14 +795,31 @@ class ComplexTableField_ItemRequest extends RequestHandler {
* even if there is no action relevant for the main controller (to provide the instance of ComplexTableField
* which in turn saves the record.
*
* This is for editing existing item records. {@link ComplexTableField::saveComplexTableField()}
*
* @see Form::ReferencedField
*/
function saveComplexTableField($data, $form, $request) {
$dataObject = $this->dataObj();
$form->saveInto($dataObject);
$dataObject->write();
// Save the many many relationship if it's available
if(isset($data['ctf']['manyManyRelation'])) {
$parentRecord = DataObject::get_by_id($data['ctf']['parentClass'], (int) $data['ctf']['sourceID']);
$relationName = $data['ctf']['manyManyRelation'];
$extraFields = array();
if(isset($data['ctf']['extraFields'])) {
foreach($data['ctf']['extraFields'] as $field => $value) {
$extraFields[$field] = $value;
}
}
$componentSet = $parentRecord->getManyManyComponents($relationName);
$componentSet->add($dataObject, $extraFields);
}
$closeLink = sprintf(
'<small><a href="' . $_SERVER['HTTP_REFERER'] . '" onclick="javascript:window.top.GB_hide(); return false;">(%s)</a></small>',
_t('ComplexTableField.CLOSEPOPUP', 'Close Popup')
@ -742,6 +830,7 @@ class ComplexTableField_ItemRequest extends RequestHandler {
'<a href="' . $this->Link() . '">"' . $dataObject->Title . '"</a>',
$closeLink
);
$form->sessionMessage($message, 'good');
Director::redirectBack();

View File

@ -187,6 +187,7 @@ class Form extends RequestHandler {
}
}
// Protection against CSRF attacks
if($this->securityTokenEnabled()) {
$securityID = Session::get('SecurityID');

View File

@ -553,6 +553,23 @@ class DataObjectTest extends SapphireTest {
$this->assertType('RedirectorPage', $changedPage);
$this->assertEquals($changedPage->ClassName, 'RedirectorPage');
}
function testManyManyExtraFields() {
$player = $this->fixture->objFromFixture('DataObjectTest_Player', 'player1');
$team = $this->fixture->objFromFixture('DataObjectTest_Team', 'team1');
// Extra fields are immediately available on the Team class (defined in $many_many_extraFields)
$teamExtraFields = $team->many_many_extraFields('Players');
$this->assertEquals($teamExtraFields, array(
'Position' => 'Varchar(100)'
));
// We'll have to go through the relation to get the extra fields on Player
$playerExtraFields = $player->many_many_extraFields('Teams');
$this->assertEquals($playerExtraFields, array(
'Position' => 'Varchar(100)'
));
}
}
@ -583,6 +600,12 @@ class DataObjectTest_Team extends DataObject implements TestOnly {
'Players' => 'DataObjectTest_Player'
);
static $many_many_extraFields = array(
'Players' => array(
'Position' => 'Varchar(100)'
)
);
function getDynamicField() {
return 'dynamicfield';
}

View File

@ -0,0 +1,155 @@
<?php
/**
* @package sapphire
* @subpackage tests
*/
class ComplexTableFieldTest extends FunctionalTest {
static $fixture_file = 'sapphire/tests/forms/ComplexTableFieldTest.yml';
/**
* An instance of {@link Controller} used for
* running tests against.
*
* @var Controller object
*/
protected $controller;
/**
* An instance of {@link Form} that is taken
* from the test controller, used for testing.
*
* @var Form object
*/
protected $form;
function setUp() {
parent::setUp();
$this->controller = new ComplexTableFieldTest_Controller();
$this->form = $this->controller->Form();
}
function testCorrectNumberOfRowsInTable() {
$field = $this->form->dataFieldByName('Players');
$parser = new CSSContentParser($field->FieldHolder());
/* There are 2 players (rows) in the table */
$this->assertEquals(count($parser->getBySelector('tbody tr')), 2, 'There are 2 players (rows) in the table');
/* There are 2 CTF items in the DataObjectSet */
$this->assertEquals($field->Items()->Count(), 2, 'There are 2 CTF items in the DataObjectSet');
}
function testDetailFormDisplaysWithCorrectFields() {
$field = $this->form->dataFieldByName('Players');
$detailForm = $field->add();
$parser = new CSSContentParser($detailForm);
/* There is a field called "Name", which is a text input */
$this->assertNotNull($parser->getBySelector('#Name input'), 'There is a field called "Name", which is a text input');
/* There is a field called "Role" - this field is the extra field for $many_many_extraFields */
$this->assertNotNull($parser->getBySelector('#Role input'), 'There is a field called "Role" - this field is the extra field for $many_many_extraFields');
}
function testAddingNewPlayerWithExtraData() {
$team = DataObject::get_one('ComplexTableFieldTest_Team', "Name = 'The Awesome People'");
$this->post('ComplexTableFieldTest_Controller/Form/field/Players/AddForm', array(
'Name' => 'Bobby Joe',
'ctf' => array(
'extraFields' => array(
'Role' => 'Goalie'
),
'ClassName' => 'ComplexTableFieldTest_Player',
'manyManyRelation' => 'Players',
'parentClass' => 'ComplexTableFieldTest_Team',
'sourceID' => $team->ID
)
));
/* Retrieve the new player record we created */
$newPlayer = DataObject::get_one('ComplexTableFieldTest_Player', "Name = 'Bobby Joe'");
/* A new ComplexTableFieldTest_Player record was created, Name = "Bobby Joe" */
$this->assertNotNull($newPlayer, 'A new ComplexTableFieldTest_Player record was created, Name = "Bobby Joe"');
/* Get the many-many related Teams to the new player that were automatically linked by CTF */
$teams = $newPlayer->getManyManyComponents('Teams');
/* Automatic many-many relation was set correctly on the new player */
$this->assertEquals($teams->Count(), 1, 'Automatic many-many relation was set correctly on the new player');
/* The extra fields have the correct value */
$extraFields = $teams->getExtraData('Teams', $team->ID);
$this->assertEquals($extraFields['Role'], 'Goalie', 'The extra fields have the correct value');
}
}
class ComplexTableFieldTest_Controller extends Controller {
function Link($action = null) {
return "ComplexTableFieldTest_Controller/$action";
}
function Form() {
$team = DataObject::get_one('ComplexTableFieldTest_Team', "Name = 'The Awesome People'");
$playersField = new ComplexTableField(
$this,
'Players',
'ComplexTableFieldTest_Player',
ComplexTableFieldTest_Player::$summary_fields,
'getCMSFields'
);
$playersField->setParentClass('ComplexTableFieldTest_Team');
$form = new Form(
$this,
'Form',
new FieldSet(
new HiddenField('ID', '', $team->ID),
$playersField
),
new FieldSet(
new FormAction('doSubmit', 'Submit')
)
);
$form->disableSecurityToken();
return $form;
}
}
class ComplexTableFieldTest_Player extends DataObject implements TestOnly {
public static $db = array(
'Name' => 'Varchar(100)'
);
public static $many_many = array(
'Teams' => 'ComplexTableFieldTest_Team'
);
public static $many_many_extraFields = array(
'Teams' => array(
'Role' => 'Varchar(100)'
)
);
}
class ComplexTableFieldTest_Team extends DataObject implements TestOnly {
public static $db = array(
'Name' => 'Varchar(100)'
);
public static $belongs_many_many = array(
'Players' => 'ComplexTableFieldTest_Player'
);
}
?>

View File

@ -0,0 +1,10 @@
ComplexTableFieldTest_Player:
p1:
Name: Joe Bloggs
p2:
Name: Some Guy
ComplexTableFieldTest_Team:
t1:
Name: The Awesome People
t2:
Name: Incredible Four