From dd6a86c1c15a6706e8569b141b8537c8e5658c67 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Wed, 23 Mar 2011 12:00:35 +1300 Subject: [PATCH] MINOR Moved Widget, WidgetArea and related files from 'sapphire' to 'cms' module --- code/Widget.php | 233 +++++++++++++++++++++++++++ code/WidgetArea.php | 73 +++++++++ templates/WidgetArea.ss | 3 + templates/WidgetHolder.ss | 4 + tests/WidgetControllerTest.php | 89 ++++++++++ tests/WidgetControllerTest.yml | 10 ++ tests/WidgetControllerTestPage.php | 27 ++++ tests/WidgetControllerTestPage.ss | 1 + tests/WidgetControllerTest_Widget.ss | 1 + 9 files changed, 441 insertions(+) create mode 100755 code/Widget.php create mode 100644 code/WidgetArea.php create mode 100644 templates/WidgetArea.ss create mode 100644 templates/WidgetHolder.ss create mode 100644 tests/WidgetControllerTest.php create mode 100644 tests/WidgetControllerTest.yml create mode 100644 tests/WidgetControllerTestPage.php create mode 100644 tests/WidgetControllerTestPage.ss create mode 100644 tests/WidgetControllerTest_Widget.ss diff --git a/code/Widget.php b/code/Widget.php new file mode 100755 index 00000000..aaddd2af --- /dev/null +++ b/code/Widget.php @@ -0,0 +1,233 @@ + "Int", + "Enabled" => "Boolean" + ); + + static $defaults = array( + 'Enabled' => true + ); + + static $has_one = array( + "Parent" => "WidgetArea", + ); + + static $has_many = array(); + static $many_many = array(); + static $belongs_many_many = array(); + + static $default_sort = "\"Sort\""; + + static $title = "Widget Title"; + static $cmsTitle = "Name of this widget"; + static $description = "Description of what this widget does."; + + function getCMSFields() { + $fields = new FieldSet(); + $this->extend('updateCMSFields', $fields); + return $fields; + } + + /** + * Note: Overloaded in {@link Widget_Controller}. + * + * @return string HTML + */ + function WidgetHolder() { + return $this->renderWith("WidgetHolder"); + } + + /** + * Renders the widget content in a custom template with the same name as the current class. + * This should be the main point of output customization. + * + * Invoked from within WidgetHolder.ss, which contains + * the "framing" around the custom content, like a title. + * + * Note: Overloaded in {@link Widget_Controller}. + * + * @return string HTML + */ + function Content() { + return $this->renderWith(array_reverse(ClassInfo::ancestry($this->class))); + } + + function Title() { + return Object::get_static($this->class, 'title'); + } + + function CMSTitle() { + return Object::get_static($this->class, 'cmsTitle'); + } + + function Description() { + return Object::get_static($this->class, 'description'); + } + + function DescriptionSegment() { + return $this->renderWith('WidgetDescription'); + } + + /** + * @see Widget_Controller->editablesegment() + */ + function EditableSegment() { + return $this->renderWith('WidgetEditor'); + } + + function CMSEditor() { + $output = ''; + $fields = $this->getCMSFields(); + foreach($fields as $field) { + $name = $field->Name(); + $field->setValue($this->getField($name)); + $renderedField = $field->FieldHolder(); + $renderedField = ereg_replace("name=\"([A-Za-z0-9\-_]+)\"", "name=\"Widget[" . $this->ID . "][\\1]\"", $renderedField); + $renderedField = ereg_replace("id=\"([A-Za-z0-9\-_]+)\"", "id=\"Widget[" . $this->ID . "][\\1]\"", $renderedField); + $output .= $renderedField; + } + return $output; + } + + function ClassName() { + return $this->class; + } + + function Name() { + return "Widget[".$this->ID."]"; + } + + function populateFromPostData($data) { + foreach($data as $name => $value) { + if($name != "Type") { + $this->setField($name, $value); + } + } + + $this->write(); + + // The field must be written to ensure a unique ID. + $this->Name = $this->class.$this->ID; + $this->write(); + } + +} + +/** + * Optional controller for every widget which has its own logic, + * e.g. in forms. It always handles a single widget, usually passed + * in as a database identifier through the controller URL. + * Needs to be constructed as a nested controller + * within a {@link ContentController}. + * + * ## Forms + * You can add forms like in any other sapphire controller. + * If you need access to the widget from within a form, + * you can use `$this->controller->getWidget()` inside the form logic. + * Note: Widget controllers currently only work on {@link Page} objects, + * because the logic is implemented in {@link ContentController->handleWidget()}. + * Copy this logic and the URL rules to enable it for other controllers. + * + * @package sapphire + * @subpackage widgets + */ +class Widget_Controller extends Controller { + + /** + * @var Widget + */ + protected $widget; + + static $allowed_actions = array( + 'editablesegment' + ); + + function __construct($widget = null) { + // TODO This shouldn't be optional, is only necessary for editablesegment() + if($widget) { + $this->widget = $widget; + $this->failover = $widget; + } + + parent::__construct(); + } + + public function Link($action = null) { + $segment = Controller::join_links('widget', ($this->widget ? $this->widget->ID : null), $action); + + if(Director::get_current_page()) { + return Director::get_current_page()->Link($segment); + } else { + return Controller::curr()->Link($segment); + } + } + + /** + * @return Widget + */ + function getWidget() { + return $this->widget; + } + + /** + * Overloaded from {@link Widget->Content()} + * to allow for controller/form linking. + * + * @return string HTML + */ + function Content() { + return $this->renderWith(array_reverse(ClassInfo::ancestry($this->widget->class))); + } + + /** + * Overloaded from {@link Widget->WidgetHolder()} + * to allow for controller/form linking. + * + * @return string HTML + */ + function WidgetHolder() { + return $this->renderWith("WidgetHolder"); + } + + /** + * Uses the `WidgetEditor.ss` template and {@link Widget->editablesegment()} + * to render a administrator-view of the widget. It is assumed that this + * view contains form elements which are submitted and saved through {@link WidgetAreaEditor} + * within the CMS interface. + * + * @return string HTML + */ + function editablesegment() { + $className = $this->urlParams['ID']; + if(class_exists($className) && is_subclass_of($className, 'Widget')) { + $obj = new $className(); + return $obj->EditableSegment(); + } else { + user_error("Bad widget class: $className", E_USER_WARNING); + return "Bad widget class name given"; + } + } +} + +/** + * @package sapphire + * @subpackage widgets + */ +class Widget_TreeDropdownField extends TreeDropdownField { + function FieldHolder() {} + function Field() {} +} + +?> \ No newline at end of file diff --git a/code/WidgetArea.php b/code/WidgetArea.php new file mode 100644 index 00000000..14075d25 --- /dev/null +++ b/code/WidgetArea.php @@ -0,0 +1,73 @@ + "Widget" + ); + + static $many_many = array(); + + static $belongs_many_many = array(); + + public $template = __CLASS__; + + /** + * Used in template instead of {@link Widgets()} + * to wrap each widget in its controller, making + * it easier to access and process form logic + * and actions stored in {@link Widget_Controller}. + * + * @return DataObjectSet Collection of {@link Widget_Controller} + */ + function WidgetControllers() { + $controllers = new DataObjectSet(); + + foreach($this->ItemsToRender() as $widget) { + // find controller + $controllerClass = ''; + foreach(array_reverse(ClassInfo::ancestry($widget->class)) as $widgetClass) { + $controllerClass = "{$widgetClass}_Controller"; + if(class_exists($controllerClass)) break; + } + $controller = new $controllerClass($widget); + $controller->init(); + $controllers->push($controller); + } + + return $controllers; + } + + function Items() { + return $this->getComponents('Widgets'); + } + + function ItemsToRender() { + return $this->getComponents('Widgets', "\"Widget\".\"Enabled\" = 1"); + } + + function forTemplate() { + return $this->renderWith($this->template); + } + + function setTemplate($template) { + $this->template = $template; + } + + function onBeforeDelete() { + parent::onBeforeDelete(); + foreach($this->Widgets() as $widget) { + $widget->delete(); + } + } +} + +?> \ No newline at end of file diff --git a/templates/WidgetArea.ss b/templates/WidgetArea.ss new file mode 100644 index 00000000..b5279962 --- /dev/null +++ b/templates/WidgetArea.ss @@ -0,0 +1,3 @@ +<% control WidgetControllers %> + $WidgetHolder +<% end_control %> \ No newline at end of file diff --git a/templates/WidgetHolder.ss b/templates/WidgetHolder.ss new file mode 100644 index 00000000..e6787fd5 --- /dev/null +++ b/templates/WidgetHolder.ss @@ -0,0 +1,4 @@ +
+ <% if Title %>

$Title

<% end_if %> + $Content +
diff --git a/tests/WidgetControllerTest.php b/tests/WidgetControllerTest.php new file mode 100644 index 00000000..d7dac820 --- /dev/null +++ b/tests/WidgetControllerTest.php @@ -0,0 +1,89 @@ +objFromFixture('WidgetControllerTestPage', 'page1'); + $page->publish('Stage', 'Live'); + + $widget = $this->objFromFixture('WidgetControllerTest_Widget', 'widget1'); + + $response = $this->get($page->URLSegment); + + $formAction = sprintf('%s/widget/%d/Form', $page->URLSegment, $widget->ID); + $this->assertContains( + $formAction, + $response->getBody(), + "Widget forms are rendered through WidgetArea templates" + ); + } + + function testWidgetFormSubmission() { + $page = $this->objFromFixture('WidgetControllerTestPage', 'page1'); + $page->publish('Stage', 'Live'); + + $widget = $this->objFromFixture('WidgetControllerTest_Widget', 'widget1'); + + $this->get($page->URLSegment); + $response = $this->submitForm('Form_Form', null, array('TestValue'=>'Updated')); + + $this->assertContains( + 'TestValue: Updated', + $response->getBody(), + "Form values are submitted to correct widget form" + ); + $this->assertContains( + sprintf('Widget ID: %d', $widget->ID), + $response->getBody(), + "Widget form acts on correct widget, as identified in the URL" + ); + } +} + +/** + * @package sapphire + * @subpackage tests + */ +class WidgetControllerTest_Widget extends Widget implements TestOnly { + static $db = array( + 'TestValue' => 'Text' + ); +} + +/** + * @package sapphire + * @subpackage tests + */ +class WidgetControllerTest_Widget_Controller extends Widget_Controller implements TestOnly { + function Form() { + $widgetform = new Form( + $this, + 'Form', + new FieldSet( + new TextField('TestValue') + ), + new FieldSet( + new FormAction('doAction') + ) + ); + + return $widgetform; + } + + function doAction($data, $form) { + return sprintf('TestValue: %s\nWidget ID: %d', + $data['TestValue'], + $this->widget->ID + ); + } +} +?> \ No newline at end of file diff --git a/tests/WidgetControllerTest.yml b/tests/WidgetControllerTest.yml new file mode 100644 index 00000000..a549c230 --- /dev/null +++ b/tests/WidgetControllerTest.yml @@ -0,0 +1,10 @@ +WidgetControllerTest_Widget: + widget1: + Title: Widget 1 +WidgetArea: + area1: + Widgets: =>WidgetControllerTest_Widget.widget1 +WidgetControllerTestPage: + page1: + Title: Page1 + WidgetControllerTestSidebar: =>WidgetArea.area1 \ No newline at end of file diff --git a/tests/WidgetControllerTestPage.php b/tests/WidgetControllerTestPage.php new file mode 100644 index 00000000..8900e6ae --- /dev/null +++ b/tests/WidgetControllerTestPage.php @@ -0,0 +1,27 @@ + 'WidgetArea' + ); +} + +/** + * @package sapphire + * @subpackage tests + */ +class WidgetControllerTestPage_Controller extends Page_Controller implements TestOnly { + + /** + * Template selection doesnt work in test folders, + * so we enforce a template name. + */ + function getViewer($action) { + $templates = array('WidgetControllerTestPage'); + + return new SSViewer($templates); + } +} \ No newline at end of file diff --git a/tests/WidgetControllerTestPage.ss b/tests/WidgetControllerTestPage.ss new file mode 100644 index 00000000..0c65a8d4 --- /dev/null +++ b/tests/WidgetControllerTestPage.ss @@ -0,0 +1 @@ +$WidgetControllerTestSidebar \ No newline at end of file diff --git a/tests/WidgetControllerTest_Widget.ss b/tests/WidgetControllerTest_Widget.ss new file mode 100644 index 00000000..701a1e15 --- /dev/null +++ b/tests/WidgetControllerTest_Widget.ss @@ -0,0 +1 @@ +$Form \ No newline at end of file