API CHANGE Removed unnecessary WidgetFormProxy class and Widget->FormObjectLink(), broken functionality since the RequestHandler restructuring in 2.3. Use Widget_Controller instead.
FEATURE Added Widget_Controller class to enable nested forms within Wiget class. ENHANCEMENT Changed WidgetArea.ss to iterate over $WidgetControllers instead of $Widgets, to allow forms rendered within to retain their controller context (through Widget_Controller and $failover mechanisms). ENHANCEMENT Added handleWidgets() to ContentController to support new Widget_Controller class git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@85789 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
parent
e9d25ca2ce
commit
2cc0d016f4
|
@ -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: <URLSegment>/widget/<Widget-ID>.
|
||||||
|
*
|
||||||
|
* @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
|
* Get the project name
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
<% control Widgets %>
|
<% control WidgetControllers %>
|
||||||
$WidgetHolder
|
$WidgetHolder
|
||||||
<% end_control %>
|
<% end_control %>
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @package sapphire
|
||||||
|
* @subpackage tests
|
||||||
|
*/
|
||||||
|
class WidgetControllerTest extends FunctionalTest {
|
||||||
|
static $fixture_file = 'sapphire/tests/widgets/WidgetControllerTest.yml';
|
||||||
|
|
||||||
|
function testWidgetFormRendering() {
|
||||||
|
$page = $this->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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
|
@ -0,0 +1,10 @@
|
||||||
|
WidgetControllerTest_Widget:
|
||||||
|
widget1:
|
||||||
|
Title: Widget 1
|
||||||
|
WidgetArea:
|
||||||
|
area1:
|
||||||
|
Widgets: =>WidgetControllerTest_Widget.widget1
|
||||||
|
WidgetControllerTestPage:
|
||||||
|
page1:
|
||||||
|
Title: Page1
|
||||||
|
WidgetControllerTestSidebar: =>WidgetArea.area1
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @package sapphire
|
||||||
|
* @subpackage tests
|
||||||
|
*/
|
||||||
|
class WidgetControllerTestPage extends Page implements TestOnly {
|
||||||
|
static $has_one = array(
|
||||||
|
'WidgetControllerTestSidebar' => '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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
$WidgetControllerTestSidebar
|
|
@ -0,0 +1 @@
|
||||||
|
$Form
|
|
@ -1,7 +1,12 @@
|
||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Base class for widgets.
|
* Widgets let CMS authors drag and drop small pieces of functionality into
|
||||||
* Widgets let CMS authors drag and drop small pieces of functionality into defined areas of their websites.
|
* defined areas of their websites.
|
||||||
|
*
|
||||||
|
* ## Forms
|
||||||
|
* You can use forms in widgets by implementing a {@link Widget_Controller}.
|
||||||
|
* See {@link Widget_Controller} for more information.
|
||||||
|
*
|
||||||
* @package sapphire
|
* @package sapphire
|
||||||
* @subpackage widgets
|
* @subpackage widgets
|
||||||
*/
|
*/
|
||||||
|
@ -29,10 +34,26 @@ class Widget extends DataObject {
|
||||||
return new FieldSet();
|
return new FieldSet();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note: Overloaded in {@link Widget_Controller}.
|
||||||
|
*
|
||||||
|
* @return string HTML
|
||||||
|
*/
|
||||||
function WidgetHolder() {
|
function WidgetHolder() {
|
||||||
return $this->renderWith("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() {
|
function Content() {
|
||||||
return $this->renderWith($this->class);
|
return $this->renderWith($this->class);
|
||||||
}
|
}
|
||||||
|
@ -53,6 +74,9 @@ class Widget extends DataObject {
|
||||||
return $this->renderWith('WidgetDescription');
|
return $this->renderWith('WidgetDescription');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see Widget_Controller->editablesegment()
|
||||||
|
*/
|
||||||
function EditableSegment() {
|
function EditableSegment() {
|
||||||
return $this->renderWith('WidgetEditor');
|
return $this->renderWith('WidgetEditor');
|
||||||
}
|
}
|
||||||
|
@ -93,17 +117,86 @@ class Widget extends DataObject {
|
||||||
$this->write();
|
$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 {
|
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() {
|
function editablesegment() {
|
||||||
$className = $this->urlParams['ID'];
|
$className = $this->urlParams['ID'];
|
||||||
if(class_exists($className) && is_subclass_of($className, 'Widget')) {
|
if(class_exists($className) && is_subclass_of($className, 'Widget')) {
|
||||||
|
|
|
@ -18,6 +18,29 @@ class WidgetArea extends DataObject {
|
||||||
|
|
||||||
static $belongs_many_many = array();
|
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() {
|
function forTemplate() {
|
||||||
return $this->renderWith($this->class);
|
return $this->renderWith($this->class);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package sapphire
|
|
||||||
* @subpackage widgets
|
|
||||||
*/
|
|
||||||
class WidgetFormProxy extends Controller {
|
|
||||||
function getFormOwner() {
|
|
||||||
$widget = DataObject::get_by_id("Widget", $this->urlParams['ID']);
|
|
||||||
|
|
||||||
// Put this in once widget->canView is implemented
|
|
||||||
//if($widget->canView())
|
|
||||||
return $widget;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue