From 552a5006daea58a66a87281f7e0435012a38539a Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Wed, 18 Apr 2012 23:15:45 +0200 Subject: [PATCH] MINOR Initial commit --- LICENSE | 24 ++ README.md | 329 +++++++++++++++++++ _config.php | 0 code/form/WidgetAreaEditor.php | 142 ++++++++ code/model/Widget.php | 232 +++++++++++++ code/model/WidgetArea.php | 72 ++++ templates/WidgetArea.ss | 3 + templates/WidgetAreaEditor.ss | 32 ++ templates/WidgetDescription.ss | 6 + templates/WidgetEditor.ss | 12 + templates/WidgetHolder.ss | 4 + tests/WidgetAreaEditorTest.php | 471 +++++++++++++++++++++++++++ tests/WidgetControllerTest.php | 88 +++++ tests/WidgetControllerTest.yml | 10 + tests/WidgetControllerTestPage.php | 27 ++ tests/WidgetControllerTestPage.ss | 1 + tests/WidgetControllerTest_Widget.ss | 1 + 17 files changed, 1454 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 _config.php create mode 100644 code/form/WidgetAreaEditor.php create mode 100644 code/model/Widget.php create mode 100644 code/model/WidgetArea.php create mode 100644 templates/WidgetArea.ss create mode 100644 templates/WidgetAreaEditor.ss create mode 100644 templates/WidgetDescription.ss create mode 100644 templates/WidgetEditor.ss create mode 100644 templates/WidgetHolder.ss create mode 100644 tests/WidgetAreaEditorTest.php 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/LICENSE b/LICENSE new file mode 100644 index 0000000..57109f4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +* Copyright (c) 2011, SilverStripe Ltd +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* * Redistributions of source code must retain the above copyright +* notice, this list of conditions and the following disclaimer. +* * Redistributions in binary form must reproduce the above copyright +* notice, this list of conditions and the following disclaimer in the +* documentation and/or other materials provided with the distribution. +* * Neither the name of the nor the +* names of its contributors may be used to endorse or promote products +* derived from this software without specific prior written permission. +* +* THIS SOFTWARE IS PROVIDED BY SilverStripe Ltd. ``AS IS'' AND ANY +* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL Silverstripe Ltd. BE LIABLE FOR ANY +* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ea1d442 --- /dev/null +++ b/README.md @@ -0,0 +1,329 @@ +# Widgets Module + +## Introduction + +[Widgets](http://silverstripe.org/widgets) are small pieces of functionality such as showing the latest Comments or Flickr Photos. They normally display on +the sidebar of your website. To check out a what a [Widget](http://silverstripe.org/widgets) can do watch the +[Widget video](http://silverstripe.com/assets/screencasts/SilverStripe-Blog-DragDrop-Widgets.swf) and try out the +[demo site](http://demo.silverstripe.org/) + +## Requirements + + * SilverStripe 3.0 + +## How to Use A Widget + +### Downloading and Contributing Widgets + +* To download widgets visit [Widgets section](http://silverstripe.org/widgets) +* Upload widgets you want to share to +[http://silverstripe.org/widgets/manage/add](http://silverstripe.org/widgets/manage/add). Make sure you read the +packaging instructions at the bottom of the page about how to make your widget package. + + +### Installing a widget + +By following the "Packaging" rules below, widgets are easily installed. + +* Install the [blog module](http://www.silverstripe.org/blog-module/) (by default only the Blog has widgets enabled) +* Download the file and unzip to the main folder of your SilverStripe website, e.g. to `/widget_/`. The folder +will contain a few files, which generally won't need editing or reading. +* Run `http://my-website.com/dev/build` +* Login to the CMS and go to the 'Blog' page. Choose the "widgets" tab and drag n drop the new widget to activate it. +* Your blog will now have the widget shown + +### Adding widgets to other pages + +You have to do a couple things to get a Widget to work on a page. + +First step is to add an WidgetArea to the Database to store the widget details. Then you have to edit the CMS to add a +Widget Form to manage the widgets. An example of this is below + +**mysite/code/Page.php** + + class Page extends SiteTree { + + ... + static $has_one = array( + "Sidebar" => "WidgetArea", + ); + + public function getCMSFields() { + $fields = parent::getCMSFields(); + $fields->addFieldToTab("Root.Content.Widgets", new WidgetAreaEditor("Sidebar")); + return $fields; + } + .... + } + + +Then in your Template you need to call $SideBar wherever you want to render the widget + +For example: using the blackcandy theme I put this piece of code above the closing `` + +**themes/blackcandy/templates/Includes/Sidebar.ss** + + $Sidebar + + +## Writing your own widgets + +To create a Widget you need at least three files - a php file containing the class, a template file of the same name and +a config file called *_config.php* (if you dont need any config options for the widget to work then you can make it +blank). Each widget should be in its own folder like widgets_widgetName/ + +After installing or creating a new widget, **make sure to run db/build?flush=1** at the end of the URL, *before* +attempting to use it. + +The class should extend the Widget class, and must specify three static variables - $title, the title that will appear +in the rendered widget (eg Photos), $cmsTitle, a more descriptive title that will appear in the cms editor (eg Flickr +Photos), and $description, a short description that will appear in the cms editor (eg This widget shows photos from +Flickr). The class may also specify functions to be used in the template like a page type can. + +If a Widget has configurable options, then it can specify a number of database fields to store these options in via the +static $db array, and also specify a getCMSFields function that returns a !FieldList, much the same way as a page type +does. + +An example widget is below: + +**FlickrWidget.php** + + "Varchar", + "Photoset" => "Varchar", + "Tags" => "Varchar", + "NumberToShow" => "Int" + ); + + + static $defaults = array( + "NumberToShow" => 8 + ); + + + static $title = "Photos"; + static $cmsTitle = "Flickr Photos"; + static $description = "Shows flickr photos."; + + public function Photos() { + Requirements::javascript(THIRDPARTY_DIR . "/prototype/prototype.js"); + Requirements::javascript(THIRDPARTY_DIR . "/scriptaculous/effects.js"); + Requirements::javascript("mashups/javascript/lightbox.js"); + Requirements::css("mashups/css/lightbox.css"); + + $flickr = new FlickrService(); + if($this->Photoset == "") { + $photos = $flickr->getPhotos($this->Tags, $this->User, $this->NumberToShow, 1); + } else { + $photos = $flickr->getPhotoSet($this->Photoset, $this->User, $this->NumberToShow, 1); + } + + $output = new DataObjectSet(); + foreach($photos->PhotoItems as $photo) { + $output->push(new ArrayData(array( + "Title" => $photo->title, + "Link" => "http://farm1.static.flickr.com/" . $photo->image_path .".jpg", + "Image" => "http://farm1.static.flickr.com/" .$photo->image_path. "_s.jpg" + ))); + } + + return $output; + } + + public function getCMSFields() { + return new FieldList( + new TextField("User", "User"), + new TextField("PhotoSet", "Photo Set"), + new TextField("Tags", "Tags"), + new NumericField("NumberToShow", "Number to Show") + ); + } + } + + ?> + + +**FlickrWidget.ss** + + <% control Photos %> + $Title + <% end_control %> + + +## Extending and Customizing + +### Rendering a $Widget Individually + +To call a single Widget in a page - without adding a widget area in the CMS for you to add / delete the widgets, you can +define a merge variable in the Page Controller and include it in the Page Template. + +This example creates an RSSWidget with the SilverStripe blog feed. + + RssUrl = "http://feeds.feedburner.com/silverstripe-blog"; + return $widget->renderWith("WidgetHolder"); + } + ?> + + +To render the widget, simply include $SilverStripeFeed in your template: + + $SilverStripeFeed + + +As directed in the definition of SilverStripeFeed(), the Widget will be rendered through the WidgetHolder template. This +is pre-defined at `framework/templates/WidgetHolder.ss` and simply consists of: + +
+

$Title

+ $Content +
+ + +You can override the WidgetHolder.ss and Widget.ss templates in your theme too by adding WidgetHolder and Widget +templates to `themes/myThemeName/templates/Includes/` + +### Changing the title of your widget + +To change the title of your widget, you need to override the Title() method. By default, this simply returns the $title +variable. For example, to set your widgets title to 'Hello World!', you could use: + +**widgets_yourWidget/YourWidgetWidget.php** + + public function Title() { + return "Hello World!"; + } + + +but, you can do exactly the same by setting your $title variable. + +A more common reason for overriding Title() is to allow the title to be set in the CMS. Say you had a text field in your +widget called WidgetTitle, that you wish to use as your title. If nothing is set, then you'll use your default title. +This is similar to the RSS Widget in the blog module. + + public function Title() { + return $this->WidgetTitle ? $this->WidgetTitle : self::$title; + } + + +This returns the value inputted in the CMS, if it's set or what is in the $title variable if it isn't. + +### Forms within Widgets + +To implement a form inside a widget, you need to implement a custom controller for your widget to return this form. Make +sure that your controller follows the usual naming conventions, and it will be automatically picked up by the +`WidgetArea` rendering in your *Page.ss* template. + +**mysite/code/MyWidget.php** + + class MyWidget extends Widget { + static $db = array( + 'TestValue' => 'Text' + ); + } + + class MyWidget_Controller extends Widget_Controller { + public function MyFormName() { + return new Form( + $this, + 'MyFormName', + new FieldList( + new TextField('TestValue') + ), + new FieldList( + new FormAction('doAction') + ) + ); + } + + public function doAction($data, $form) { + // $this->widget points to the widget + } + } + + +To output this form, modify your widget template. + +**mysite/templates/MyWidget.ss** + + $Content + $MyFormName + +**Note:** The necessary controller actions are only present in subclasses of `Page_Controller`. To use +widget forms in other controller subclasses, have a look at *ContentController->handleWidget()* and +*ContentController::$url_handlers*. + +## But what if I have widgets on my blog currently?? + +If you currently have a blog installed, the widget fields are going to double up on those pages (as the blog extends the +Page class). One way to fix this is to comment out line 30 in BlogHolder.php and remove the DB entry by running a +`http://www.mysite.com/db/build`. + +**blog/code/BlogHolder.php** + + "WidgetArea", COMMENT OUT + 'Newsletter' => 'NewsletterType' + ....... + public function getCMSFields() { + $fields = parent::getCMSFields(); + $fields->removeFieldFromTab("Root.Content","Content"); + // $fields->addFieldToTab("Root.Content.Widgets", new WidgetAreaEditor("SideBar")); COMMENT OUT + + ........ + + +Then you can use the Widget area you defined on Page.php + +## Releasing Your Widget + +### Packaging + +For a widget to be put in our official widget database they must follow this convention - If the name of your widget was +"YourName" then: + +#### File Structure for your widget + +You should have a folder called widget_YourName in the top level (the one with framework, cms..) with all your files. See +the example below. Your widget **MUST** have at least 1 Template file, 1 PHP file, the README File +[(Example)](http://open.silverstripe.com/browser/modules/widgets/twitter/trunk/README)and an _config.php file for +configuration. If you dont need any config options for the widget to work then you still need an _config.php by you can +make it blank + +The decision over whether to configure a widget in _config.php or in the CMS is important: + +* If the setting is the kind of thing that a website author, familiar with common business apps such as Word and +Outlook, would understand - then make it configurable in the CMS. +* If the setting is the kind of thing that the person setting up the website - doing the design and/or development - +would understand, then make it configurable in the _config.php file. + +This way, the CMS remains an application designed for content authors, and not developers. + +*widget_name/_config.php* + + + + +**Example Widget Structure** + +![](_images/widget_demo.gif) + + +#### How to make the Package + +* Make a tar.gz file called widgets_YourName-0.1.tar.gz (where 0.1 is the version number). + * Ensure when you "unzip" the compressed file it has everything the "widgets_YourName" folder with everything inside +it. +* If made official, it will be given these locations at silverstripe.com: + * SVN location: http://svn.silverstripe.com/open/modules/widgets/flickr/trunk + * Official download: http://www.silverstripe.com/assets/downloads/widgets/widgets_flickr-0.1.1.tar.gz diff --git a/_config.php b/_config.php new file mode 100644 index 0000000..e69de29 diff --git a/code/form/WidgetAreaEditor.php b/code/form/WidgetAreaEditor.php new file mode 100644 index 0000000..532c688 --- /dev/null +++ b/code/form/WidgetAreaEditor.php @@ -0,0 +1,142 @@ +MaxWidgets = $maxWidgets; + $this->widgetClasses = $widgetClasses; + + parent::__construct($name); + } + + function FieldHolder($properties = array()) { + Requirements::css('widgets/css/WidgetAreaEditor.css'); + Requirements::javascript(THIRDPARTY_DIR . "/prototype/prototype.js"); + Requirements::javascript(THIRDPARTY_DIR . '/behaviour/behaviour.js'); + Requirements::javascript('widgets/javascript/WidgetAreaEditor.js'); + + return $this->renderWith("WidgetAreaEditor"); + } + + function AvailableWidgets() { + + $widgets= new ArrayList(); + + foreach($this->widgetClasses as $widgetClass) { + $classes = ClassInfo::subclassesFor($widgetClass); + array_shift($classes); + foreach($classes as $class) { + $widgets->push(singleton($class)); + } + } + + return $widgets; + } + + function UsedWidgets() { + // Call class_exists() to load Widget.php earlier and avoid a segfault + class_exists('Widget'); + + $relationName = $this->name; + $widgets = $this->form->getRecord()->getComponent($relationName)->Items(); + return $widgets; + } + + function IdxField() { + return $this->id() . 'ID'; + } + + function Value() { + $relationName = $this->name; + return $this->form->getRecord()->getComponent($relationName)->ID; + } + + function saveInto(DataObjectInterface $record) { + $name = $this->name; + $idName = $name . "ID"; + + $widgetarea = $record->getComponent($name); + $widgetarea->write(); + + $record->$idName = $widgetarea->ID; + + $widgets = $widgetarea->Items(); + + // store the field IDs and delete the missing fields + // alternatively, we could delete all the fields and re add them + $missingWidgets = array(); + + if($widgets) { + foreach($widgets as $existingWidget) { + $missingWidgets[$existingWidget->ID] = $existingWidget; + } + } + + if(isset($_REQUEST['Widget'])) { + foreach(array_keys($_REQUEST['Widget']) as $widgetAreaName) { + if ($widgetAreaName !== $this->name) { + continue; + } + + foreach(array_keys($_REQUEST['Widget'][$widgetAreaName]) as $newWidgetID) { + $newWidgetData = $_REQUEST['Widget'][$widgetAreaName][$newWidgetID]; + + // Sometimes the id is "new-1" or similar, ensure this doesn't get into the query + if(!is_numeric($newWidgetID)) { + $newWidgetID = 0; + } + + // \"ParentID\" = '0' is for the new page + $widget = DataObject::get_one( + 'Widget', + "(\"ParentID\" = '{$record->$name()->ID}' OR \"ParentID\" = '0') AND \"Widget\".\"ID\" = '$newWidgetID'" + ); + + + // check if we are updating an existing widget + if($widget && isset($missingWidgets[$widget->ID])) { + unset($missingWidgets[$widget->ID]); + } + + // create a new object + if(!$widget && !empty($newWidgetData['Type']) && class_exists($newWidgetData['Type'])) { + $widget = new $newWidgetData['Type'](); + $widget->ID = 0; + $widget->ParentID = $record->$name()->ID; + + if(!is_subclass_of($widget, 'Widget')) { + $widget = null; + } + } + + if($widget) { + if($widget->ParentID == 0) { + $widget->ParentID = $record->$name()->ID; + } + // echo "Saving $widget->ID into $name/$widget->ParentID\n
"; + $widget->populateFromPostData($newWidgetData); + } + } + } + } + + // remove the fields not saved + if($missingWidgets) { + foreach($missingWidgets as $removedWidget) { + if(isset($removedWidget) && is_numeric($removedWidget->ID)) { + $removedWidget->delete(); + } + } + } + } +} diff --git a/code/model/Widget.php b/code/model/Widget.php new file mode 100644 index 0000000..8ef5b7a --- /dev/null +++ b/code/model/Widget.php @@ -0,0 +1,232 @@ + "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 FieldList(); + $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 = preg_replace("/name=\"([A-Za-z0-9\-_]+)\"/", "name=\"Widget[" . $this->ID . "][\\1]\"", $renderedField); + $renderedField = preg_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 SilverStripe 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 cms + * @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 cms + * @subpackage widgets + */ +class Widget_TreeDropdownField extends TreeDropdownField { + function FieldHolder($properties = array()) {} + function Field($properties = array()) {} +} + diff --git a/code/model/WidgetArea.php b/code/model/WidgetArea.php new file mode 100644 index 0000000..a3f1d9e --- /dev/null +++ b/code/model/WidgetArea.php @@ -0,0 +1,72 @@ + "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 SS_List Collection of {@link Widget_Controller} + */ + function WidgetControllers() { + $controllers = new ArrayList(); + + 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(); + } + } +} + diff --git a/templates/WidgetArea.ss b/templates/WidgetArea.ss new file mode 100644 index 0000000..b527996 --- /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/WidgetAreaEditor.ss b/templates/WidgetAreaEditor.ss new file mode 100644 index 0000000..6e3854e --- /dev/null +++ b/templates/WidgetAreaEditor.ss @@ -0,0 +1,32 @@ +
maxwidgets="$MaxWidgets"<% end_if %>> + +
+

<% _t('AVAILABLE', 'Available Widgets') %>

+

<% _t('AVAILWIDGETS', 'Click a widget title below to use it on this page.') %>

+
+ <% if AvailableWidgets %> + <% control AvailableWidgets %> + $DescriptionSegment + <% end_control %> + <% else %> +
+

<% _t('NOAVAIL', 'There are currently no widgets available.') %>

+
+ <% end_if %> +
+
+
+

<% _t('INUSE', 'Widgets currently used') %>

+

<% _t('TOSORT', 'To sort currently used widgets on this page, drag them up and down.') %>

+ +
+ <% if UsedWidgets %> + <% control UsedWidgets %> + $EditableSegment + <% end_control %> + <% else %> +
+ <% end_if %> +
+
+
\ No newline at end of file diff --git a/templates/WidgetDescription.ss b/templates/WidgetDescription.ss new file mode 100644 index 0000000..423f990 --- /dev/null +++ b/templates/WidgetDescription.ss @@ -0,0 +1,6 @@ +
+

$CMSTitle

+
+

$Description

+
+
\ No newline at end of file diff --git a/templates/WidgetEditor.ss b/templates/WidgetEditor.ss new file mode 100644 index 0000000..0075b76 --- /dev/null +++ b/templates/WidgetEditor.ss @@ -0,0 +1,12 @@ +
+

$CMSTitle

+
+

$Description

+
+
+ $CMSEditor + + +
+

<% _t('DELETE', 'Delete') %>

+
\ No newline at end of file diff --git a/templates/WidgetHolder.ss b/templates/WidgetHolder.ss new file mode 100644 index 0000000..e6787fd --- /dev/null +++ b/templates/WidgetHolder.ss @@ -0,0 +1,4 @@ +
+ <% if Title %>

$Title

<% end_if %> + $Content +
diff --git a/tests/WidgetAreaEditorTest.php b/tests/WidgetAreaEditorTest.php new file mode 100644 index 0000000..5a32536 --- /dev/null +++ b/tests/WidgetAreaEditorTest.php @@ -0,0 +1,471 @@ + array( + 'BottomBar' => array( + 'new-1' => array( + 'Title' => 'MyTestWidget', + 'Type' => $this->widgetToTest, + 'Sort' => 0 + ) + ) + ) + ); + + $editorSide = new WidgetAreaEditor('SideBar'); + $editorBott = new WidgetAreaEditor('BottomBar'); + $page = new WidgetAreaEditorTest_FakePage(); + + $editorSide->saveInto($page); + $editorBott->saveInto($page); + $page->write(); + $page->flushCache(); + $page->BottomBar()->flushCache(); + $page->SideBar()->flushCache(); + + $this->assertEquals($page->BottomBar()->Widgets()->Count(), 1); + $this->assertEquals($page->SideBar()->Widgets()->Count(), 0); + + $_REQUEST = $oldRequest; + } + + function testFillingTwoAreas() { + $oldRequest = $_REQUEST; + + $_REQUEST = array( + 'Widget' => array( + 'SideBar' => array( + 'new-1' => array( + 'Title' => 'MyTestWidgetSide', + 'Type' => $this->widgetToTest, + 'Sort' => 0 + ) + ), + 'BottomBar' => array( + 'new-1' => array( + 'Title' => 'MyTestWidgetBottom', + 'Type' => $this->widgetToTest, + 'Sort' => 0 + ) + ) + ) + ); + + $editorSide = new WidgetAreaEditor('SideBar'); + $editorBott = new WidgetAreaEditor('BottomBar'); + $page = new WidgetAreaEditorTest_FakePage(); + + $editorSide->saveInto($page); + $editorBott->saveInto($page); + $page->write(); + $page->flushCache(); + $page->BottomBar()->flushCache(); + $page->SideBar()->flushCache(); + + // Make sure they both got saved + $this->assertEquals($page->BottomBar()->Widgets()->Count(), 1); + $this->assertEquals($page->SideBar()->Widgets()->Count(), 1); + + $sideWidgets = $page->SideBar()->Widgets()->toArray(); + $bottWidgets = $page->BottomBar()->Widgets()->toArray(); + $this->assertEquals($sideWidgets[0]->Title(), 'MyTestWidgetSide'); + $this->assertEquals($bottWidgets[0]->Title(), 'MyTestWidgetBottom'); + + $_REQUEST = $oldRequest; + } + + function testDeletingOneWidgetFromOneArea() { + $oldRequest = $_REQUEST; + + // First get some widgets in there + $_REQUEST = array( + 'Widget' => array( + 'SideBar' => array( + 'new-1' => array( + 'Title' => 'MyTestWidgetSide', + 'Type' => $this->widgetToTest, + 'Sort' => 0 + ) + ), + 'BottomBar' => array( + 'new-1' => array( + 'Title' => 'MyTestWidgetBottom', + 'Type' => $this->widgetToTest, + 'Sort' => 0 + ) + ) + ) + ); + + $editorSide = new WidgetAreaEditor('SideBar'); + $editorBott = new WidgetAreaEditor('BottomBar'); + $page = new WidgetAreaEditorTest_FakePage(); + + $editorSide->saveInto($page); + $editorBott->saveInto($page); + $page->write(); + $page->flushCache(); + $page->BottomBar()->flushCache(); + $page->SideBar()->flushCache(); + $sideWidgets = $page->SideBar()->Widgets()->toArray(); + $bottWidgets = $page->BottomBar()->Widgets()->toArray(); + + // Save again (after removing the SideBar's widget) + $_REQUEST = array( + 'Widget' => array( + 'SideBar' => array( + ), + 'BottomBar' => array( + $bottWidgets[0]->ID => array( + 'Title' => 'MyTestWidgetBottom', + 'Type' => $this->widgetToTest, + 'Sort' => 0 + ) + ) + ) + ); + + $editorSide->saveInto($page); + $editorBott->saveInto($page); + + $page->write(); + $page->flushCache(); + $page->BottomBar()->flushCache(); + $page->SideBar()->flushCache(); + $sideWidgets = $page->SideBar()->Widgets()->toArray(); + $bottWidgets = $page->BottomBar()->Widgets()->toArray(); + + $this->assertEquals($page->BottomBar()->Widgets()->Count(), 1); + $this->assertEquals($bottWidgets[0]->Title(), 'MyTestWidgetBottom'); + $this->assertEquals($page->SideBar()->Widgets()->Count(), 0); + + $_REQUEST = $oldRequest; + } + + function testDeletingAWidgetFromEachArea() { + $oldRequest = $_REQUEST; + + // First get some widgets in there + $_REQUEST = array( + 'Widget' => array( + 'SideBar' => array( + 'new-1' => array( + 'Title' => 'MyTestWidgetSide', + 'Type' => $this->widgetToTest, + 'Sort' => 0 + ) + ), + 'BottomBar' => array( + 'new-1' => array( + 'Title' => 'MyTestWidgetBottom', + 'Type' => $this->widgetToTest, + 'Sort' => 0 + ) + ) + ) + ); + + $editorSide = new WidgetAreaEditor('SideBar'); + $editorBott = new WidgetAreaEditor('BottomBar'); + $page = new WidgetAreaEditorTest_FakePage(); + + $editorSide->saveInto($page); + $editorBott->saveInto($page); + $page->write(); + $page->flushCache(); + $page->BottomBar()->flushCache(); + $page->SideBar()->flushCache(); + $sideWidgets = $page->SideBar()->Widgets()->toArray(); + $bottWidgets = $page->BottomBar()->Widgets()->toArray(); + + // Save again (after removing the SideBar's widget) + $_REQUEST = array( + 'Widget' => array( + 'SideBar' => array( + ), + 'BottomBar' => array( + ) + ) + ); + + $editorSide->saveInto($page); + $editorBott->saveInto($page); + + $page->write(); + $page->flushCache(); + $page->BottomBar()->flushCache(); + $page->SideBar()->flushCache(); + $sideWidgets = $page->SideBar()->Widgets()->toArray(); + $bottWidgets = $page->BottomBar()->Widgets()->toArray(); + + $this->assertEquals($page->BottomBar()->Widgets()->Count(), 0); + $this->assertEquals($page->SideBar()->Widgets()->Count(), 0); + + $_REQUEST = $oldRequest; + } + + function testEditingOneWidget() { + $oldRequest = $_REQUEST; + + // First get some widgets in there + $_REQUEST = array( + 'Widget' => array( + 'SideBar' => array( + 'new-1' => array( + 'Title' => 'MyTestWidgetSide', + 'Type' => $this->widgetToTest, + 'Sort' => 0 + ) + ), + 'BottomBar' => array( + 'new-1' => array( + 'Title' => 'MyTestWidgetBottom', + 'Type' => $this->widgetToTest, + 'Sort' => 0 + ) + ) + ) + ); + + $editorSide = new WidgetAreaEditor('SideBar'); + $editorBott = new WidgetAreaEditor('BottomBar'); + $page = new WidgetAreaEditorTest_FakePage(); + + $editorSide->saveInto($page); + $editorBott->saveInto($page); + $page->write(); + $page->flushCache(); + $page->BottomBar()->flushCache(); + $page->SideBar()->flushCache(); + $sideWidgets = $page->SideBar()->Widgets()->toArray(); + $bottWidgets = $page->BottomBar()->Widgets()->toArray(); + + // Save again (after removing the SideBar's widget) + $_REQUEST = array( + 'Widget' => array( + 'SideBar' => array( + $sideWidgets[0]->ID => array( + 'Title' => 'MyTestWidgetSide-edited', + 'Type' => $this->widgetToTest, + 'Sort' => 0 + ) + ), + 'BottomBar' => array( + $bottWidgets[0]->ID => array( + 'Title' => 'MyTestWidgetBottom', + 'Type' => $this->widgetToTest, + 'Sort' => 0 + ) + ) + ) + ); + + + $editorSide->saveInto($page); + $editorBott->saveInto($page); + + $page->write(); + $page->flushCache(); + $page->BottomBar()->flushCache(); + $page->SideBar()->flushCache(); + $sideWidgets = $page->SideBar()->Widgets()->toArray(); + $bottWidgets = $page->BottomBar()->Widgets()->toArray(); + + $this->assertEquals($page->BottomBar()->Widgets()->Count(), 1); + $this->assertEquals($page->SideBar()->Widgets()->Count(), 1); + $this->assertEquals($bottWidgets[0]->Title(), 'MyTestWidgetBottom'); + $this->assertEquals($sideWidgets[0]->Title(), 'MyTestWidgetSide-edited'); + + + $_REQUEST = $oldRequest; + } + + function testEditingAWidgetFromEachArea() { + $oldRequest = $_REQUEST; + + // First get some widgets in there + $_REQUEST = array( + 'Widget' => array( + 'SideBar' => array( + 'new-1' => array( + 'Title' => 'MyTestWidgetSide', + 'Type' => $this->widgetToTest, + 'Sort' => 0 + ) + ), + 'BottomBar' => array( + 'new-1' => array( + 'Title' => 'MyTestWidgetBottom', + 'Type' => $this->widgetToTest, + 'Sort' => 0 + ) + ) + ) + ); + + $editorSide = new WidgetAreaEditor('SideBar'); + $editorBott = new WidgetAreaEditor('BottomBar'); + $page = new WidgetAreaEditorTest_FakePage(); + + $editorSide->saveInto($page); + $editorBott->saveInto($page); + $page->write(); + $page->flushCache(); + $page->BottomBar()->flushCache(); + $page->SideBar()->flushCache(); + $sideWidgets = $page->SideBar()->Widgets()->toArray(); + $bottWidgets = $page->BottomBar()->Widgets()->toArray(); + + // Save again (after removing the SideBar's widget) + $_REQUEST = array( + 'Widget' => array( + 'SideBar' => array( + $sideWidgets[0]->ID => array( + 'Title' => 'MyTestWidgetSide-edited', + 'Type' => $this->widgetToTest, + 'Sort' => 0 + ) + ), + 'BottomBar' => array( + $bottWidgets[0]->ID => array( + 'Title' => 'MyTestWidgetBottom-edited', + 'Type' => $this->widgetToTest, + 'Sort' => 0 + ) + ) + ) + ); + + + $editorSide->saveInto($page); + $editorBott->saveInto($page); + + $page->write(); + $page->flushCache(); + $page->BottomBar()->flushCache(); + $page->SideBar()->flushCache(); + $sideWidgets = $page->SideBar()->Widgets()->toArray(); + $bottWidgets = $page->BottomBar()->Widgets()->toArray(); + + $this->assertEquals($page->BottomBar()->Widgets()->Count(), 1); + $this->assertEquals($page->SideBar()->Widgets()->Count(), 1); + $this->assertEquals($bottWidgets[0]->Title(), 'MyTestWidgetBottom-edited'); + $this->assertEquals($sideWidgets[0]->Title(), 'MyTestWidgetSide-edited'); + + + $_REQUEST = $oldRequest; + } + + function testEditAWidgetFromOneAreaAndDeleteAWidgetFromAnotherArea() { + $oldRequest = $_REQUEST; + + // First get some widgets in there + $_REQUEST = array( + 'Widget' => array( + 'SideBar' => array( + 'new-1' => array( + 'Title' => 'MyTestWidgetSide', + 'Type' => $this->widgetToTest, + 'Sort' => 0 + ) + ), + 'BottomBar' => array( + 'new-1' => array( + 'Title' => 'MyTestWidgetBottom', + 'Type' => $this->widgetToTest, + 'Sort' => 0 + ) + ) + ) + ); + + $editorSide = new WidgetAreaEditor('SideBar'); + $editorBott = new WidgetAreaEditor('BottomBar'); + $page = new WidgetAreaEditorTest_FakePage(); + + $editorSide->saveInto($page); + $editorBott->saveInto($page); + $page->write(); + $page->flushCache(); + $page->BottomBar()->flushCache(); + $page->SideBar()->flushCache(); + $sideWidgets = $page->SideBar()->Widgets()->toArray(); + $bottWidgets = $page->BottomBar()->Widgets()->toArray(); + + // Save again (after removing the SideBar's widget) + $_REQUEST = array( + 'Widget' => array( + 'SideBar' => array( + $sideWidgets[0]->ID => array( + 'Title' => 'MyTestWidgetSide-edited', + 'Type' => $this->widgetToTest, + 'Sort' => 0 + ) + ), + 'BottomBar' => array( + ) + ) + ); + + + $editorSide->saveInto($page); + $editorBott->saveInto($page); + + $page->write(); + $page->flushCache(); + $page->BottomBar()->flushCache(); + $page->SideBar()->flushCache(); + $sideWidgets = $page->SideBar()->Widgets()->toArray(); + $bottWidgets = $page->BottomBar()->Widgets()->toArray(); + + $this->assertEquals($page->BottomBar()->Widgets()->Count(), 0); + $this->assertEquals($page->SideBar()->Widgets()->Count(), 1); + $this->assertEquals($sideWidgets[0]->Title(), 'MyTestWidgetSide-edited'); + + + $_REQUEST = $oldRequest; + } +} + +class WidgetAreaEditorTest_FakePage extends Page implements TestOnly { + public static $has_one = array( + "SideBar" => "WidgetArea", + "BottomBar" => "WidgetArea", + ); +} + +class WidgetAreaEditorTest_TestWidget extends Widget implements TestOnly { + static $cmsTitle = "Test widget"; + static $title = "Test widget"; + static $description = "Test widget"; + static $db = array( + 'Title' => 'Varchar' + ); + public function getCMSFields() { + $fields = new FieldList(); + $fields->push(new TextField('Title')); + return $fields; + } + function Title() { + return $this->Title ? $this->Title : self::$title; + } +} diff --git a/tests/WidgetControllerTest.php b/tests/WidgetControllerTest.php new file mode 100644 index 0000000..a254cb9 --- /dev/null +++ b/tests/WidgetControllerTest.php @@ -0,0 +1,88 @@ +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 cms + * @subpackage tests + */ +class WidgetControllerTest_Widget extends Widget implements TestOnly { + static $db = array( + 'TestValue' => 'Text' + ); +} + +/** + * @package cms + * @subpackage tests + */ +class WidgetControllerTest_Widget_Controller extends Widget_Controller implements TestOnly { + function Form() { + $widgetform = new Form( + $this, + 'Form', + new FieldList( + new TextField('TestValue') + ), + new FieldList( + new FormAction('doAction') + ) + ); + + return $widgetform; + } + + function doAction($data, $form) { + return sprintf('TestValue: %s\nWidget ID: %d', + $data['TestValue'], + $this->widget->ID + ); + } +} diff --git a/tests/WidgetControllerTest.yml b/tests/WidgetControllerTest.yml new file mode 100644 index 0000000..a549c23 --- /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 0000000..f8821ee --- /dev/null +++ b/tests/WidgetControllerTestPage.php @@ -0,0 +1,27 @@ + 'WidgetArea' + ); +} + +/** + * @package cms + * @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); + } +} diff --git a/tests/WidgetControllerTestPage.ss b/tests/WidgetControllerTestPage.ss new file mode 100644 index 0000000..0c65a8d --- /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 0000000..701a1e1 --- /dev/null +++ b/tests/WidgetControllerTest_Widget.ss @@ -0,0 +1 @@ +$Form \ No newline at end of file