FEATURE Added ForeignKey and used it in relation- and databasefield getters in DataObject

FEATURE Added FormScaffolder for more flexible scaffolding of FieldSets from DataObject metadata
API CHANGE Removed DataObject->addScaffoldRelationFields(), now in separate class FormScaffolder
API CHANGE Changed parameters for DataObject->scaffoldSearchFields() to unify them with scaffoldFormFields()
API CHANGE Added optional $params parameter to DataObject->getCMSFields() to be passed on to scaffoldFormFields()
API CHANGE Renamed DataObject->getFormFields() to getFrontEndFields()
ENHANCEMENT Added $params parameter to all DBField->scaffoldFormField() subclasses
API CHANGE Added third optional parameter $object to DBField::create() to comply with ForeignKey and PrimaryKey constructors

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@64157 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Ingo Schommer 2008-10-13 22:20:41 +00:00
parent 57a84d5408
commit a24ccecb69
18 changed files with 432 additions and 145 deletions

View File

@ -1198,61 +1198,6 @@ class DataObject extends ViewableData implements DataObjectInterface {
}
}
/**
* Add the scaffold-generated relation fields to the given field set
*/
protected function addScaffoldRelationFields($fieldSet) {
// make sure we have a tabset
if(($this->has_many() || $this->many_many()) && !$fieldSet->fieldByName('Root')) {
$oldFields = $fieldSet;
$fieldSet = new FieldSet(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) {
$relationTab = $fieldSet->findOrMakeTab("Root.$relationship", $this->fieldLabel($relationship));
$relationshipFields = singleton($component)->summaryFields();
$foreignKey = $this->getComponentJoinField($relationship);
$ctf = new ComplexTableField(
$this,
$relationship,
$component,
$relationshipFields,
"getCMSFields",
"$foreignKey = $this->ID"
);
$ctf->setPermissions(TableListField::permissions_for_object($component));
$fieldSet->addFieldToTab("Root.$relationship", $ctf);
}
}
if ($this->many_many()) {
foreach($this->many_many() as $relationship => $component) {
$relationTab = $fieldSet->findOrMakeTab("Root.$relationship", $this->fieldLabel($relationship));
$relationshipFields = singleton($component)->summaryFields();
$filterWhere = $this->getManyManyFilter($relationship, $component);
$filterJoin = $this->getManyManyJoin($relationship, $component);
$ctf = new ComplexTableField(
$this,
$relationship,
$component,
$relationshipFields,
"getCMSFields",
$filterWhere,
'',
$filterJoin
);
$ctf->setPermissions(TableListField::permissions_for_object($component));
$ctf->popupClass = "ScaffoldingComplexTableField_Popup";
$fieldSet->addFieldToTab("Root.$relationship", $ctf);
}
}
return $fieldSet;
}
/**
* Pull out a join clause for a many-many relationship.
*
@ -1453,18 +1398,26 @@ class DataObject extends ViewableData implements DataObjectInterface {
*
* @usedby {@link SearchContext}
*
* @param array $restrictFields
* @param array $fieldClasses
* @param array $_params
* 'fieldClasses': Associative array of field names as keys and FormField classes as values
* 'restrictFields': Numeric array of a field name whitelist
* @return FieldSet
*/
public function scaffoldSearchFields($restrictFields = null, $fieldClasses = null) {
public function scaffoldSearchFields($_params = null) {
$params = array_merge(
array(
'fieldClasses' => false,
'restrictFields' => false
),
(array)$_params
);
$fields = new FieldSet();
foreach($this->searchableFields() as $fieldName => $spec) {
if($restrictFields && !in_array($fieldName, $restrictFields)) continue;
if($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) continue;
// If a custom fieldclass is provided as a string, use it
if($fieldClasses && isset($fieldClasses[$fieldName])) {
$fieldClass = $fieldClasses[$fieldName];
if($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
$fieldClass = $params['fieldClasses'][$fieldName];
$field = new $fieldClass($fieldName);
// If we explicitly set a field, then construct that
} else if(isset($spec['field'])) {
@ -1502,65 +1455,33 @@ class DataObject extends ViewableData implements DataObjectInterface {
* based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
* Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
*
* @uses DBField::scaffoldFormField()
* @uses DataObject::fieldLabels()
* @uses FormScaffolder
*
* @param
* @param array $fieldClasses Optional mapping of fieldnames to subclasses of {@link DBField}
* @param array $_params Associative array passing through properties to {@link FormScaffolder}.
* @return FieldSet
*/
public function scaffoldFormFields($restrictFields = null, $fieldClasses = null) {
$fields = new FieldSet();
foreach($this->db() as $fieldName => $fieldType) {
if($restrictFields && !in_array($fieldName, $restrictFields)) continue;
// @todo Pass localized title
if(isset($fieldClasses[$fieldName])) {
$fieldClass = $fieldClasses[$fieldName];
$fieldObject = new $fieldClass($fieldName);
} else {
$fieldObject = $this->dbObject($fieldName)->scaffoldFormField();
}
$fieldObject->setTitle($this->fieldLabel($fieldName));
$fields->push($fieldObject);
}
foreach($this->has_one() as $relationship => $component) {
if($restrictFields && !in_array($relationship, $restrictFields)) continue;
$model = singleton($component);
$records = DataObject::get($component);
$collect = ($model->hasMethod('customSelectOption')) ? 'customSelectOption' : 'Title';
$options = $records ? $records->filter_map('ID', $collect) : array();
$fields->push(new DropdownField($relationship.'ID', $this->fieldLabel($relationship), $options));
}
return $fields;
public function scaffoldFormFields($_params = null) {
$params = array_merge(
array(
'tabbed' => false,
'includeRelations' => false,
'restrictFields' => false,
'fieldClasses' => false,
'ajaxSafe' => false
),
(array)$_params
);
$fs = new FormScaffolder($this);
$fs->tabbed = $params['tabbed'];
$fs->includeRelations = $params['includeRelations'];
$fs->restrictFields = $params['restrictFields'];
$fs->fieldClasses = $params['fieldClasses'];
$fs->ajaxSafe = $params['ajaxSafe'];
return $fs->getFieldSet();
}
/**
* Returns automatically generated fields useable within the CMS.
* Does not include relations, you can add them by using
* {@link addScaffoldRelationFields()}. Call {@link getCMSFields()}
* to get all fields including relational tables and decorated
* fields in a TabSet.
*
* @uses getCMSFields()
*
* @return FieldSet Tabbed fields
*/
public function scaffoldCMSFields($restrictFields = null, $fieldClasses = null) {
$untabbedFields = $this->scaffoldFormFields($restrictFields, $fieldClasses);
// make sure we have a tabset
if(!$untabbedFields->hasTabSet()) {
$tabbedFields = new FieldSet(new TabSet("Root", new Tab("Main")));
foreach($untabbedFields as $field) {
$tabbedFields->addFieldToTab("Root.Main", $field);
}
}
return $tabbedFields;
}
/**
* Centerpiece of every data administration interface in Silverstripe,
* which returns a {@link FieldSet} suitable for a {@link Form} object.
@ -1582,36 +1503,40 @@ class DataObject extends ViewableData implements DataObjectInterface {
*
* @see Good example of complex FormField building: SiteTree::getCMSFields()
*
* @param array $params See {@link scaffoldFormFields()}
* @return FieldSet Returns a TabSet for usage within the CMS - don't use for frontend forms.
*/
public function getCMSFields() {
// should be a plain FieldSet without tabs
$tabbedFields = $this->scaffoldCMSFields();
// If we don't have an ID, then relation fields don't work
if($this->ID) {
$tabbedFields = $this->addScaffoldRelationFields($tabbedFields);
}
public function getCMSFields($params = null) {
$tabbedFields = $this->scaffoldFormFields(array_merge(
array(
'includeRelations' => true,
'tabbed' => true,
'ajaxSafe' => true
),
(array)$params
));
$this->extend('updateCMSFields', $tabbedFields);
return $tabbedFields;
}
/**
* Used for simple frontend forms without relation editing
* or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
* by default. To customize, either overload this method in your
* subclass, or decorate it by {@link DataObjectDecorator->updateFormFields()}.
*
* @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
*
* @param array $params See {@link scaffoldFormFields()}
* @return FieldSet Always returns a simple field collection without TabSet.
*/
public function getFormFields() {
$fields = $this->scaffoldFormFields();
$this->extend('updateFormFields', $fields);
public function getFrontEndFields($params = null) {
$untabbedFields = $this->scaffoldFormFields($params);
$this->extend('updateFormFields', $untabbedFields);
return $fields;
return $untabbedFields;
}
/**
@ -1827,7 +1752,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
// add foreign key
$hasOne = $this->uninherited('has_one', true);
if($hasOne) foreach($hasOne as $fieldName => $fieldSchema) {
$fieldMap[$fieldName . 'ID'] = "Int";
$fieldMap[$fieldName . 'ID'] = "ForeignKey";
}
// set cached fieldmap
@ -1995,7 +1920,8 @@ class DataObject extends ViewableData implements DataObjectInterface {
// Special case for has_one relationships
} else if(preg_match('/ID$/', $fieldName) && $this->has_one(substr($fieldName,0,-2))) {
return DBField::create('Int', $this->record[$fieldName], $fieldName);
$val = (isset($this->record[$fieldName])) ? $this->record[$fieldName] : null;
return DBField::create('ForeignKey', $val, $fieldName, $this);
}
}
@ -2515,7 +2441,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
$def = $db;
if($has_one) {
foreach($has_one as $field => $joinTo) {
$def[$field . 'ID'] = "Int";
$def[$field . 'ID'] = "ForeignKey";
}
}

View File

@ -40,7 +40,7 @@ class Boolean extends DBField {
}
}
public function scaffoldFormField($title = null) {
public function scaffoldFormField($title = null, $params = null) {
return new CheckboxField($this->name, $title);
}

View File

@ -45,8 +45,8 @@ abstract class DBField extends ViewableData {
* Create a DBField object that's not bound to any particular field.
* Useful for accessing the classes behaviour for other parts of your code.
*/
static function create($className, $value, $name = null) {
$dbField = Object::create($className, $name);
static function create($className, $value, $name = null, $object = null) {
$dbField = Object::create($className, $name, $object);
$dbField->setValue($value);
return $dbField;
}

View File

@ -236,7 +236,7 @@ class Date extends DBField {
}
}
public function scaffoldFormField($title = null) {
public function scaffoldFormField($title = null, $params = null) {
return new DateField($this->name, $title);
}
}

View File

@ -40,7 +40,7 @@ if(!class_exists('Datetime')) {
DB::requireField($this->tableName, $this->name, "datetime");
}
public function scaffoldFormField($title = null) {
public function scaffoldFormField($title = null, $params = null) {
return new PopupDateTimeField($this->name, $title);
}
}

View File

@ -37,7 +37,7 @@ class Decimal extends DBField {
}
}
public function scaffoldFormField($title = null) {
public function scaffoldFormField($title = null, $params = null) {
return new NumericField($this->name, $title);
}

View File

@ -46,7 +46,7 @@ class Enum extends DBField {
}
public function scaffoldFormField($title = null) {
public function scaffoldFormField($title = null, $params = null) {
return $this->formField($title);
}

View File

@ -0,0 +1,52 @@
<?php
/**
* A special type Int field used for foreign keys in has_one relationships.
*
* @param string $name
* @param DataOject $object The object that the foreign key is stored on (should have a relation with $name)
*
* @package sapphire
* @subpackage model
*/
class ForeignKey extends Int {
/**
* @var DataObject
*/
protected $object;
protected static $default_search_filter_class = 'ExactMatchMultiFilter';
function __construct($name, $object) {
$this->object = $object;
parent::__construct($name);
}
public function scaffoldFormField($title = null, $params = null) {
$relationName = substr($this->name,0,-2);
$hasOneClass = $this->object->has_one($relationName);
if($hasOneClass && singleton($hasOneClass) instanceof Image) {
if(isset($params['ajax']) && $params['ajax']) {
$field = new ImageField($relationName, $title, $this->value);
} else {
$field = new SimpleImageField($relationName, $title, $this->value);
}
} elseif($hasOneClass && singleton($hasOneClass) instanceof File) {
if(isset($params['ajax']) && $params['ajax']) {
$field = new FileIframeField($relationName, $title, $this->value);
} else {
$field = new FileField($relationName, $title, $this->value);
}
} else {
$objs = DataObject::get($this->object->class);
$titleField = (singleton($this->object->class)->hasField('Title')) ? "Title" : "Name";
$map = ($objs) ? $objs->toDropdownMap("ID", $titleField) : false;
$field = new DropdownField($this->name, $title, $map, null, null, ' ');
}
return $field;
}
}
?>

View File

@ -65,7 +65,7 @@ class HTMLText extends Text {
return $summary;
}
public function scaffoldFormField($title = null) {
public function scaffoldFormField($title = null, $params = null) {
return new HtmlEditorField($this->name, $title);
}

View File

@ -8,7 +8,7 @@
*/
class HTMLVarchar extends Varchar {
public function scaffoldFormField($title = null) {
public function scaffoldFormField($title = null, $params = null) {
return new HtmlOneLineField($this->name, $title);
}

View File

@ -38,7 +38,7 @@ class Int extends DBField {
return sprintf( '%d', $this->value );
}
public function scaffoldFormField($title = null) {
public function scaffoldFormField($title = null, $params = null) {
return new NumericField($this->name, $title);
}

View File

@ -21,7 +21,7 @@ class PrimaryKey extends Int {
parent::__construct($name);
}
public function scaffoldFormField($title = null) {
public function scaffoldFormField($title = null, $params = null) {
$objs = DataObject::get($this->object->class);
$titleField = (singleton($this->object->class)->hasField('Title')) ? "Title" : "Name";

View File

@ -34,7 +34,7 @@ class SSDatetime extends Date {
return date('Y-m-d%20H:i:s', strtotime($this->value));
}
public function scaffoldFormField($title = null) {
public function scaffoldFormField($title = null, $params = null) {
return new PopupDateTimeField($this->name, $title);
}
}

View File

@ -39,7 +39,7 @@ class Time extends DBField {
DB::requireField($this->tableName, $this->name, "time");
}
public function scaffoldFormField($title = null) {
public function scaffoldFormField($title = null, $params = null) {
return new TimeField($this->name, $title);
}
}

View File

@ -16,7 +16,7 @@ class Year extends DBField {
DB::requireField($this->tableName, $this->name, "year(4)");
}
public function scaffoldFormField($title = null) {
public function scaffoldFormField($title = null, $params = null) {
$selectBox = new DropdownField($this->name, $title);
$selectBox->setSource($this->getDefaultOptions());
return $selectBox;

187
forms/FormScaffolder.php Normal file
View File

@ -0,0 +1,187 @@
<?php
/**
*
* @package sapphire
* @subpackage forms
*
* @uses DBField::scaffoldFormField()
* @uses DataObject::fieldLabels()
*/
class FormScaffolder extends Object {
/**
* @var DataObject $obj The object defining the fields to be scaffolded
* through its metadata like $db, $searchable_fields, etc.
*/
protected $obj;
/**
* @var boolean $tabbed Return fields in a tabset, with all main fields in the path "Root.Main",
* relation fields in "Root.<relationname>" (if {@link $includeRelations} is enabled).
*/
public $tabbed = false;
/**
* @var boolean $ajaxSafe
*/
public $ajaxSafe = false;
/**
* @var array $restrictFields Numeric array of a field name whitelist.
* If left blank, all fields from {@link DataObject->db()} will be included.
*
* @todo Implement restrictions for has_many and many_many relations.
*/
public $restrictFields;
/**
* @var array $fieldClasses Optional mapping of fieldnames to subclasses of {@link FormField}.
* By default the scaffolder will determine the field instance by {@link DBField::scaffoldFormField()}.
*
* @todo Implement fieldClasses for has_many and many_many relations
*/
public $fieldClasses;
/**
* @var boolean $includeRelations Include has_one, has_many and many_many relations
*/
public $includeRelations = false;
/**
* @param DataObject $obj
* @param array $params
*/
function __construct($obj) {
$this->obj = $obj;
}
/**
* Gets the form fields as defined through the metadata
* on {@link $obj} and the custom parameters passed to FormScaffolder.
* Depending on those parameters, the fields can be used in ajax-context,
* contain {@link TabSet}s etc.
*
* @return FieldSet
*/
public function getFieldSet() {
$fields = new FieldSet();
// tabbed or untabbed
if($this->tabbed) $fields->push(new TabSet("Root", new Tab("Main")));
// add database fields
foreach($this->obj->db() as $fieldName => $fieldType) {
if($this->restrictFields && !in_array($fieldName, $this->restrictFields)) continue;
// @todo Pass localized title
if($this->fieldClasses && isset($this->fieldClasses[$fieldName])) {
$fieldClass = $this->fieldClasses[$fieldName];
$fieldObject = new $fieldClass($fieldName);
} else {
$fieldObject = $this->obj->dbObject($fieldName)->scaffoldFormField(null, $this->getParamsArray());
}
$fieldObject->setTitle($this->obj->fieldLabel($fieldName));
if($this->tabbed) {
$fields->addFieldToTab("Root.Main", $fieldObject);
} else {
$fields->push($fieldObject);
}
}
// add has_one relation fields
if($this->obj->has_one() && ($this->includeRelations === true || isset($this->includeRelations['has_one']))) {
foreach($this->obj->has_one() as $relationship => $component) {
if($this->restrictFields && !in_array($relationship, $this->restrictFields)) continue;
$hasOneField = $this->obj->dbObject("{$relationship}ID")->scaffoldFormField(null, $this->getParamsArray());
if($this->tabbed) {
$fields->addFieldToTab("Root.Main", $hasOneField);
} else {
$fields->push($hasOneField);
}
}
}
// only add relational fields if an ID is present
if($this->obj->ID) {
// add has_many relation fields
if($this->obj->has_many() && ($this->includeRelations === true || isset($this->includeRelations['has_many']))) {
foreach($this->obj->has_many() as $relationship => $component) {
if($this->tabbed) {
$relationTab = $fields->findOrMakeTab(
"Root.$relationship",
$this->obj->fieldLabel($relationship)
);
}
$relationshipFields = singleton($component)->summaryFields();
$foreignKey = $this->obj->getComponentJoinField($relationship);
$ctf = new ComplexTableField(
$this,
$relationship,
$component,
$relationshipFields,
"getCMSFields",
"$foreignKey = $this->obj->ID"
);
$ctf->setPermissions(TableListField::permissions_for_object($component));
if($this->tabbed) {
$fields->addFieldToTab("Root.$relationship", $ctf);
} else {
$fields->push($ctf);
}
}
}
if($this->obj->many_many() && ($this->includeRelations === true || isset($this->includeRelations['many_many']))) {
foreach($this->obj->many_many() as $relationship => $component) {
if($this->tabbed) {
$relationTab = $fields->findOrMakeTab(
"Root.$relationship",
$this->obj->fieldLabel($relationship)
);
}
$relationshipFields = singleton($component)->summaryFields();
$filterWhere = $this->obj->getManyManyFilter($relationship, $component);
$filterJoin = $this->obj->getManyManyJoin($relationship, $component);
$ctf = new ComplexTableField(
$this,
$relationship,
$component,
$relationshipFields,
"getCMSFields",
$filterWhere,
'',
$filterJoin
);
$ctf->setPermissions(TableListField::permissions_for_object($component));
$ctf->popupClass = "ScaffoldingComplexTableField_Popup";
if($this->tabbed) {
$fields->addFieldToTab("Root.$relationship", $ctf);
} else {
$fields->push($ctf);
}
}
}
}
return $fields;
}
/**
* Return an array suitable for passing on to {@link DBField->scaffoldFormField()}
* without tying this call to a FormScaffolder interface.
*
* @return array
*/
protected function getParamsArray() {
return array(
'tabbed' => $this->tabbed,
'includeRelations' => $this->includeRelations,
'restrictFields' => $this->restrictFields,
'fieldClasses' => $this->fieldClasses,
'ajaxSafe' => $this->ajaxSafe
);
}
}
?>

View File

@ -0,0 +1,110 @@
<?php
/**
* Tests for DataObject FormField scaffolding
*
* @package sapphire
* @subpackage tests
*
*/
class FormScaffolderTest extends SapphireTest {
static $fixture_file = 'sapphire/tests/forms/FormScaffolderTest.yml';
function testGetCMSFieldsSingleton() {
$fields = singleton('FormScaffolderTest_Article')->getCMSFields();
$this->assertTrue($fields->hasTabSet(), 'getCMSFields() produces a TabSet');
$this->assertNotNull($fields->dataFieldByName('Title'), 'getCMSFields() includes db fields');
$this->assertNotNull($fields->dataFieldByName('Content'), 'getCMSFields() includes db fields');
$this->assertNotNull($fields->dataFieldByName('AuthorID'), 'getCMSFields() includes has_one fields on singletons');
$this->assertNull($fields->dataFieldByName('Tags'), 'getCMSFields() doesnt include many_many fields if no ID is present');
}
function testGetCMSFieldsInstance() {
$article1 = $this->objFromFixture('FormScaffolderTest_Article', 'article1');
$fields = $article1->getCMSFields();
$this->assertNotNull($fields->dataFieldByName('AuthorID'), 'getCMSFields() includes has_one fields on instances');
$this->assertNotNull($fields->dataFieldByName('Tags'), 'getCMSFields() includes many_many fields if ID is present on instances');
}
function testUpdateCMSFields() {
$article1 = $this->objFromFixture('FormScaffolderTest_Article', 'article1');
$fields = $article1->getCMSFields();
$this->assertNotNull(
$fields->dataFieldByName('AddedDecoratorField'),
'getCMSFields() includes decorated fields'
);
}
function testRestrictCMSFields() {
$article1 = $this->objFromFixture('FormScaffolderTest_Article', 'article1');
$fields = $article1->scaffoldFormFields(array(
'restrictFields' => array('Title')
));
$this->assertNotNull($fields->dataFieldByName('Title'), 'scaffoldCMSFields() includes explitly defined "restrictFields"');
$this->assertNull($fields->dataFieldByName('Content'), 'getCMSFields() doesnt include fields left out in a "restrictFields" definition');
}
function testFieldClassesOnGetCMSFields() {
$article1 = $this->objFromFixture('FormScaffolderTest_Article', 'article1');
$fields = $article1->scaffoldFormFields(array(
'fieldClasses' => array('Title' => 'HtmlEditorField')
));
$this->assertNotNull(
$fields->dataFieldByName('Title')
);
$this->assertEquals(
get_class($fields->dataFieldByName('Title')),
'HtmlEditorField',
'getCMSFields() doesnt include fields left out in a "restrictFields" definition'
);
}
function testGetFormFields() {
$fields = singleton('FormScaffolderTest_Article')->getFrontEndFields();
$this->assertFalse($fields->hasTabSet(), 'getFrontEndFields() doesnt produce a TabSet by default');
}
}
class FormScaffolderTest_Article extends DataObject implements TestOnly {
static $db = array(
'Title' => 'Varchar',
'Content' => 'HTMLText'
);
static $has_one = array(
'Author' => 'FormScaffolderTest_Author'
);
static $many_many = array(
'Tags' => 'FormScaffolderTest_Tag',
);
}
class FormScaffolderTest_Author extends Member implements TestOnly {
static $has_one = array(
'ProfileImage' => 'Image'
);
static $has_many = array(
'Articles' => 'FormScaffolderTest_Article'
);
}
class FormScaffolderTest_Tag extends DataObject implements TestOnly {
static $db = array(
'Title' => 'Varchar',
);
static $belongs_many_many = array(
'Articles' => 'FormScaffolderTest_Article'
);
}
class FormScaffolderTest_ArticleDecorator extends DataObjectDecorator implements TestOnly {
static $db = array(
'DecoratedField' => 'Varchar'
);
function updateCMSFields(&$fields) {
$fields->addFieldToTab('Root.Main',
new TextField('AddedDecoratorField')
);
}
}
DataObject::add_extension('FormScaffolderTest_Article', 'FormScaffolderTest_ArticleDecorator');
?>

View File

@ -0,0 +1,12 @@
FormScaffolderTest_Tag:
tag1:
Title: Tag 1
FormScaffolderTest_Article:
article1:
Title: Article 1
Content: Test
Tags: =>FormScaffolderTest_Tag.tag1
FormScaffolderTest_Author:
author1:
FirstName: Author 1
Tags: =>FormScaffolderTest_Article.article1