MINOR Removed dependency on SiteTree in various unit tests

This commit is contained in:
Ingo Schommer 2011-03-23 16:32:24 +13:00
parent 50e8467535
commit a9b13509d2
23 changed files with 319 additions and 385 deletions

View File

@ -472,6 +472,8 @@ class Hierarchy extends DataObjectDecorator {
* from both stage & live.
*/
public function AllHistoricalChildren() {
if(!$this->owner->hasExtension('Versioned')) throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied');
$baseClass=ClassInfo::baseDataClass($this->owner->class);
return Versioned::get_including_deleted($baseClass,
"\"ParentID\" = " . (int)$this->owner->ID, "\"$baseClass\".\"ID\" ASC");
@ -481,6 +483,8 @@ class Hierarchy extends DataObjectDecorator {
* Return the number of children that this page ever had, including pages that were deleted
*/
public function numHistoricalChildren() {
if(!$this->owner->hasExtension('Versioned')) throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied');
$query = Versioned::get_including_deleted_query(ClassInfo::baseDataClass($this->owner->class),
"\"ParentID\" = " . (int)$this->owner->ID);
@ -550,6 +554,8 @@ class Hierarchy extends DataObjectDecorator {
* @return DataObjectSet
*/
public function liveChildren($showAll = false, $onlyDeletedFromStage = false) {
if(!$this->owner->hasExtension('Versioned')) throw new Exception('Hierarchy->liveChildren() only works with Versioned extension applied');
if($this->owner->db('ShowInMenus')) {
$extraFilter = ($showAll) ? '' : " AND \"ShowInMenus\"=1";
} else {

View File

@ -145,12 +145,14 @@ class Filesystem extends Object {
}
// Update the image tracking of all pages
if(class_exists('Subsite')) Subsite::$disable_subsite_filter = true;
foreach(DataObject::get("SiteTree") as $page) {
// syncLinkTracking is called by SiteTree::onBeforeWrite()
$page->write();
if(class_exists('SiteTree')) {
if(class_exists('Subsite')) Subsite::$disable_subsite_filter = true;
foreach(DataObject::get("SiteTree") as $page) {
// syncLinkTracking is called by SiteTree::onBeforeWrite()
$page->write();
}
if(class_exists('Subsite')) Subsite::$disable_subsite_filter = false;
}
if(class_exists('Subsite')) Subsite::$disable_subsite_filter = false;
return _t(
'Filesystem.SYNCRESULTS',

View File

@ -45,6 +45,8 @@ class FulltextSearchable extends DataObjectDecorator {
if(!is_array($searchableClasses)) $searchableClasses = array($searchableClasses);
foreach($searchableClasses as $class) {
if(!class_exists($class)) continue;
if(isset($defaultColumns[$class])) {
Object::add_extension($class, "FulltextSearchable('{$defaultColumns[$class]}')");
} else {

View File

@ -57,16 +57,16 @@ class DataObjectDecoratorTest extends SapphireTest {
$this->assertEquals(0, $parent->Faves()->Count());
$homepage = $this->objFromFixture('Page', 'home');
$firstpage = $this->objFromFixture('Page', 'page1');
$obj1 = $this->objFromFixture('DataObjectDecoratorTest_RelatedObject', 'obj1');
$obj2 = $this->objFromFixture('DataObjectDecoratorTest_RelatedObject', 'obj2');
$parent->Faves()->add($homepage->ID);
$parent->Faves()->add($obj1->ID);
$this->assertEquals(1, $parent->Faves()->Count());
$parent->Faves()->add($firstpage->ID);
$parent->Faves()->add($obj2->ID);
$this->assertEquals(2, $parent->Faves()->Count());
$parent->Faves()->remove($firstpage->ID);
$parent->Faves()->remove($obj2->ID);
$this->assertEquals(1, $parent->Faves()->Count());
}
@ -296,7 +296,7 @@ class DataObjectDecoratorTest_Faves extends DataObjectDecorator implements TestO
public function extraStatics() {
return array(
'many_many' => array(
'Faves' => 'Page'
'Faves' => 'DataObjectDecoratorTest_RelatedObject'
)
);
}

View File

@ -1,11 +1,8 @@
Page:
home:
Title: Home
page1:
Title: First Page
Content: <p>Some test content</p>
page2:
Title: Second Page
DataObjectDecoratorTest_RelatedObject:
obj1:
FieldOne: Obj1
obj2:
FieldOne: Obj2
Permission:
adminpermission:
Code: ADMIN

View File

@ -13,7 +13,9 @@ class DataObjectSetTest extends SapphireTest {
'DataObjectTest_Team',
'DataObjectTest_SubTeam',
'DataObjectTest_Player',
'DataObjectSetTest_TeamComment'
'DataObjectSetTest_TeamComment',
'DataObjectSetTest_Base',
'DataObjectSetTest_ChildClass',
);
function testArrayAccessExists() {
@ -232,14 +234,14 @@ class DataObjectSetTest extends SapphireTest {
// Ensure that duplicates are removed where the base data class is the same.
$mixedSet = new DataObjectSet();
$mixedSet->push(new SiteTree(array('ID' => 1)));
$mixedSet->push(new Page(array('ID' => 1))); // dup: same base class and ID
$mixedSet->push(new Page(array('ID' => 1))); // dup: more than one dup of the same object
$mixedSet->push(new Page(array('ID' => 2))); // not dup: same type again, but different
$mixedSet->push(new SiteTree(array('ID' => 1))); // dup: another dup, not consequetive.
$mixedSet->push(new DataObjectSetTest_Base(array('ID' => 1)));
$mixedSet->push(new DataObjectSetTest_ChildClass(array('ID' => 1))); // dup: same base class and ID
$mixedSet->push(new DataObjectSetTest_ChildClass(array('ID' => 1))); // dup: more than one dup of the same object
$mixedSet->push(new DataObjectSetTest_ChildClass(array('ID' => 2))); // not dup: same type again, but different
$mixedSet->push(new DataObjectSetTest_Base(array('ID' => 1))); // dup: another dup, not consequetive.
$mixedSet->removeDuplicates('ID');
$this->assertEquals($mixedSet->Count(), 2, 'There are 3 unique data objects in a very mixed set');
}
@ -422,4 +424,13 @@ class DataObjectSetTest_TeamComment extends DataObject implements TestOnly {
static $has_one = array(
'Team' => 'DataObjectTest_Team',
);
}
class DataObjectSetTest_Base extends DataObject implements TestOnly {
static $db = array(
'Name' => 'Varchar'
);
}
class DataObjectSetTest_ChildClass extends DataObjectSetTest_Base implements TestOnly {
}

View File

@ -1,18 +1,9 @@
Page:
home:
Title: Home
page1:
Title: First Page
Content: <p>Some test content</p>
page2:
Title: Second Page
DataObjectTest_Team:
team1:
Title: Team 1
team2:
Title: Team 2
team1:
Title: Team 1
team2:
Title: Team 2
DataObjectTest_Player:
captain1:
FirstName: Captain 1

View File

@ -61,28 +61,28 @@ class DataObjectTest extends SapphireTest {
function testDelete() {
// Test deleting using delete() on the DataObject
// Get the first page
$page = $this->objFromFixture('Page', 'page1');
$pageID = $page->ID;
$obj = $this->objFromFixture('DataObjectTest_Player', 'captain1');
$objID = $obj->ID;
// Check the page exists before deleting
$this->assertTrue(is_object($page) && $page->exists());
$this->assertTrue(is_object($obj) && $obj->exists());
// Delete the page
$page->delete();
$obj->delete();
// Check that page does not exist after deleting
$page = DataObject::get_by_id('Page', $pageID);
$this->assertTrue(!$page || !$page->exists());
$obj = DataObject::get_by_id('DataObjectTest_Player', $objID);
$this->assertTrue(!$obj || !$obj->exists());
// Test deleting using DataObject::delete_by_id()
// Get the second page
$page2 = $this->objFromFixture('Page', 'page2');
$page2ID = $page2->ID;
$obj = $this->objFromFixture('DataObjectTest_Player', 'captain2');
$objID = $obj->ID;
// Check the page exists before deleting
$this->assertTrue(is_object($page2) && $page2->exists());
$this->assertTrue(is_object($obj) && $obj->exists());
// Delete the page
DataObject::delete_by_id('Page', $page2->ID);
DataObject::delete_by_id('DataObjectTest_Player', $obj->ID);
// Check that page does not exist after deleting
$page2 = DataObject::get_by_id('Page', $page2ID);
$this->assertTrue(!$page2 || !$page2->exists());
$obj = DataObject::get_by_id('DataObjectTest_Player', $objID);
$this->assertTrue(!$obj || !$obj->exists());
}
/**
@ -146,9 +146,9 @@ class DataObjectTest extends SapphireTest {
// Test get_by_id()
$homepageID = $this->idFromFixture('Page', 'home');
$page = DataObject::get_by_id('Page', $homepageID);
$this->assertEquals('Home', $page->Title);
$captain1ID = $this->idFromFixture('DataObjectTest_Player', 'captain1');
$captain1 = DataObject::get_by_id('DataObjectTest_Player', $captain1ID);
$this->assertEquals('Captain', $captain1->FirstName);
// Test get_one() without caching
$comment1 = DataObject::get_one('DataObjectTest_TeamComment', "\"Name\" = 'Joe'", false);
@ -184,12 +184,12 @@ class DataObjectTest extends SapphireTest {
*
*/
function testWritePropertyWithoutDBField() {
$page = $this->objFromFixture('Page', 'page1');
$page->ParentID = 99;
$page->write();
$obj = $this->objFromFixture('DataObjectTest_Player', 'captain1');
$obj->FavouriteTeamID = 99;
$obj->write();
// reload the page from the database
$savedPage = DataObject::get_by_id('Page', $page->ID);
$this->assertTrue($savedPage->ParentID == 99);
$savedObj = DataObject::get_by_id('DataObjectTest_Player', $obj->ID);
$this->assertTrue($savedObj->FavouriteTeamID == 99);
}
/**
@ -278,19 +278,19 @@ class DataObjectTest extends SapphireTest {
* @todo Extend type change tests (e.g. '0'==NULL)
*/
function testChangedFields() {
$page = $this->objFromFixture('Page', 'home');
$page->Title = 'Home-Changed';
$page->ShowInMenus = true;
$obj = $this->objFromFixture('DataObjectTest_Player', 'captain1');
$obj->FirstName = 'Captain-changed';
$obj->IsRetired = true;
$this->assertEquals(
$page->getChangedFields(false, 1),
$obj->getChangedFields(false, 1),
array(
'Title' => array(
'before' => 'Home',
'after' => 'Home-Changed',
'FirstName' => array(
'before' => 'Captain',
'after' => 'Captain-changed',
'level' => 2
),
'ShowInMenus' => array(
'IsRetired' => array(
'before' => 1,
'after' => true,
'level' => 1
@ -300,25 +300,25 @@ class DataObjectTest extends SapphireTest {
);
$this->assertEquals(
$page->getChangedFields(false, 2),
$obj->getChangedFields(false, 2),
array(
'Title' => array(
'before'=>'Home',
'after'=>'Home-Changed',
'FirstName' => array(
'before'=>'Captain',
'after'=>'Captain-changed',
'level' => 2
)
),
'Changed fields are correctly detected while ignoring type changes (level=2)'
);
$newPage = new Page();
$newPage->Title = "New Page Title";
$newObj = new DataObjectTest_Player();
$newObj->FirstName = "New Player";
$this->assertEquals(
$newPage->getChangedFields(false, 2),
$newObj->getChangedFields(false, 2),
array(
'Title' => array(
'FirstName' => array(
'before' => null,
'after' => 'New Page Title',
'after' => 'New Player',
'level' => 2
)
),
@ -327,42 +327,42 @@ class DataObjectTest extends SapphireTest {
}
function testIsChanged() {
$page = $this->objFromFixture('Page', 'home');
$page->Title = 'Home-Changed';
$page->ShowInMenus = true; // type change only, database stores "1"
$obj = $this->objFromFixture('DataObjectTest_Player', 'captain1');
$obj->FirstName = 'Captain-changed';
$obj->IsRetired = true; // type change only, database stores "1"
$this->assertTrue($page->isChanged('Title', 1));
$this->assertTrue($page->isChanged('Title', 2));
$this->assertTrue($page->isChanged('ShowInMenus', 1));
$this->assertFalse($page->isChanged('ShowInMenus', 2));
$this->assertFalse($page->isChanged('Content', 1));
$this->assertFalse($page->isChanged('Content', 2));
$this->assertTrue($obj->isChanged('FirstName', 1));
$this->assertTrue($obj->isChanged('FirstName', 2));
$this->assertTrue($obj->isChanged('IsRetired', 1));
$this->assertFalse($obj->isChanged('IsRetired', 2));
$this->assertFalse($obj->isChanged('Email', 1), 'Doesnt change mark unchanged property');
$this->assertFalse($obj->isChanged('Email', 2), 'Doesnt change mark unchanged property');
$newPage = new Page();
$newPage->Title = "New Page Title";
$this->assertTrue($newPage->isChanged('Title', 1));
$this->assertTrue($newPage->isChanged('Title', 2));
$this->assertFalse($newPage->isChanged('Content', 1));
$this->assertFalse($newPage->isChanged('Content', 2));
$newObj = new DataObjectTest_Player();
$newObj->FirstName = "New Player";
$this->assertTrue($newObj->isChanged('FirstName', 1));
$this->assertTrue($newObj->isChanged('FirstName', 2));
$this->assertFalse($newObj->isChanged('Email', 1));
$this->assertFalse($newObj->isChanged('Email', 2));
$newPage->write();
$this->assertFalse($newPage->isChanged('Title', 1));
$this->assertFalse($newPage->isChanged('Title', 2));
$this->assertFalse($newPage->isChanged('Content', 1));
$this->assertFalse($newPage->isChanged('Content', 2));
$newObj->write();
$this->assertFalse($newObj->isChanged('FirstName', 1));
$this->assertFalse($newObj->isChanged('FirstName', 2));
$this->assertFalse($newObj->isChanged('Email', 1));
$this->assertFalse($newObj->isChanged('Email', 2));
$page = $this->objFromFixture('Page', 'home');
$page->Title = null;
$this->assertTrue($page->isChanged('Title', 1));
$this->assertTrue($page->isChanged('Title', 2));
$obj = $this->objFromFixture('DataObjectTest_Player', 'captain1');
$obj->FirstName = null;
$this->assertTrue($obj->isChanged('FirstName', 1));
$this->assertTrue($obj->isChanged('FirstName', 2));
/* Test when there's not field provided */
$page = $this->objFromFixture('Page', 'home');
$page->Title = "New Page Title";
$this->assertTrue($page->isChanged());
$obj = $this->objFromFixture('DataObjectTest_Player', 'captain1');
$obj->FirstName = "New Player";
$this->assertTrue($obj->isChanged());
$page->write();
$this->assertFalse($page->isChanged());
$obj->write();
$this->assertFalse($obj->isChanged());
}
function testRandomSort() {
@ -374,12 +374,12 @@ class DataObjectTest extends SapphireTest {
foreach($itemsB as $item) $keysB[] = $item->ID;
/* Test when there's not field provided */
$page = $this->objFromFixture('Page', 'home');
$page->Title = "New Page Title";
$this->assertTrue($page->isChanged());
$obj = $this->objFromFixture('DataObjectTest_Player', 'captain1');
$obj->FirstName = "New Player";
$this->assertTrue($obj->isChanged());
$page->write();
$this->assertFalse($page->isChanged());
$obj->write();
$this->assertFalse($obj->isChanged());
}
function testWriteSavesToHasOneRelations() {
@ -1002,6 +1002,10 @@ class DataObjectTest extends SapphireTest {
}
class DataObjectTest_Player extends Member implements TestOnly {
static $db = array(
'IsRetired' => 'Boolean'
);
static $has_one = array(
'FavouriteTeam' => 'DataObjectTest_Team',
);

View File

@ -1,12 +1,3 @@
Page:
home:
Title: Home
page1:
Title: First Page
Content: <p>Some test content</p>
page2:
Title: Second Page
DataObjectTest_Team:
team1:
Title: Team 1
@ -18,6 +9,7 @@ DataObjectTest_Player:
FirstName: Captain
FavouriteTeam: =>DataObjectTest_Team.team1
Teams: =>DataObjectTest_Team.team1
IsRetired: 1
captain2:
FirstName: Captain 2
Teams: =>DataObjectTest_Team.team2

View File

@ -17,16 +17,10 @@ class ObjectTest extends SapphireTest {
}
function testHasmethodBehaviour() {
/* SiteTree should have all of the methods that Versioned has, because Versioned is listed in SiteTree's
* extensions */
$st = new SiteTree();
$obj = new ObjectTest_ExtendTest();
$this->assertTrue($st->hasMethod('publish'), "Test SiteTree has publish");
$this->assertTrue($st->hasMethod('migrateVersion'), "Test SiteTree has migrateVersion");
/* This relationship should be case-insensitive, too */
$this->assertTrue($st->hasMethod('PuBliSh'), "Test SiteTree has PuBliSh");
$this->assertTrue($st->hasMethod('MiGratEVersIOn'), "Test SiteTree has MiGratEVersIOn");
$this->assertTrue($obj->hasMethod('extendableMethod'), "Extension method found in original spelling");
$this->assertTrue($obj->hasMethod('ExTendableMethod'), "Extension method found case-insensitive");
/* The above examples make use of SiteTree, Versioned and ContentController. Let's test defineMethods() more
* directly, with some sample objects */

View File

@ -319,7 +319,7 @@ class RequestHandlingTest_Controller extends Controller implements TestOnly {
}
public function getViewer(){
return new SSViewer('ContentController');
return new SSViewer('BlankPage');
}
}
@ -374,7 +374,7 @@ class RequestHandlingTest_FormActionController extends Controller {
}
public function getViewer(){
return new SSViewer('ContentController');
return new SSViewer('BlankPage');
}
}

View File

@ -78,8 +78,8 @@ class TransactionTest extends SapphireTest {
DataObject::flush_and_destroy_cache();
$success=DataObject::get('Page', "\"Title\"='Read only success'");
$fail=DataObject::get('Page', "\"Title\"='Read only page failed'");
$success=DataObject::get('TransactionTest_Object', "\"Title\"='Read only success'");
$fail=DataObject::get('TransactionTest_Object', "\"Title\"='Read only page failed'");
//This page should be in the system
$this->assertTrue(is_object($success) && $success->exists());

View File

@ -1,7 +1,3 @@
SiteTree:
home:
Title: Home
URLSegment: home
ComplexTableFieldTest_Player:
p1:
Name: Joe Bloggs

View File

@ -424,7 +424,7 @@ class FormTest_Controller extends Controller implements TestOnly {
}
function getViewer(){
return new SSViewer('ContentController');
return new SSViewer('BlankPage');
}
}
@ -461,7 +461,7 @@ class FormTest_ControllerWithSecurityToken extends Controller implements TestOnl
}
function getViewer(){
return new SSViewer('ContentController');
return new SSViewer('BlankPage');
}
}

View File

@ -35,48 +35,6 @@ class HtmlEditorFieldTest extends FunctionalTest {
$this->assertEquals('', $sitetree->Content, "Doesn't choke on empty/null values.");
}
public function testLinkTracking() {
$sitetree = $this->objFromFixture('SiteTree', 'home');
$editor = new HtmlEditorField('Content');
$aboutID = $this->idFromFixture('SiteTree', 'about');
$contactID = $this->idFromFixture('SiteTree', 'contact');
$editor->setValue("<a href=\"[sitetree_link id=$aboutID]\">Example Link</a>");
$editor->saveInto($sitetree);
$this->assertEquals(array($aboutID => $aboutID), $sitetree->LinkTracking()->getIdList(), 'Basic link tracking works.');
$editor->setValue (
"<a href=\"[sitetree_link id=$aboutID]\"></a><a href=\"[sitetree_link id=$contactID]\"></a>"
);
$editor->saveInto($sitetree);
$this->assertEquals (
array($aboutID => $aboutID, $contactID => $contactID),
$sitetree->LinkTracking()->getIdList(),
'Tracking works on multiple links'
);
$editor->setValue(null);
$editor->saveInto($sitetree);
$this->assertEquals(array(), $sitetree->LinkTracking()->getIdList(), 'Link tracking is removed when links are.');
}
public function testFileLinkTracking() {
$sitetree = $this->objFromFixture('SiteTree', 'home');
$editor = new HtmlEditorField('Content');
$fileID = $this->idFromFixture('File', 'example_file');
$editor->setValue('<a href="assets/example.pdf">Example File</a>');
$editor->saveInto($sitetree);
$this->assertEquals (
array($fileID => $fileID), $sitetree->ImageTracking()->getIDList(), 'Links to assets are tracked.'
);
$editor->setValue(null);
$editor->saveInto($sitetree);
$this->assertEquals(array(), $sitetree->ImageTracking()->getIdList(), 'Asset tracking is removed with links.');
}
public function testImageInsertion() {
$sitetree = new SiteTree();
$editor = new HtmlEditorField('Content');
@ -96,24 +54,6 @@ class HtmlEditorFieldTest extends FunctionalTest {
$this->assertNotNull('bar', $xml['title'], 'Title tags are preserved.');
}
public function testImageTracking() {
$sitetree = $this->objFromFixture('SiteTree', 'home');
$editor = new HtmlEditorField('Content');
$fileID = $this->idFromFixture('Image', 'example_image');
$editor->setValue('<img src="assets/example.jpg" />');
$editor->saveInto($sitetree);
$this->assertEquals (
array($fileID => $fileID), $sitetree->ImageTracking()->getIDList(), 'Inserted images are tracked.'
);
$editor->setValue(null);
$editor->saveInto($sitetree);
$this->assertEquals (
array(), $sitetree->ImageTracking()->getIDList(), 'Tracked images are deleted when removed.'
);
}
public function testMultiLineSaving() {
$sitetree = $this->objFromFixture('SiteTree', 'home');
$editor = new HtmlEditorField('Content');
@ -133,45 +73,6 @@ class HtmlEditorFieldTest extends FunctionalTest {
'<p><a name="example-anchor"/></p>', $sitetree->Content, 'Saving a link without a href attribute works'
);
}
public function testBrokenLinkTracking() {
$sitetree = new SiteTree();
$editor = new HtmlEditorField('Content');
$this->assertFalse((bool) $sitetree->HasBrokenLink);
$editor->setValue('<p><a href="[sitetree_link id=0]">Broken Link</a></p>');
$editor->saveInto($sitetree);
$this->assertTrue($sitetree->HasBrokenLink);
$editor->setValue(sprintf (
'<p><a href="[sitetree_link id=%d]">Working Link</a></p>',
$this->idFromFixture('SiteTree', 'home')
));
$sitetree->HasBrokenLink = false;
$editor->saveInto($sitetree);
$this->assertFalse((bool) $sitetree->HasBrokenLink);
}
public function testBrokenLinkHighlighting() {
$sitetree = new SiteTree();
$editor = new HtmlEditorField('Content');
$editor->setValue('<a href="[sitetree_link id=0]">Broken Link</a>');
$element = new SimpleXMLElement(html_entity_decode((string) new SimpleXMLElement($editor->Field())));
$this->assertContains('ss-broken', (string) $element['class'], 'A broken link class is added to broken links');
$editor->setValue(sprintf (
'<a href="[sitetree_link id=%d]">Working Link</a>',
$this->idFromFixture('SiteTree', 'home')
));
$element = new SimpleXMLElement(html_entity_decode((string) new SimpleXMLElement($editor->Field())));
$this->assertNotContains('ss-broken', (string) $element['class']);
}
public function testExtendImageFormFields() {
$controller = new ContentController();
@ -196,4 +97,10 @@ class HtmlEditorFieldTest_DummyImageFormFieldExtension extends Extension impleme
self::$update_called = true;
self::$fields = $form->Fields();
}
}
class HtmlEditorFieldTest_Object extends DataObject implements TestOnly {
static $db = array(
'Title' => 'Varchar'
);
}

View File

@ -1,15 +1,7 @@
SiteTree:
HtmlEditorFieldTest_Object:
home:
Title: Home Page
about:
Title: About Us
contact:
Title: Contact Us
File:
example_file:
Name: example.pdf
Image:
example_image:
Name: example.jpg
Title: Contact Us

View File

@ -1,6 +1,12 @@
<?php
class DataObjectDuplicationTest extends SapphireTest {
protected $extraDataObjects = array(
'DataObjectDuplicateTestClass1',
'DataObjectDuplicateTestClass2',
'DataObjectDuplicateTestClass3'
);
function testDuplicateManyManyClasses() {
//create new test classes below
@ -59,7 +65,7 @@ class DataObjectDuplicationTest extends SapphireTest {
}
class DataObjectDuplicateTestClass1 extends SiteTree {
class DataObjectDuplicateTestClass1 extends DataObject implements TestOnly {
static $db = array(
'text' => 'Varchar'
@ -74,7 +80,7 @@ class DataObjectDuplicateTestClass1 extends SiteTree {
);
}
class DataObjectDuplicateTestClass2 extends SiteTree {
class DataObjectDuplicateTestClass2 extends DataObject implements TestOnly {
static $db = array(
'text' => 'Varchar'
@ -86,7 +92,7 @@ class DataObjectDuplicateTestClass2 extends SiteTree {
}
class DataObjectDuplicateTestClass3 extends SiteTree {
class DataObjectDuplicateTestClass3 extends DataObject implements TestOnly {
static $db = array(
'text' => 'Varchar'

View File

@ -3,6 +3,8 @@
class DbDatetimeTest extends FunctionalTest {
static $fixture_file = 'sapphire/tests/model/DbDatetimeTest.yml';
protected $extraDataObjects = array('DbDatetimeTest_Team');
private static $offset = 0; // number of seconds of php and db time are out of sync
private static $offset_thresholds = array( // throw an error if the offset exceeds 30 minutes
@ -69,10 +71,10 @@ class DbDatetimeTest extends FunctionalTest {
$result = DB::query($query)->value();
$this->matchesRoughly($result, date('d', $this->getDbNow()), 'todays day');
$query = 'SELECT ' . $this->adapter->formattedDatetimeClause('"Created"', '%U') . ' AS test FROM "SiteTree" WHERE "URLSegment" = \'home\'';
$query = 'SELECT ' . $this->adapter->formattedDatetimeClause('"Created"', '%U') . ' AS test FROM "DbDateTimeTest_Team"';
$result = DB::query($query)->value();
$this->matchesRoughly($result, strtotime(DataObject::get_one('SiteTree',"\"URLSegment\" = 'home'")->Created), 'SiteTree[home]->Created as timestamp');
$this->matchesRoughly($result, strtotime(DataObject::get_one('DbDateTimeTest_Team')->Created), 'fixture ->Created as timestamp');
}
}
@ -87,9 +89,9 @@ class DbDatetimeTest extends FunctionalTest {
$result = DB::query($query)->value();
$this->matchesRoughly($result, date('Y-m-d H:i:s', strtotime('+1 Day', $this->getDbNow())), 'tomorrow');
$query = 'SELECT ' . $this->adapter->datetimeIntervalClause('"Created"', '-15 Minutes') . ' AS "test" FROM "SiteTree" WHERE "URLSegment" = \'home\'';
$query = 'SELECT ' . $this->adapter->datetimeIntervalClause('"Created"', '-15 Minutes') . ' AS "test" FROM "DbDateTimeTest_Team" LIMIT 1';
$result = DB::query($query)->value();
$this->matchesRoughly($result, date('Y-m-d H:i:s', strtotime(Dataobject::get_one('SiteTree',"\"URLSegment\" = 'home'")->Created) - 900), '15 Minutes before creating SiteTree[home]');
$this->matchesRoughly($result, date('Y-m-d H:i:s', strtotime(Dataobject::get_one('DbDateTimeTest_Team')->Created) - 900), '15 Minutes before creating fixture');
}
}
@ -109,13 +111,19 @@ class DbDatetimeTest extends FunctionalTest {
$result = DB::query($query)->value();
$this->matchesRoughly($result, -45 * 60, 'now - 45 minutes ahead');
$query = 'SELECT ' . $this->adapter->datetimeDifferenceClause('"LastEdited"', '"Created"') . ' AS "test" FROM "SiteTree" WHERE "URLSegment" = \'home\'';
$query = 'SELECT ' . $this->adapter->datetimeDifferenceClause('"LastEdited"', '"Created"') . ' AS "test" FROM "DbDateTimeTest_Team" LIMIT 1';
$result = DB::query($query)->value();
$lastedited = Dataobject::get_one('SiteTree',"\"URLSegment\" = 'home'")->LastEdited;
$created = Dataobject::get_one('SiteTree',"\"URLSegment\" = 'home'")->Created;
$lastedited = Dataobject::get_one('DbDateTimeTest_Team')->LastEdited;
$created = Dataobject::get_one('DbDateTimeTest_Team')->Created;
$this->matchesRoughly($result, strtotime($lastedited) - strtotime($created), 'age of HomePage record in seconds since unix epoc');
}
}
}
class DbDateTimeTest_Team extends DataObject implements TestOnly {
static $db = array(
'Title' => 'Varchar'
);
}

View File

@ -1,4 +1,5 @@
Page:
first:
Title: Home page
URLSegment: home
DbDateTimeTest_Team:
team1:
Title: Team 1
team2:
Title: Team 2

View File

@ -2,112 +2,131 @@
class HierarchyTest extends SapphireTest {
static $fixture_file = 'sapphire/tests/model/HierarchyTest.yml';
protected $requiredExtensions = array(
'HierarchyTest_Object' => array('Hierarchy', 'Versioned')
);
protected $extraDataObjects = array(
'HierarchyTest_Object'
);
/**
* Test Hierarchy::AllHistoricalChildren().
*/
function testAllHistoricalChildren() {
// Delete some pages
$this->objFromFixture('Page', 'page2b')->delete();
$this->objFromFixture('Page', 'page3a')->delete();
$this->objFromFixture('Page', 'page3')->delete();
// Delete some objs
$this->objFromFixture('HierarchyTest_Object', 'obj2b')->delete();
$this->objFromFixture('HierarchyTest_Object', 'obj3a')->delete();
$this->objFromFixture('HierarchyTest_Object', 'obj3')->delete();
// Check that page1-3 appear at the top level of the AllHistoricalChildren tree
$this->assertEquals(array("Page 1", "Page 2", "Page 3"),
singleton('Page')->AllHistoricalChildren()->column('Title'));
// Check that obj1-3 appear at the top level of the AllHistoricalChildren tree
$this->assertEquals(array("Obj 1", "Obj 2", "Obj 3"),
singleton('HierarchyTest_Object')->AllHistoricalChildren()->column('Title'));
// Check numHistoricalChildren
$this->assertEquals(3, singleton('Page')->numHistoricalChildren());
$this->assertEquals(3, singleton('HierarchyTest_Object')->numHistoricalChildren());
// Check that both page 2 children are returned
$page2 = $this->objFromFixture('Page', 'page2');
$this->assertEquals(array("Page 2a", "Page 2b"),
$page2->AllHistoricalChildren()->column('Title'));
// Check that both obj 2 children are returned
$obj2 = $this->objFromFixture('HierarchyTest_Object', 'obj2');
$this->assertEquals(array("Obj 2a", "Obj 2b"),
$obj2->AllHistoricalChildren()->column('Title'));
// Check numHistoricalChildren
$this->assertEquals(2, $page2->numHistoricalChildren());
$this->assertEquals(2, $obj2->numHistoricalChildren());
// Page 3 has been deleted; let's bring it back from the grave
$page3 = Versioned::get_including_deleted("SiteTree", "\"Title\" = 'Page 3'")->First();
// Obj 3 has been deleted; let's bring it back from the grave
$obj3 = Versioned::get_including_deleted("HierarchyTest_Object", "\"Title\" = 'Obj 3'")->First();
// Check that both page 3 children are returned
$this->assertEquals(array("Page 3a", "Page 3b"),
$page3->AllHistoricalChildren()->column('Title'));
// Check that both obj 3 children are returned
$this->assertEquals(array("Obj 3a", "Obj 3b"),
$obj3->AllHistoricalChildren()->column('Title'));
// Check numHistoricalChildren
$this->assertEquals(2, $page3->numHistoricalChildren());
$this->assertEquals(2, $obj3->numHistoricalChildren());
}
/**
* Test that you can call Hierarchy::markExpanded/Unexpanded/Open() on a page, and that
* Test that you can call Hierarchy::markExpanded/Unexpanded/Open() on a obj, and that
* calling Hierarchy::isMarked() on a different instance of that object will return true.
*/
function testItemMarkingIsntRestrictedToSpecificInstance() {
// Mark a few pages
$this->objFromFixture('Page', 'page2')->markExpanded();
$this->objFromFixture('Page', 'page2a')->markExpanded();
$this->objFromFixture('Page', 'page2b')->markExpanded();
$this->objFromFixture('Page', 'page3')->markUnexpanded();
// Mark a few objs
$this->objFromFixture('HierarchyTest_Object', 'obj2')->markExpanded();
$this->objFromFixture('HierarchyTest_Object', 'obj2a')->markExpanded();
$this->objFromFixture('HierarchyTest_Object', 'obj2b')->markExpanded();
$this->objFromFixture('HierarchyTest_Object', 'obj3')->markUnexpanded();
// Query some pages in a different context and check their m
$pages = DataObject::get("Page", '', '"ID" ASC');
// Query some objs in a different context and check their m
$objs = DataObject::get("HierarchyTest_Object", '', '"ID" ASC');
$marked = $expanded = array();
foreach($pages as $page) {
if($page->isMarked()) $marked[] = $page->Title;
if($page->isExpanded()) $expanded[] = $page->Title;
foreach($objs as $obj) {
if($obj->isMarked()) $marked[] = $obj->Title;
if($obj->isExpanded()) $expanded[] = $obj->Title;
}
$this->assertEquals(array('Page 2', 'Page 3', 'Page 2a', 'Page 2b'), $marked);
$this->assertEquals(array('Page 2', 'Page 2a', 'Page 2b'), $expanded);
$this->assertEquals(array('Obj 2', 'Obj 3', 'Obj 2a', 'Obj 2b'), $marked);
$this->assertEquals(array('Obj 2', 'Obj 2a', 'Obj 2b'), $expanded);
}
function testNumChildren() {
$this->assertEquals($this->objFromFixture('Page', 'page1')->numChildren(), 0);
$this->assertEquals($this->objFromFixture('Page', 'page2')->numChildren(), 2);
$this->assertEquals($this->objFromFixture('Page', 'page3')->numChildren(), 2);
$this->assertEquals($this->objFromFixture('Page', 'page2a')->numChildren(), 2);
$this->assertEquals($this->objFromFixture('Page', 'page2b')->numChildren(), 0);
$this->assertEquals($this->objFromFixture('Page', 'page3a')->numChildren(), 2);
$this->assertEquals($this->objFromFixture('Page', 'page3b')->numChildren(), 0);
$this->assertEquals($this->objFromFixture('HierarchyTest_Object', 'obj1')->numChildren(), 0);
$this->assertEquals($this->objFromFixture('HierarchyTest_Object', 'obj2')->numChildren(), 2);
$this->assertEquals($this->objFromFixture('HierarchyTest_Object', 'obj3')->numChildren(), 2);
$this->assertEquals($this->objFromFixture('HierarchyTest_Object', 'obj2a')->numChildren(), 2);
$this->assertEquals($this->objFromFixture('HierarchyTest_Object', 'obj2b')->numChildren(), 0);
$this->assertEquals($this->objFromFixture('HierarchyTest_Object', 'obj3a')->numChildren(), 2);
$this->assertEquals($this->objFromFixture('HierarchyTest_Object', 'obj3b')->numChildren(), 0);
$page1 = $this->objFromFixture('Page', 'page1');
$this->assertEquals($page1->numChildren(), 0);
$page1Child1 = new Page();
$page1Child1->ParentID = $page1->ID;
$page1Child1->write();
$this->assertEquals($page1->numChildren(false), 1,
$obj1 = $this->objFromFixture('HierarchyTest_Object', 'obj1');
$this->assertEquals($obj1->numChildren(), 0);
$obj1Child1 = new HierarchyTest_Object();
$obj1Child1->ParentID = $obj1->ID;
$obj1Child1->write();
$this->assertEquals($obj1->numChildren(false), 1,
'numChildren() caching can be disabled through method parameter'
);
$page1Child2 = new Page();
$page1Child2->ParentID = $page1->ID;
$page1Child2->write();
$page1->flushCache();
$this->assertEquals($page1->numChildren(), 2,
$obj1Child2 = new HierarchyTest_Object();
$obj1Child2->ParentID = $obj1->ID;
$obj1Child2->write();
$obj1->flushCache();
$this->assertEquals($obj1->numChildren(), 2,
'numChildren() caching can be disabled by flushCache()'
);
}
function testLoadDescendantIDListIntoArray() {
$page2 = $this->objFromFixture('Page', 'page2');
$page2a = $this->objFromFixture('Page', 'page2a');
$page2b = $this->objFromFixture('Page', 'page2b');
$page2aa = $this->objFromFixture('Page', 'page2aa');
$page2ab = $this->objFromFixture('Page', 'page2ab');
$obj2 = $this->objFromFixture('HierarchyTest_Object', 'obj2');
$obj2a = $this->objFromFixture('HierarchyTest_Object', 'obj2a');
$obj2b = $this->objFromFixture('HierarchyTest_Object', 'obj2b');
$obj2aa = $this->objFromFixture('HierarchyTest_Object', 'obj2aa');
$obj2ab = $this->objFromFixture('HierarchyTest_Object', 'obj2ab');
$page2IdList = $page2->getDescendantIDList();
$page2aIdList = $page2a->getDescendantIDList();
$obj2IdList = $obj2->getDescendantIDList();
$obj2aIdList = $obj2a->getDescendantIDList();
$this->assertContains($page2a->ID, $page2IdList);
$this->assertContains($page2b->ID, $page2IdList);
$this->assertContains($page2aa->ID, $page2IdList);
$this->assertContains($page2ab->ID, $page2IdList);
$this->assertEquals(4, count($page2IdList));
$this->assertContains($obj2a->ID, $obj2IdList);
$this->assertContains($obj2b->ID, $obj2IdList);
$this->assertContains($obj2aa->ID, $obj2IdList);
$this->assertContains($obj2ab->ID, $obj2IdList);
$this->assertEquals(4, count($obj2IdList));
$this->assertContains($page2aa->ID, $page2aIdList);
$this->assertContains($page2ab->ID, $page2aIdList);
$this->assertEquals(2, count($page2aIdList));
$this->assertContains($obj2aa->ID, $obj2aIdList);
$this->assertContains($obj2ab->ID, $obj2aIdList);
$this->assertEquals(2, count($obj2aIdList));
}
}
class HierarchyTest_Object extends DataObject implements TestOnly {
static $db = array(
'Title' => 'Varchar'
);
static $extensions = array(
'Hierarchy',
"Versioned('Stage', 'Live')",
);
}

View File

@ -1,31 +1,31 @@
Page:
page1:
Title: Page 1
page2:
Title: Page 2
page3:
Title: Page 3
page2a:
Parent: =>Page.page2
Title: Page 2a
page2b:
Parent: =>Page.page2
Title: Page 2b
page3a:
Parent: =>Page.page3
Title: Page 3a
page3b:
Parent: =>Page.page3
Title: Page 3b
page2aa:
Parent: =>Page.page2a
Title: Page 2aa
page2ab:
Parent: =>Page.page2a
Title: Page 2ab
page3aa:
Parent: =>Page.page3a
Title: Page 3aa
page3ab:
Parent: =>Page.page3a
Title: Page 3ab
HierarchyTest_Object:
obj1:
Title: Obj 1
obj2:
Title: Obj 2
obj3:
Title: Obj 3
obj2a:
Parent: =>HierarchyTest_Object.obj2
Title: Obj 2a
obj2b:
Parent: =>HierarchyTest_Object.obj2
Title: Obj 2b
obj3a:
Parent: =>HierarchyTest_Object.obj3
Title: Obj 3a
obj3b:
Parent: =>HierarchyTest_Object.obj3
Title: Obj 3b
obj2aa:
Parent: =>HierarchyTest_Object.obj2a
Title: Obj 2aa
obj2ab:
Parent: =>HierarchyTest_Object.obj2a
Title: Obj 2ab
obj3aa:
Parent: =>HierarchyTest_Object.obj3a
Title: Obj 3aa
obj3ab:
Parent: =>HierarchyTest_Object.obj3a
Title: Obj 3ab

View File

@ -71,22 +71,22 @@ class VersionedTest extends SapphireTest {
*/
function testGetIncludingDeleted() {
// Delete a page
$this->objFromFixture('Page', 'page3')->delete();
$this->objFromFixture('VersionedTest_DataObject', 'page3')->delete();
// Get all items, ignoring deleted
$remainingPages = DataObject::get("SiteTree", "\"ParentID\" = 0", "\"SiteTree\".\"ID\" ASC");
$remainingPages = DataObject::get("VersionedTest_DataObject", "\"ParentID\" = 0", "\"VersionedTest_DataObject\".\"ID\" ASC");
// Check that page 3 has gone
$this->assertNotNull($remainingPages);
$this->assertEquals(array("Page 1", "Page 2"), $remainingPages->column('Title'));
// Get all including deleted
$allPages = Versioned::get_including_deleted("SiteTree", "\"ParentID\" = 0", "\"SiteTree\".\"ID\" ASC");
$allPages = Versioned::get_including_deleted("VersionedTest_DataObject", "\"ParentID\" = 0", "\"VersionedTest_DataObject\".\"ID\" ASC");
// Check that page 3 is still there
$this->assertEquals(array("Page 1", "Page 2", "Page 3"), $allPages->column('Title'));
// Check that this still works if we switch to reading the other stage
Versioned::reading_stage("Live");
$allPages = Versioned::get_including_deleted("SiteTree", "\"ParentID\" = 0", "\"SiteTree\".\"ID\" ASC");
$allPages = Versioned::get_including_deleted("VersionedTest_DataObject", "\"ParentID\" = 0", "\"VersionedTest_DataObject\".\"ID\" ASC");
$this->assertEquals(array("Page 1", "Page 2", "Page 3"), $allPages->column('Title'));
}
@ -102,7 +102,7 @@ class VersionedTest extends SapphireTest {
}
function testPublishCreateNewVersion() {
$page1 = $this->objFromFixture('Page', 'page1');
$page1 = $this->objFromFixture('VersionedTest_DataObject', 'page1');
$page1->Content = 'orig';
$page1->write();
$oldVersion = $page1->Version;
@ -117,7 +117,7 @@ class VersionedTest extends SapphireTest {
}
function testRollbackTo() {
$page1 = $this->objFromFixture('Page', 'page1');
$page1 = $this->objFromFixture('VersionedTest_DataObject', 'page1');
$page1->Content = 'orig';
$page1->write();
$page1->publish('Stage', 'Live');
@ -129,51 +129,51 @@ class VersionedTest extends SapphireTest {
$changedVersion = $page1->Version;
$page1->doRollbackTo($origVersion);
$page1 = Versioned::get_one_by_stage('Page', 'Stage', sprintf('"SiteTree"."ID" = %d', $page1->ID));
$page1 = Versioned::get_one_by_stage('VersionedTest_DataObject', 'Stage', sprintf('"VersionedTest_DataObject"."ID" = %d', $page1->ID));
$this->assertTrue($page1->Version > $changedVersion, 'Create a new higher version number');
$this->assertEquals('orig', $page1->Content, 'Copies the content from the old version');
}
function testDeleteFromStage() {
$page1 = $this->objFromFixture('Page', 'page1');
$page1 = $this->objFromFixture('VersionedTest_DataObject', 'page1');
$pageID = $page1->ID;
$page1->Content = 'orig';
$page1->write();
$page1->publish('Stage', 'Live');
$this->assertEquals(1, DB::query('SELECT COUNT(*) FROM "SiteTree" WHERE "ID" = '.$pageID)->value());
$this->assertEquals(1, DB::query('SELECT COUNT(*) FROM "SiteTree_Live" WHERE "ID" = '.$pageID)->value());
$this->assertEquals(1, DB::query('SELECT COUNT(*) FROM "VersionedTest_DataObject" WHERE "ID" = '.$pageID)->value());
$this->assertEquals(1, DB::query('SELECT COUNT(*) FROM "VersionedTest_DataObject_Live" WHERE "ID" = '.$pageID)->value());
$page1->deleteFromStage('Live');
// Confirm that deleteFromStage() doesn't manipulate the original record
$this->assertEquals($pageID, $page1->ID);
$this->assertEquals(1, DB::query('SELECT COUNT(*) FROM "SiteTree" WHERE "ID" = '.$pageID)->value());
$this->assertEquals(0, DB::query('SELECT COUNT(*) FROM "SiteTree_Live" WHERE "ID" = '.$pageID)->value());
$this->assertEquals(1, DB::query('SELECT COUNT(*) FROM "VersionedTest_DataObject" WHERE "ID" = '.$pageID)->value());
$this->assertEquals(0, DB::query('SELECT COUNT(*) FROM "VersionedTest_DataObject_Live" WHERE "ID" = '.$pageID)->value());
$page1->delete();
$this->assertEquals(0, $page1->ID);
$this->assertEquals(0, DB::query('SELECT COUNT(*) FROM "SiteTree" WHERE "ID" = '.$pageID)->value());
$this->assertEquals(0, DB::query('SELECT COUNT(*) FROM "SiteTree_Live" WHERE "ID" = '.$pageID)->value());
$this->assertEquals(0, DB::query('SELECT COUNT(*) FROM "VersionedTest_DataObject" WHERE "ID" = '.$pageID)->value());
$this->assertEquals(0, DB::query('SELECT COUNT(*) FROM "VersionedTest_DataObject_Live" WHERE "ID" = '.$pageID)->value());
}
function testWritingNewToStage() {
$origStage = Versioned::current_stage();
Versioned::reading_stage("Stage");
$page = new Page();
$page = new VersionedTest_DataObject();
$page->Title = "testWritingNewToStage";
$page->URLSegment = "testWritingNewToStage";
$page->write();
$live = Versioned::get_by_stage('SiteTree', 'Live', "\"SiteTree_Live\".\"ID\"='$page->ID'");
$live = Versioned::get_by_stage('VersionedTest_DataObject', 'Live', "\"VersionedTest_DataObject_Live\".\"ID\"='$page->ID'");
$this->assertNull($live);
$stage = Versioned::get_by_stage('SiteTree', 'Stage', "\"SiteTree\".\"ID\"='$page->ID'");
$stage = Versioned::get_by_stage('VersionedTest_DataObject', 'Stage', "\"VersionedTest_DataObject\".\"ID\"='$page->ID'");
$this->assertNotNull($stage);
$this->assertEquals($stage->First()->Title, 'testWritingNewToStage');
@ -182,23 +182,23 @@ class VersionedTest extends SapphireTest {
/**
* This tests for the situation described in the ticket #5596.
* Writing new Page to live first creates a row in SiteTree table (to get the new ID), then "changes
* it's mind" in Versioned and writes SiteTree_Live. It does not remove the SiteTree record though.
* Writing new Page to live first creates a row in VersionedTest_DataObject table (to get the new ID), then "changes
* it's mind" in Versioned and writes VersionedTest_DataObject_Live. It does not remove the VersionedTest_DataObject record though.
*/
function testWritingNewToLive() {
$origStage = Versioned::current_stage();
Versioned::reading_stage("Live");
$page = new Page();
$page = new VersionedTest_DataObject();
$page->Title = "testWritingNewToLive";
$page->URLSegment = "testWritingNewToLive";
$page->write();
$live = Versioned::get_by_stage('SiteTree', 'Live', "\"SiteTree_Live\".\"ID\"='$page->ID'");
$live = Versioned::get_by_stage('VersionedTest_DataObject', 'Live', "\"VersionedTest_DataObject_Live\".\"ID\"='$page->ID'");
$this->assertNotNull($live->First());
$this->assertEquals($live->First()->Title, 'testWritingNewToLive');
$stage = Versioned::get_by_stage('SiteTree', 'Stage', "\"SiteTree\".\"ID\"='$page->ID'");
$stage = Versioned::get_by_stage('VersionedTest_DataObject', 'Stage', "\"VersionedTest_DataObject\".\"ID\"='$page->ID'");
$this->assertNull($stage);
Versioned::reading_stage($origStage);
@ -235,11 +235,17 @@ class VersionedTest extends SapphireTest {
class VersionedTest_DataObject extends DataObject implements TestOnly {
static $db = array(
"Name" => "Varchar",
'Title' => 'Varchar',
'Content' => 'HTMLText'
);
static $extensions = array(
"Versioned('Stage', 'Live')"
);
static $has_one = array(
'Parent' => 'VersionedTest_DataObject'
);
}
class VersionedTest_Subclass extends VersionedTest_DataObject implements TestOnly {

View File

@ -1,4 +1,4 @@
Page:
VersionedTest_DataObject:
page1:
Title: Page 1
page2:
@ -6,14 +6,14 @@ Page:
page3:
Title: Page 3
page2a:
Parent: =>Page.page2
Parent: =>VersionedTest_DataObject.page2
Title: Page 2a
page2b:
Parent: =>Page.page2
Parent: =>VersionedTest_DataObject.page2
Title: Page 2b
page3a:
Parent: =>Page.page3
Parent: =>VersionedTest_DataObject.page3
Title: Page 3a
page3b:
Parent: =>Page.page3
Parent: =>VersionedTest_DataObject.page3
Title: Page 3b