API Implement cascading deletes API for model relations

This commit is contained in:
Robbie Averill 2017-08-15 15:30:59 +12:00
parent 98f69f344f
commit 5c9417da21
13 changed files with 129 additions and 419 deletions

View File

@ -14,9 +14,9 @@ use SilverStripe\Forms\GridField\GridFieldDetailForm;
use SilverStripe\Forms\GridField\GridFieldToolbarHeader;
use SilverStripe\ORM\DataExtension;
use SilverStripe\UserForms\Form\GridFieldAddClassesButton;
use SilverStripe\UserForms\Model\EditableFormField;
use SilverStripe\UserForms\Model\EditableFormField\EditableFieldGroup;
use SilverStripe\UserForms\Model\EditableFormField\EditableFieldGroupEnd;
use SilverStripe\UserForms\Model\EditableFormField;
use SilverStripe\UserForms\Model\EditableFormField\EditableFormStep;
use SilverStripe\UserForms\Model\EditableFormField\EditableTextField;
use SilverStripe\Versioned\Versioned;
@ -36,6 +36,14 @@ class UserFormFieldEditorExtension extends DataExtension
'Fields' => EditableFormField::class
);
private static $owns = [
'Fields'
];
private static $cascade_deletes = [
'Fields'
];
/**
* Adds the field editor to the page.
*
@ -159,31 +167,23 @@ class UserFormFieldEditorExtension extends DataExtension
}
/**
* @see SiteTree::doPublish
* @param Page $original
*
* @return void
* Remove any orphaned child records on publish
*/
public function onAfterPublish($original)
public function onAfterPublish()
{
if (!$original) {
return;
}
// store IDs of fields we've published
$seenIDs = array();
$seenIDs = [];
foreach ($this->owner->Fields() as $field) {
// store any IDs of fields we publish so we don't unpublish them
$seenIDs[] = $field->ID;
$field->publishRecursive();
$field->doPublish(Versioned::DRAFT, Versioned::LIVE);
$field->destroy();
}
// fetch any orphaned live records
$live = Versioned::get_by_stage(EditableFormField::class, "Live")
$live = Versioned::get_by_stage(EditableFormField::class, Versioned::LIVE)
->filter([
'ParentID' => $original->ID,
'ParentID' => $this->owner->ID,
]);
if (!empty($seenIDs)) {
@ -194,35 +194,34 @@ class UserFormFieldEditorExtension extends DataExtension
// delete orphaned records
foreach ($live as $field) {
$field->doDeleteFromStage('Live');
$field->deleteFromStage(Versioned::LIVE);
$field->destroy();
}
}
/**
* @see SiteTree::doUnpublish
* @param Page $page
*
* @return void
* Remove all fields from the live stage when unpublishing the page
*/
public function onAfterUnpublish($page)
public function onAfterUnpublish()
{
foreach ($page->Fields() as $field) {
$field->doDeleteFromStage('Live');
foreach ($this->owner->Fields() as $field) {
$field->deleteFromStage(Versioned::LIVE);
}
}
/**
* @see SiteTree::duplicate
* @param DataObject $newPage
* When duplicating a UserDefinedForm, duplicate all of its fields and display rules
*
* @see DataObject::duplicate
* @param DataObject $newPage
* @param bool $doWrite
* @param string $manyMany
* @return DataObject
*/
public function onAfterDuplicate($newPage)
public function onAfterDuplicate($newPage, $doWrite, $manyMany)
{
// List of EditableFieldGroups, where the
// key of the array is the ID of the old end group
$fieldGroups = array();
// List of EditableFieldGroups, where the key of the array is the ID of the old end group
$fieldGroups = [];
foreach ($this->owner->Fields() as $field) {
$newField = $field->duplicate(false);
$newField->ParentID = $newPage->ID;
@ -254,35 +253,29 @@ class UserFormFieldEditorExtension extends DataExtension
}
/**
* @see SiteTree::getIsModifiedOnStage
* @param boolean $isModified
* Checks child fields to see if any are modified in draft as well. The owner of this extension will still
* use the Versioned method to determine its own status.
*
* @return boolean
* @see Versioned::isModifiedOnDraft
*
* @return boolean|null
*/
public function getIsModifiedOnStage($isModified)
public function isModifiedOnDraft()
{
if (!$isModified) {
foreach ($this->owner->Fields() as $field) {
if ($field->getIsModifiedOnStage()) {
$isModified = true;
break;
}
foreach ($this->owner->Fields() as $field) {
if ($field->isModifiedOnDraft()) {
return true;
}
}
return $isModified;
}
/**
* @see SiteTree::doRevertToLive
* @param Page $page
*
* @return void
* @see Versioned::doRevertToLive
*/
public function onAfterRevertToLive($page)
public function onAfterRevertToLive()
{
foreach ($page->Fields() as $field) {
$field->copyVersionToStage('Live', 'Stage', false);
foreach ($this->owner->Fields() as $field) {
$field->copyVersionToStage(Versioned::LIVE, Versioned::STAGE, false);
$field->writeWithoutVersion();
}
}

View File

@ -57,27 +57,6 @@ class EditableCustomRule extends DataObject
private static $table_name = 'EditableCustomRule';
/**
* Publish this custom rule to the live site
*
* Wrapper for the {@link Versioned} publish function
*/
public function doPublish($fromStage, $toStage, $createNewVersion = false)
{
$this->publish($fromStage, $toStage, $createNewVersion);
}
/**
* Delete this custom rule from a given stage
*
* Wrapper for the {@link Versioned} deleteFromStage function
*/
public function doDeleteFromStage($stage)
{
$this->deleteFromStage($stage);
}
/**
* @param Member $member
* @return bool

View File

@ -173,6 +173,14 @@ class EditableFormField extends DataObject
'DisplayRules' => EditableCustomRule::class . '.Parent'
];
private static $owns = [
'DisplayRules',
];
private static $cascade_deletes = [
'DisplayRules',
];
/**
* @var bool
*/
@ -198,10 +206,12 @@ class EditableFormField extends DataObject
* Set the visibility of an individual form field
*
* @param bool
* @return $this
*/
public function setReadonly($readonly = true)
{
$this->readonly = $readonly;
return $this;
}
/**
@ -546,92 +556,6 @@ class EditableFormField extends DataObject
return null;
}
/**
* Check if can publish
*
* @param Member $member
* @return bool
*/
public function canPublish($member = null)
{
return $this->canEdit($member);
}
/**
* Check if can unpublish
*
* @param Member $member
* @return bool
*/
public function canUnpublish($member = null)
{
return $this->canDelete($member);
}
/**
* Publish this Form Field to the live site
*
* Wrapper for the {@link Versioned} publish function
*
* @param string $fromStage
* @param string $toStage
* @param bool $createNewVersion
*/
public function doPublish($fromStage, $toStage, $createNewVersion = false)
{
$this->publish($fromStage, $toStage, $createNewVersion);
$this->publishRules($fromStage, $toStage, $createNewVersion);
}
/**
* Publish all field rules
*
* @param string $fromStage
* @param string $toStage
* @param bool $createNewVersion
*/
protected function publishRules($fromStage, $toStage, $createNewVersion)
{
$seenRuleIDs = [];
// Don't forget to publish the related custom rules...
foreach ($this->DisplayRules() as $rule) {
$seenRuleIDs[] = $rule->ID;
$rule->doPublish($fromStage, $toStage, $createNewVersion);
$rule->destroy();
}
// remove any orphans from the "fromStage"
$rules = Versioned::get_by_stage(EditableCustomRule::class, $toStage)
->filter('ParentID', $this->ID);
if (!empty($seenRuleIDs)) {
$rules = $rules->exclude('ID', $seenRuleIDs);
}
foreach ($rules as $rule) {
$rule->deleteFromStage($toStage);
}
}
/**
* Delete this field from a given stage
*
* Wrapper for the {@link Versioned} deleteFromStage function
*/
public function doDeleteFromStage($stage)
{
// Remove custom rules in this stage
$rules = Versioned::get_by_stage(EditableCustomRule::class, $stage)
->filter('ParentID', $this->ID);
foreach ($rules as $rule) {
$rule->deleteFromStage($stage);
}
// Remove record
$this->deleteFromStage($stage);
}
/**
* checks whether record is new, copied from SiteTree
*/
@ -648,23 +572,6 @@ class EditableFormField extends DataObject
return stripos($this->ID, 'new') === 0;
}
/**
* checks if records is changed on stage
* @return boolean
*/
public function getIsModifiedOnStage()
{
// new unsaved fields could be never be published
if ($this->isNew()) {
return false;
}
$stageVersion = Versioned::get_versionnumber_by_stage(EditableFormField::class, 'Stage', $this->ID);
$liveVersion = Versioned::get_versionnumber_by_stage(EditableFormField::class, 'Live', $this->ID);
return ($stageVersion && $stageVersion != $liveVersion);
}
/**
* @deprecated since version 4.0
*/

View File

@ -14,7 +14,15 @@ use SilverStripe\UserForms\Model\EditableFormField\EditableFieldGroupEnd;
class EditableFieldGroup extends EditableFormField
{
private static $has_one = [
'End' => EditableFieldGroupEnd::class
'End' => EditableFieldGroupEnd::class,
];
private static $owns = [
'End',
];
private static $cascade_deletes = [
'End',
];
/**
@ -86,22 +94,4 @@ class EditableFieldGroup extends EditableFormField
$field->addExtraClass($this->ExtraClass);
}
}
protected function onBeforeDelete()
{
parent::onBeforeDelete();
// Ensures EndID is lazy-loaded for onAfterDelete
$this->EndID;
}
protected function onAfterDelete()
{
parent::onAfterDelete();
// Delete end
if (($end = $this->End()) && $end->exists()) {
$end->delete();
}
}
}

View File

@ -98,14 +98,4 @@ class EditableFieldGroupEnd extends EditableFormField
}
}
}
protected function onAfterDelete()
{
parent::onAfterDelete();
// Delete group
if (($group = $this->Group()) && $group->exists()) {
$group->delete();
}
}
}

View File

@ -43,7 +43,15 @@ class EditableMultipleOptionField extends EditableFormField
private static $abstract = true;
private static $has_many = [
'Options' => EditableOption::class
'Options' => EditableOption::class,
];
private static $owns = [
'Options',
];
private static $cascade_deletes = [
'Options',
];
private static $table_name = 'EditableMultipleOptionField';
@ -103,100 +111,15 @@ class EditableMultipleOptionField extends EditableFormField
return $fields;
}
/**
* Publishing Versioning support.
*
* When publishing it needs to handle copying across / publishing
* each of the individual field options
*
* @param string $fromStage
* @param string $toStage
* @param bool $createNewVersion
*/
public function copyVersionToStage($fromStage, $toStage, $createNewVersion = false)
{
parent::copyVersionToStage($fromStage, $toStage, $createNewVersion);
$this->publishOptions($fromStage, $toStage, $createNewVersion);
}
/**
* Publish list options
*
* @param string $fromStage
* @param string $toStage
* @param bool $createNewVersion
*/
protected function publishOptions($fromStage, $toStage, $createNewVersion)
{
$seenIDs = [];
// Publish all options
foreach ($this->Options() as $option) {
$seenIDs[] = $option->ID;
$option->copyVersionToStage($fromStage, $toStage, $createNewVersion);
}
// remove any orphans from the "fromStage"
$options = Versioned::get_by_stage(EditableOption::class, $toStage)
->filter('ParentID', $this->ID);
if (!empty($seenIDs)) {
$options = $options->exclude('ID', $seenIDs);
}
foreach ($options as $rule) {
$rule->deleteFromStage($toStage);
}
}
/**
* Unpublishing Versioning support
*
* When unpublishing the field it has to remove all options attached
*
* @return void
*/
public function doDeleteFromStage($stage)
{
// Remove options
$options = Versioned::get_by_stage(EditableOption::class, $stage)
->filter('ParentID', $this->ID);
foreach ($options as $option) {
$option->deleteFromStage($stage);
}
parent::doDeleteFromStage($stage);
}
/**
* Deletes all the options attached to this field before deleting the
* field. Keeps stray options from floating around
*
* @return void
*/
public function delete()
{
$options = $this->Options();
if ($options) {
foreach ($options as $option) {
$option->delete();
}
}
parent::delete();
}
/**
* Duplicate a pages content. We need to make sure all the fields attached
* to that page go with it
*
* @return DataObject
* {@inheritDoc}
*/
public function duplicate($doWrite = true, $manyMany = 'many_many')
{
$clonedNode = parent::duplicate();
$clonedNode = parent::duplicate($doWrite, $manyMany);
foreach ($this->Options() as $field) {
$newField = $field->duplicate(false);

View File

@ -65,26 +65,6 @@ class EditableOption extends DataObject
self::$allow_empty_values = (bool) $allow;
}
/**
* @param Member $member
*
* @return boolean
*/
public function canEdit($member = null)
{
return $this->Parent()->canEdit($member);
}
/**
* @param Member $member
*
* @return boolean
*/
public function canDelete($member = null)
{
return $this->canEdit($member);
}
/**
* @deprecated 5.0 Use "$Title.XML" in templates instead
* @return string
@ -94,73 +74,6 @@ class EditableOption extends DataObject
return Convert::raw2att($this->Title);
}
/**
* @param Member $member
* @return bool
*/
public function canView($member = null)
{
return $this->Parent()->canView($member);
}
/**
* Return whether a user can create an object of this type
*
* @param Member $member
* @param array $context Virtual parameter to allow context to be passed in to check
* @return bool
*/
public function canCreate($member = null, $context = [])
{
// Check parent page
$parent = $this->getCanCreateContext(func_get_args());
if ($parent) {
return $parent->canEdit($member);
}
// Fall back to secure admin permissions
return parent::canCreate($member);
}
/**
* Helper method to check the parent for this object
*
* @param array $args List of arguments passed to canCreate
* @return DataObject Some parent dataobject to inherit permissions from
*/
protected function getCanCreateContext($args)
{
// Inspect second parameter to canCreate for a 'Parent' context
if (isset($args[1]['Parent'])) {
return $args[1]['Parent'];
}
// Hack in currently edited page if context is missing
if (Controller::has_curr() && Controller::curr() instanceof CMSMain) {
return Controller::curr()->currentPage();
}
// No page being edited
return null;
}
/**
* @param Member $member
* @return bool
*/
public function canPublish($member = null)
{
return $this->canEdit($member);
}
/**
* @param Member $member
* @return bool
*/
public function canUnpublish($member = null)
{
return $this->canDelete($member);
}
/**
* Fetches a value for $this->Value. If empty values are not allowed,
* then this will return the title in the case of an empty value.

View File

@ -65,7 +65,15 @@ class EmailRecipient extends DataObject
];
private static $has_many = [
'CustomRules' => EmailRecipientCondition::class
'CustomRules' => EmailRecipientCondition::class,
];
private static $owns = [
'CustomRules',
];
private static $cascade_deetes = [
'CustomRules',
];
private static $summary_fields = [

View File

@ -29,6 +29,10 @@ class SubmittedForm extends DataObject
'Values' => SubmittedFormField::class
];
private static $cascade_deletes = [
'Values',
];
private static $summary_fields = [
'ID',
'Created'

View File

@ -129,6 +129,10 @@ class UserDefinedForm extends Page
'EmailRecipients' => EmailRecipient::class
];
private static $cascade_deletes = [
'EmailRecipients',
];
/**
* @var array
* @config
@ -160,6 +164,8 @@ class UserDefinedForm extends Page
*/
private static $recipients_warning_enabled = false;
private static $non_live_permissions = ['SITETREE_VIEW_ALL'];
/**
* Temporary storage of field ids when the form is duplicated.
* Example layout: array('EditableCheckbox3' => 'EditableCheckbox14')

View File

@ -1,18 +1,15 @@
<?php
namespace SilverStripe\UserForms\Test\Model\EditableFormField;
namespace SilverStripe\UserForms\Test\Model;
use SilverStripe\Control\Controller;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\FunctionalTest;
use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\OptionsetField;
use SilverStripe\Security\IdentityStore;
use SilverStripe\UserForms\Model\EditableFormField;
use SilverStripe\UserForms\Model\EditableFormField\EditableCheckbox;
use SilverStripe\UserForms\Model\EditableFormField\EditableDropdown;
use SilverStripe\UserForms\Model\EditableFormField\EditableFileField;
use SilverStripe\UserForms\Model\EditableFormField;
use SilverStripe\UserForms\Model\EditableFormField\EditableOption;
use SilverStripe\UserForms\Model\EditableFormField\EditableRadioField;
use SilverStripe\UserForms\Model\EditableFormField\EditableTextField;
@ -30,6 +27,7 @@ class EditableFormFieldTest extends FunctionalTest
$text = $this->objFromFixture(EditableTextField::class, 'basic-text');
$this->logInWithPermission('ADMIN');
$this->assertTrue($text->canCreate());
$this->assertTrue($text->canView());
$this->assertTrue($text->canEdit());
@ -45,9 +43,9 @@ class EditableFormFieldTest extends FunctionalTest
$this->assertTrue($text->canEdit());
$this->assertTrue($text->canDelete());
Injector::inst()->get(IdentityStore::class)->logOut(Controller::curr()->getRequest());
$this->logOut();
$this->logInWithPermission('SITETREE_VIEW_ALL');
$this->assertFalse($text->canCreate());
$text->setReadonly(false);
@ -139,12 +137,12 @@ class EditableFormFieldTest extends FunctionalTest
$clone = $dropdown->duplicate();
$this->assertEquals($clone->Options()->Count(), $dropdown->Options()->Count());
$this->assertEquals($dropdown->Options()->Count(), $clone->Options()->Count());
foreach ($clone->Options() as $option) {
$orginal = $dropdown->Options()->find('Title', $option->Title);
$original = $dropdown->Options()->find('Title', $option->Title);
$this->assertEquals($orginal->Sort, $option->Sort);
$this->assertEquals($original->Sort, $option->Sort);
}
}
@ -221,7 +219,7 @@ class EditableFormFieldTest extends FunctionalTest
public function testFormatDisplayRules()
{
$field = $this->objFromFixture(EditableFormField::class, 'irdNumberField');
$field = $this->objFromFixture(EditableTextField::class, 'irdNumberField');
$displayRules = $field->formatDisplayRules();
$this->assertNotNull($displayRules);
$this->assertCount(1, $displayRules['operations']);

View File

@ -1,25 +1,40 @@
SilverStripe\UserForms\Model\EditableFormField:
SilverStripe\UserForms\Model\EditableFormField\EditableTextField:
basic-text:
Name: basic-text-name
Title: Basic Text Field
basic-text-2:
Name: basic-text-name
Title: Basic Text Field
required-text:
Name: required-text-field
Title: Required Text Field
CustomErrorMessage: Custom Error Message
Required: true
irdNumberField:
ClassName: SilverStripe\UserForms\Model\EditableFormField\EditableTextField
Name: IRDNumber
Title: "Enter your IRD Number"
countryTextField:
ClassName: SilverStripe\UserForms\Model\EditableFormField\EditableTextField
Name: CountryTextSelection
Title: "Enter your country (2 digit prefix)"
DisplayRulesConjunction: And
ShowOnLoad: false
SilverStripe\UserForms\Model\EditableCustomRule:
rule1:
Display: Show
ConditionOption: HasValue
FieldValue: NZ
ConditionField: =>SilverStripe\UserForms\Model\EditableFormField.countryTextField
Parent: =>SilverStripe\UserForms\Model\EditableFormField.irdNumberField
ConditionField: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.countryTextField
Parent: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.irdNumberField
rule-1:
Display: Hide
ConditionOption: HasValue
FieldValue: 6
SilverStripe\UserForms\Model\EditableFormField\EditableOption:
option-1:
Name: Option1
@ -79,21 +94,6 @@ SilverStripe\UserForms\Model\Recipient\EmailRecipient:
EmailFrom: no-reply@example.com
HideFormData: true
SilverStripe\UserForms\Model\EditableFormField\EditableTextField:
basic-text:
Name: basic-text-name
Title: Basic Text Field
basic-text-2:
Name: basic-text-name
Title: Basic Text Field
required-text:
Name: required-text-field
Title: Required Text Field
CustomErrorMessage: Custom Error Message
Required: true
SilverStripe\UserForms\Model\EditableFormField\EditableDropdown:
basic-dropdown:
Name: basic-dropdown

View File

@ -15,9 +15,9 @@ use SilverStripe\Security\Member;
use SilverStripe\UserForms\Extension\UserFormFieldEditorExtension;
use SilverStripe\UserForms\Extension\UserFormValidator;
use SilverStripe\UserForms\Model\EditableCustomRule;
use SilverStripe\UserForms\Model\EditableFormField;
use SilverStripe\UserForms\Model\EditableFormField\EditableEmailField;
use SilverStripe\UserForms\Model\EditableFormField\EditableDropdown;
use SilverStripe\UserForms\Model\EditableFormField;
use SilverStripe\UserForms\Model\EditableFormField\EditableFieldGroup;
use SilverStripe\UserForms\Model\EditableFormField\EditableFieldGroupEnd;
use SilverStripe\UserForms\Model\Recipient\EmailRecipient;
@ -47,12 +47,12 @@ class UserDefinedFormTest extends FunctionalTest
$form->SubmitButtonText = 'Button Text';
$form->write();
$form->doPublish();
$form->publishSingle();
$origVersion = $form->Version;
$form->SubmitButtonText = 'Updated Button Text';
$form->write();
$form->doPublish();
$form->publishSingle();
// check published site
$updated = Versioned::get_one_by_stage(UserDefinedForm::class, 'Stage', "\"UserDefinedForm\".\"ID\" = $form->ID");
@ -209,7 +209,7 @@ class UserDefinedFormTest extends FunctionalTest
$form = $this->objFromFixture(UserDefinedForm::class, 'basic-form-page');
$form->write();
$form->doPublish();
$form->publishSingle();
$live = Versioned::get_one_by_stage(UserDefinedForm::class, 'Live', "\"UserDefinedForm_Live\".\"ID\" = $form->ID");
@ -227,7 +227,7 @@ class UserDefinedFormTest extends FunctionalTest
$this->assertNull($liveDropdown);
// when publishing it should have added it
$form->doPublish();
$form->publishSingle();
$live = Versioned::get_one_by_stage(UserDefinedForm::class, 'Live', "\"UserDefinedForm_Live\".\"ID\" = $form->ID");
$this->assertEquals(3, $live->Fields()->Count());
@ -240,7 +240,7 @@ class UserDefinedFormTest extends FunctionalTest
$liveText = Versioned::get_one_by_stage(EditableFormField::class, 'Live', "\"EditableFormField_Live\".\"ID\" = $text->ID");
$this->assertFalse($liveText->Title == $text->Title);
$form->doPublish();
$form->publishSingle();
$liveText = Versioned::get_one_by_stage(EditableFormField::class, 'Live', "\"EditableFormField_Live\".\"ID\" = $text->ID");
$this->assertTrue($liveText->Title == $text->Title);
@ -257,7 +257,7 @@ class UserDefinedFormTest extends FunctionalTest
$this->assertEmpty($liveRule);
// Publish form, it's now live
$form->doPublish();
$form->publishSingle();
$liveRule = Versioned::get_one_by_stage(EditableCustomRule::class, 'Live', "\"EditableCustomRule_Live\".\"ID\" = $ruleID");
$this->assertNotEmpty($liveRule);
@ -269,7 +269,7 @@ class UserDefinedFormTest extends FunctionalTest
$this->assertNotEmpty($liveRule);
// Publish form, it should remove this rule
$form->doPublish();
$form->publishSingle();
$liveRule = Versioned::get_one_by_stage(EditableCustomRule::class, 'Live', "\"EditableCustomRule_Live\".\"ID\" = $ruleID");
$this->assertEmpty($liveRule);
}
@ -280,7 +280,7 @@ class UserDefinedFormTest extends FunctionalTest
$form = $this->objFromFixture(UserDefinedForm::class, 'basic-form-page');
$form->write();
$this->assertEquals(0, DB::query("SELECT COUNT(*) FROM \"EditableFormField_Live\"")->value());
$form->doPublish();
$form->publishSingle();
// assert that it exists and has a field
$live = Versioned::get_one_by_stage(UserDefinedForm::class, 'Live', "\"UserDefinedForm_Live\".\"ID\" = $form->ID");
@ -304,7 +304,7 @@ class UserDefinedFormTest extends FunctionalTest
$field->Title = 'Title';
$field->write();
$form->doPublish();
$form->publishSingle();
$field->Title = 'Edited title';
$field->write();
@ -331,7 +331,6 @@ class UserDefinedFormTest extends FunctionalTest
$duplicate = $form->duplicate();
$this->assertEquals($form->Fields()->Count(), $duplicate->Fields()->Count());
$this->assertEquals($form->EmailRecipients()->Count(), $form->EmailRecipients()->Count());
// can't compare object since the dates/ids change
$this->assertEquals($form->Fields()->First()->Title, $duplicate->Fields()->First()->Title);