diff --git a/core/control/ContentController.php b/core/control/ContentController.php index f143b3791..01a997973 100644 --- a/core/control/ContentController.php +++ b/core/control/ContentController.php @@ -110,6 +110,52 @@ class ContentController extends Controller { } } + + /** + * Handles widgets attached to a page through one or more {@link WidgetArea} elements. + * Iterated through each $has_one relation with a {@link WidgetArea} + * and looks for connected widgets by their database identifier. + * Assumes URLs in the following format: /widget/. + * + * @return RequestHandler + */ + function handleWidget() { + $SQL_id = $this->request->param('ID'); + if(!$SQL_id) return false; + + // find WidgetArea relations + $widgetAreaRelations = array(); + $hasOnes = $this->dataRecord->has_one(); + if(!$hasOnes) return false; + foreach($hasOnes as $hasOneName => $hasOneClass) { + if($hasOneClass == 'WidgetArea' || ClassInfo::is_subclass_of($hasOneClass, 'WidgetArea')) { + $widgetAreaRelations[] = $hasOneName; + } + } + + // find widget + $widget = null; + foreach($widgetAreaRelations as $widgetAreaRelation) { + if($widget) break; + $widget = $this->dataRecord->$widgetAreaRelation()->Widgets( + sprintf('"Widget"."ID" = %d', $SQL_id) + )->First(); + } + if(!$widget) user_error('No widget found', E_USER_ERROR); + + // find controller + $controllerClass = ''; + foreach(array_reverse(ClassInfo::ancestry($widget->class)) as $widgetClass) { + $controllerClass = "{$widgetClass}_Controller"; + if(class_exists($controllerClass)) break; + } + if(!$controllerClass) user_error( + sprintf('No controller available for %s', $widget->class), + E_USER_ERROR + ); + + return new $controllerClass($widget); + } /** * Get the project name diff --git a/templates/WidgetArea.ss b/templates/WidgetArea.ss index cda23ea06..b5279962a 100644 --- a/templates/WidgetArea.ss +++ b/templates/WidgetArea.ss @@ -1,3 +1,3 @@ -<% control Widgets %> +<% control WidgetControllers %> $WidgetHolder <% end_control %> \ No newline at end of file diff --git a/tests/widgets/WidgetControllerTest.php b/tests/widgets/WidgetControllerTest.php new file mode 100644 index 000000000..dfbdd9e3e --- /dev/null +++ b/tests/widgets/WidgetControllerTest.php @@ -0,0 +1,84 @@ +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/widgets/WidgetControllerTest.yml b/tests/widgets/WidgetControllerTest.yml new file mode 100644 index 000000000..a549c2303 --- /dev/null +++ b/tests/widgets/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/widgets/WidgetControllerTestPage.php b/tests/widgets/WidgetControllerTestPage.php new file mode 100644 index 000000000..8900e6aef --- /dev/null +++ b/tests/widgets/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/widgets/WidgetControllerTestPage.ss b/tests/widgets/WidgetControllerTestPage.ss new file mode 100644 index 000000000..0c65a8d44 --- /dev/null +++ b/tests/widgets/WidgetControllerTestPage.ss @@ -0,0 +1 @@ +$WidgetControllerTestSidebar \ No newline at end of file diff --git a/tests/widgets/WidgetControllerTest_Widget.ss b/tests/widgets/WidgetControllerTest_Widget.ss new file mode 100644 index 000000000..701a1e159 --- /dev/null +++ b/tests/widgets/WidgetControllerTest_Widget.ss @@ -0,0 +1 @@ +$Form \ No newline at end of file diff --git a/widgets/Widget.php b/widgets/Widget.php index 0106c0692..f51c77720 100644 --- a/widgets/Widget.php +++ b/widgets/Widget.php @@ -1,7 +1,12 @@ 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($this->class); } @@ -53,6 +74,9 @@ class Widget extends DataObject { return $this->renderWith('WidgetDescription'); } + /** + * @see Widget_Controller->editablesegment() + */ function EditableSegment() { return $this->renderWith('WidgetEditor'); } @@ -93,17 +117,86 @@ class Widget extends DataObject { $this->write(); } - function FormObjectLink($formName) { - if(is_numeric($this->ID)) { - return "WidgetFormProxy/index/$this->ID?executeForm=$formName"; - } else { - user_error("Attempted to create a form on a widget that hasn't been saved to the database.", E_USER_WARNING); - } - } } +/** + * 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; + + 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(); + } + + function Link() { + return Controller::join_links( + Controller::curr()->Link(), + 'widget', + ($this->widget) ? $this->widget->ID : null + ); + } + + /** + * @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($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')) { diff --git a/widgets/WidgetArea.php b/widgets/WidgetArea.php index 19cf572fe..a5a19b580 100644 --- a/widgets/WidgetArea.php +++ b/widgets/WidgetArea.php @@ -18,6 +18,29 @@ class WidgetArea extends DataObject { static $belongs_many_many = array(); + /** + * 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->Widgets() as $widget) { + // find controller + $controllerClass = ''; + foreach(array_reverse(ClassInfo::ancestry($widget->class)) as $widgetClass) { + $controllerClass = "{$widgetClass}_Controller"; + if(class_exists($controllerClass)) break; + } + $controllers->push(new $controllerClass($widget)); + } + + return $controllers; + } + function forTemplate() { return $this->renderWith($this->class); } diff --git a/widgets/WidgetFormProxy.php b/widgets/WidgetFormProxy.php deleted file mode 100644 index 75811e082..000000000 --- a/widgets/WidgetFormProxy.php +++ /dev/null @@ -1,15 +0,0 @@ -urlParams['ID']); - - // Put this in once widget->canView is implemented - //if($widget->canView()) - return $widget; - - } -} \ No newline at end of file