Merge remote-tracking branch 'origin/master' into translation-staging

This commit is contained in:
Ingo Schommer 2012-07-18 15:04:05 +02:00
commit 30ceee4e5d
199 changed files with 3890 additions and 3238 deletions

View File

@ -358,6 +358,17 @@ class LeftAndMain extends Controller implements PermissionProvider {
if($this->request->getHeader('X-Pjax') && !$this->response->getHeader('X-Pjax')) {
$this->response->addHeader('X-Pjax', $this->request->getHeader('X-Pjax'));
}
$oldResponse = $this->response;
$newResponse = new LeftAndMain_HTTPResponse(
$oldResponse->getBody(),
$oldResponse->getStatusCode(),
$oldResponse->getStatusDescription()
);
foreach($oldResponse->getHeaders() as $k => $v) {
$newResponse->addHeader($k, $v);
}
$newResponse->setIsFinished(true);
$this->response = $newResponse;
return ''; // Actual response will be re-requested by client
} else {
parent::redirect($url, $code);
@ -579,7 +590,8 @@ class LeftAndMain extends Controller implements PermissionProvider {
* @return ArrayList
*/
public function Breadcrumbs($unlinked = false) {
$title = self::menu_title_for_class($this->class);
$defaultTitle = LeftAndMain::menu_title_for_class($this->class);
$title = _t("{$this->class}.MENUTITLE", $defaultTitle);
$items = new ArrayList(array(
new ArrayData(array(
'Title' => $title,
@ -1471,3 +1483,19 @@ class LeftAndMainMarkingFilter {
}
}
/**
* Allow overriding finished state for faux redirects.
*/
class LeftAndMain_HTTPResponse extends SS_HTTPResponse {
protected $isFinished = false;
function isFinished() {
return (parent::isFinished() || $this->isFinished);
}
function setIsFinished($bool) {
$this->isFinished = $bool;
}
}

View File

@ -7,7 +7,7 @@
abstract class LeftAndMainDecorator extends LeftAndMainExtension {
public function __construct() {
Deprecation::notice('3.0', 'Use LeftAndMainExtension instead.');
Deprecation::notice('3.0', 'Use LeftAndMainExtension instead.', Deprecation::SCOPE_CLASS);
parent::__construct();
}

View File

@ -103,7 +103,7 @@ abstract class ModelAdmin extends LeftAndMain {
$models = $this->getManagedModels();
if($this->request->param('ModelClass')) {
$this->modelClass = $this->request->param('ModelClass');
$this->modelClass = $this->unsanitiseClassName($this->request->param('ModelClass'));
} else {
reset($models);
$this->modelClass = key($models);
@ -118,7 +118,7 @@ abstract class ModelAdmin extends LeftAndMain {
}
public function Link($action = null) {
if(!$action) $action = $this->modelClass;
if(!$action) $action = $this->sanitiseClassName($this->modelClass);
return parent::Link($action);
}
@ -127,7 +127,7 @@ abstract class ModelAdmin extends LeftAndMain {
$exportButton = new GridFieldExportButton('before');
$exportButton->setExportColumns($this->getExportFields());
$listField = GridField::create(
$this->modelClass,
$this->sanitiseClassName($this->modelClass),
false,
$list,
$fieldConfig = GridFieldConfig_RecordEditor::create($this->stat('page_length'))
@ -150,7 +150,7 @@ abstract class ModelAdmin extends LeftAndMain {
);
$form->addExtraClass('cms-edit-form cms-panel-padded center');
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
$form->setFormAction(Controller::join_links($this->Link($this->modelClass), 'EditForm'));
$form->setFormAction(Controller::join_links($this->Link($this->sanitiseClassName($this->modelClass)), 'EditForm'));
$form->setAttribute('data-pjax-fragment', 'CurrentForm');
$this->extend('updateEditForm', $form);
@ -199,7 +199,7 @@ abstract class ModelAdmin extends LeftAndMain {
new RequiredFields()
);
$form->setFormMethod('get');
$form->setFormAction($this->Link($this->modelClass));
$form->setFormAction($this->Link($this->sanitiseClassName($this->modelClass)));
$form->addExtraClass('cms-search-form');
$form->disableSecurityToken();
$form->loadDataFrom($this->request->getVars());
@ -234,7 +234,7 @@ abstract class ModelAdmin extends LeftAndMain {
$forms->push(new ArrayData(array (
'Title' => $options['title'],
'ClassName' => $class,
'Link' => $this->Link($class),
'Link' => $this->Link($this->sanitiseClassName($class)),
'LinkOrCurrent' => ($class == $this->modelClass) ? 'current' : 'link'
)));
}
@ -242,6 +242,22 @@ abstract class ModelAdmin extends LeftAndMain {
return $forms;
}
/**
* Sanitise a model class' name for inclusion in a link
* @return string
*/
protected function sanitiseClassName($class) {
return str_replace('\\', '-', $class);
}
/**
* Unsanitise a model class' name from a URL param
* @return string
*/
protected function unsanitiseClassName($class) {
return str_replace('-', '\\', $class);
}
/**
* @return array Map of class name to an array of 'title' (see {@link $managed_models})
*/
@ -350,7 +366,7 @@ abstract class ModelAdmin extends LeftAndMain {
$fields,
$actions
);
$form->setFormAction(Controller::join_links($this->Link($this->modelClass), 'ImportForm'));
$form->setFormAction(Controller::join_links($this->Link($this->sanitiseClassName($this->modelClass)), 'ImportForm'));
$this->extend('updateImportForm', $form);
@ -419,7 +435,7 @@ abstract class ModelAdmin extends LeftAndMain {
// Show the class name rather than ModelAdmin title as root node
$models = $this->getManagedModels();
$items[0]->Title = $models[$this->modelClass]['title'];
$items[0]->Link = $this->Link($this->modelClass);
$items[0]->Link = $this->Link($this->sanitiseClassName($this->modelClass));
return $items;
}

View File

@ -40,8 +40,8 @@
.filter-buttons button.ss-gridfield-button-filter { background-position: -18px 4px !important; }
.cms-panel .cms-panel-content-collapsed { width: 40px; }
.cms-panel .cms-panel-content-collapsed h2, .cms-panel .cms-panel-content-collapsed h3 { display: none; }
.cms-panel .cms-panel-content-collapsed { position: relative; width: 40px; }
.cms-panel .cms-panel-content-collapsed h2.cms-panel-header, .cms-panel .cms-panel-content-collapsed h3.cms-panel-header { zoom: 1; position: absolute; top: 10px; right: 10px; writing-mode: tb-rl; float: right; z-index: 5000; }
.cms-content-toolbar .cms-tree-view-modes .checkboxAboveTree { margin-right: 1px; }
@ -52,3 +52,6 @@
.filter-buttons button.ss-gridfield-button-close { margin-right: -7px !important; }
.col-buttons { width: 18px; }
/* fix for actions buttons on edit page content overlapping */
.cms-content-actions .ss-ui-buttonset button { margin-right: 0; }

View File

@ -181,6 +181,8 @@ form.small .field input.text, form.small .field textarea, form.small .field sele
.field .chzn-container-single .chzn-single div { width: 24px; }
.field .chzn-container-single .chzn-single div b { background-position: 4px 0px; }
.field input.hasDatepicker { width: 50%; max-width: 96px; }
.field input.month, .field input.day, .field input.year { width: 56px; }
.field input.time { width: 64px; }
.field.remove-splitter { border-bottom: none; box-shadow: none; }
/** ---------------------------------------------------- Buttons ---------------------------------------------------- */
@ -293,6 +295,14 @@ body.cms { overflow: hidden; }
.cms-content-header .ss-ui-button .ui-button-text { line-height: 1.4; }
.cms-edit-form .cms-content-header-tabs .ui-tabs-nav li a { text-indent: 0; }
.cms-edit-form .cms-content-fields .ui-tabs-nav { border-bottom: none; float: right; margin: 8px 0 -1px 0; padding: 0 24px 0 0; }
.cms-edit-form .cms-content-fields .ui-tabs-nav li { float: left; }
.cms-edit-form .cms-content-fields .ui-tabs-nav li a { font-weight: bold; line-height: 16px; padding: 8px 20px 8px; }
.cms-edit-form .cms-content-fields .ui-tabs-nav .ui-state-default, .cms-edit-form .cms-content-fields .ui-tabs-nav .ui-widget-content .ui-state-default, .cms-edit-form .cms-content-fields .ui-tabs-nav .ui-widget-header .ui-state-default { border: 1px solid #c0c0c2; }
.cms-edit-form .cms-content-fields .ui-tabs-nav .ui-state-active, .cms-edit-form .cms-content-fields .ui-tabs-nav .ui-widget-content .ui-state-active, .cms-edit-form .cms-content-fields .ui-tabs-nav .ui-widget-header .ui-state-active { padding-bottom: 1px; background: #f0f3f4 url(../images/textures/bg_cms_main_content.png) repeat top left; border: 1px solid #c0c0c2; }
.cms-edit-form .ss-tabset .ss-tabset { position: static; }
.cms-edit-form .ss-tabset .ui-tabs-panel { border-top: 1px solid #c0c0c2; clear: both; }
.cms-edit-form .ss-tabset.ss-tabset-tabshidden .ui-tabs-panel { border-top: none; }
/** -------------------------------------------- Tabs -------------------------------------------- */
.cms-content-header .ui-tabs-nav li, .cms-dialog .ui-tabs-nav li { margin: 0; }
@ -335,6 +345,7 @@ body.cms { overflow: hidden; }
.message.warning { background-color: #ffbe66; border-color: #ff9300; }
.message.error, .message.bad, .message.required { background-color: #ffbe66; border-color: #ff9300; }
.message.good { background-color: #65a839; background-color: rgba(101, 168, 57, 0.7); border-color: #65a839; color: #fff; text-shadow: 1px -1px 0 #1f9433; -webkit-border-radius: 3px 3px 3px 3px; -moz-border-radius: 3px 3px 3px 3px; -ms-border-radius: 3px 3px 3px 3px; -o-border-radius: 3px 3px 3px 3px; border-radius: 3px 3px 3px 3px; }
.message.good a { text-shadow: none; }
.message p { margin: 0; }
/** -------------------------------------------- Page icons -------------------------------------------- */
@ -353,17 +364,18 @@ body.cms { overflow: hidden; }
.cms-add-form .step-label .title { padding-top: 5px; font-weight: bold; text-shadow: 1px 1px 0 white; }
.cms-add-form ul.SelectionGroup { padding-left: 28px; }
.cms-add-form .parent-mode { padding: 8px; overflow: auto; }
.cms-add-form #PageType li { float: none; width: 100%; padding: 9px 0 9px 15px; overflow: hidden; border-bottom-width: 2px; border-bottom: 2px groove rgba(255, 255, 255, 0.8); -webkit-border-image: url(../images/textures/bg_fieldset_elements_border.png) 2 stretch stretch; border-image: url(../images/textures/bg_fieldset_elements_border.png) 2 stretch stretch; }
.cms-add-form #PageType li:last-child { border-bottom: none; }
.cms-add-form #PageType li:hover, .cms-add-form #PageType li.selected { background-color: rgba(255, 255, 102, 0.3); }
.cms-add-form #PageType li.disabled { color: #aaaaaa; }
.cms-add-form #PageType li.disabled:hover { background: none; }
.cms-add-form #PageType li input { margin: inherit; }
.cms-add-form #PageType li label { padding-left: 0; padding-bottom: 0; }
.cms-add-form #PageType li input, .cms-add-form #PageType li label, .cms-add-form #PageType li .page-icon, .cms-add-form #PageType li .title { float: left; line-height: 1.3em; }
.cms-add-form #PageType li .page-icon { margin: 0 4px; }
.cms-add-form #PageType li .title { width: 120px; font-weight: bold; padding-right: 10px; }
.cms-add-form #PageType li .description { font-style: italic; }
.cms-add-form #PageType ul { padding-left: 20px; }
.cms-add-form #PageType ul li { float: none; width: 100%; padding: 9px 0 9px 15px; overflow: hidden; border-bottom-width: 2px; border-bottom: 2px groove rgba(255, 255, 255, 0.8); -webkit-border-image: url(../images/textures/bg_fieldset_elements_border.png) 2 stretch stretch; border-image: url(../images/textures/bg_fieldset_elements_border.png) 2 stretch stretch; }
.cms-add-form #PageType ul li:last-child { border-bottom: none; }
.cms-add-form #PageType ul li:hover, .cms-add-form #PageType ul li.selected { background-color: rgba(255, 255, 102, 0.3); }
.cms-add-form #PageType ul li.disabled { color: #aaaaaa; }
.cms-add-form #PageType ul li.disabled:hover { background: none; }
.cms-add-form #PageType ul li input { margin: inherit; }
.cms-add-form #PageType ul li label { padding-left: 0; padding-bottom: 0; }
.cms-add-form #PageType ul li input, .cms-add-form #PageType ul li label, .cms-add-form #PageType ul li .page-icon, .cms-add-form #PageType ul li .title { float: left; line-height: 1.3em; }
.cms-add-form #PageType ul li .page-icon { margin: 0 4px; }
.cms-add-form #PageType ul li .title { width: 120px; font-weight: bold; padding-right: 10px; }
.cms-add-form #PageType ul li .description { font-style: italic; }
/** -------------------------------------------- Content toolbar -------------------------------------------- */
.cms-content-toolbar { display: block; margin: 0 0 15px 0; border-bottom: 1px solid rgba(201, 205, 206, 0.8); -webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); -moz-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); -o-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); *zoom: 1; /* smaller treedropdown */ }
@ -516,6 +528,8 @@ form.member-profile-form .ui-tabs-nav .ui-corner-all, form.member-profile-form .
.cms .ui-widget-overlay { background-color: #000; background-image: none; }
.cms .ui-dialog { min-width: 570px; }
.cms .ui-dialog .htmleditorfield-dialog { min-width: 570px; }
.cms .ui-dialog .ss-ui-dialog.ui-dialog-content { padding-top: 0px; }
.ui-dialog { background: url("../images/textures/bg_cms_main_content.png") repeat left top #f0f3f4; border: 3px solid #000 !important; border-radius: 8px; overflow: visible; padding: 0; }
@ -563,7 +577,7 @@ body.cms-dialog { overflow: auto; background: url("../images/textures/bg_cms_mai
.htmleditorfield-dialog .ui-tabs ul.ui-tabs-nav li.ui-state-active { background: #f0f3f4 url("../admin/images/textures/bg_cms_main_content.png") repeat left top; }
.htmleditorfield-dialog .ui-tabs ul.ui-tabs-nav li.ui-state-active a { color: #444444; text-shadow: none; }
.htmleditorfield-dialog .ui-tabs .ui-tabs-panel { padding: 0; }
.htmleditorfield-dialog .ss-insert-media, .htmleditorfield-dialog .Actions { padding: 8px 16px; }
.htmleditorfield-dialog .ss-insert-media, .htmleditorfield-dialog .Actions, .htmleditorfield-dialog .ss-insert-link { padding: 8px 16px; }
.htmleditorfield-dialog .details .file-url { display: block; width: 450px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; -o-text-overflow: ellipsis; }
.htmleditorfield-dialog .details .cms-file-info .field { border: none; -webkit-box-shadow: 0 0 0 rgba(0, 0, 0, 0); -moz-box-shadow: 0 0 0 rgba(0, 0, 0, 0); box-shadow: 0 0 0 rgba(0, 0, 0, 0); }
.htmleditorfield-dialog .details .field { border-bottom: 1px solid rgba(201, 205, 206, 0.8); -webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); -moz-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); }
@ -620,14 +634,8 @@ form.import-form label.left { width: 250px; }
/** -------------------------------------------- Page Edit Controller -------------------------------------------- */
.cms-container .CMSPageEditController, .cms-container .CMSPageSettingsController, .cms-container .CMSPageHistoryController { margin-left: -1px; }
.cms-container .CMSPageEditController .cms-edit-form, .cms-container .CMSPageSettingsController .cms-edit-form, .cms-container .CMSPageHistoryController .cms-edit-form { background: #e6eaed; }
.cms-container .CMSPageEditController .cms-edit-form .ui-tabs-nav, .cms-container .CMSPageSettingsController .cms-edit-form .ui-tabs-nav, .cms-container .CMSPageHistoryController .cms-edit-form .ui-tabs-nav { border-bottom: none; float: right; margin: 8px 0 -1px 0; padding: 0 24px 0 0; }
.cms-container .CMSPageEditController .cms-edit-form .ui-tabs-nav li, .cms-container .CMSPageSettingsController .cms-edit-form .ui-tabs-nav li, .cms-container .CMSPageHistoryController .cms-edit-form .ui-tabs-nav li { float: left; }
.cms-container .CMSPageEditController .cms-edit-form .ui-tabs-nav li a, .cms-container .CMSPageSettingsController .cms-edit-form .ui-tabs-nav li a, .cms-container .CMSPageHistoryController .cms-edit-form .ui-tabs-nav li a { font-weight: bold; line-height: 16px; padding: 8px 20px 8px; }
.cms-container .CMSPageEditController .cms-edit-form .ui-tabs-nav .ui-state-default, .cms-container .CMSPageEditController .cms-edit-form .ui-tabs-nav .ui-widget-content .ui-state-default, .cms-container .CMSPageEditController .cms-edit-form .ui-tabs-nav .ui-widget-header .ui-state-default, .cms-container .CMSPageSettingsController .cms-edit-form .ui-tabs-nav .ui-state-default, .cms-container .CMSPageSettingsController .cms-edit-form .ui-tabs-nav .ui-widget-content .ui-state-default, .cms-container .CMSPageSettingsController .cms-edit-form .ui-tabs-nav .ui-widget-header .ui-state-default, .cms-container .CMSPageHistoryController .cms-edit-form .ui-tabs-nav .ui-state-default, .cms-container .CMSPageHistoryController .cms-edit-form .ui-tabs-nav .ui-widget-content .ui-state-default, .cms-container .CMSPageHistoryController .cms-edit-form .ui-tabs-nav .ui-widget-header .ui-state-default { border: 1px solid #c0c0c2; }
.cms-container .CMSPageEditController .cms-edit-form .ui-tabs-nav .ui-state-active, .cms-container .CMSPageEditController .cms-edit-form .ui-tabs-nav .ui-widget-content .ui-state-active, .cms-container .CMSPageEditController .cms-edit-form .ui-tabs-nav .ui-widget-header .ui-state-active, .cms-container .CMSPageSettingsController .cms-edit-form .ui-tabs-nav .ui-state-active, .cms-container .CMSPageSettingsController .cms-edit-form .ui-tabs-nav .ui-widget-content .ui-state-active, .cms-container .CMSPageSettingsController .cms-edit-form .ui-tabs-nav .ui-widget-header .ui-state-active, .cms-container .CMSPageHistoryController .cms-edit-form .ui-tabs-nav .ui-state-active, .cms-container .CMSPageHistoryController .cms-edit-form .ui-tabs-nav .ui-widget-content .ui-state-active, .cms-container .CMSPageHistoryController .cms-edit-form .ui-tabs-nav .ui-widget-header .ui-state-active { padding-bottom: 1px; background: #e6eaed; border: 1px solid #c0c0c2; }
.cms-container .CMSPageEditController .ss-tabset .ss-tabset, .cms-container .CMSPageSettingsController .ss-tabset .ss-tabset, .cms-container .CMSPageHistoryController .ss-tabset .ss-tabset { position: static; }
.cms-container .CMSPageEditController .ss-tabset .ui-tabs-panel, .cms-container .CMSPageSettingsController .ss-tabset .ui-tabs-panel, .cms-container .CMSPageHistoryController .ss-tabset .ui-tabs-panel { background: #e6eaed; border-top: 1px solid #c0c0c2; clear: both; }
.cms-container .CMSPageEditController .ss-tabset.ss-tabset-tabshidden .ui-tabs-panel, .cms-container .CMSPageSettingsController .ss-tabset.ss-tabset-tabshidden .ui-tabs-panel, .cms-container .CMSPageHistoryController .ss-tabset.ss-tabset-tabshidden .ui-tabs-panel { border-top: none; }
.cms-container .CMSPageEditController .cms-edit-form .ui-tabs-nav .ui-state-active, .cms-container .CMSPageEditController .cms-edit-form .ui-tabs-nav .ui-widget-content .ui-state-active, .cms-container .CMSPageEditController .cms-edit-form .ui-tabs-nav .ui-widget-header .ui-state-active, .cms-container .CMSPageSettingsController .cms-edit-form .ui-tabs-nav .ui-state-active, .cms-container .CMSPageSettingsController .cms-edit-form .ui-tabs-nav .ui-widget-content .ui-state-active, .cms-container .CMSPageSettingsController .cms-edit-form .ui-tabs-nav .ui-widget-header .ui-state-active, .cms-container .CMSPageHistoryController .cms-edit-form .ui-tabs-nav .ui-state-active, .cms-container .CMSPageHistoryController .cms-edit-form .ui-tabs-nav .ui-widget-content .ui-state-active, .cms-container .CMSPageHistoryController .cms-edit-form .ui-tabs-nav .ui-widget-header .ui-state-active { background: #e6eaed; }
.cms-container .CMSPageEditController .ss-tabset .ui-tabs-panel, .cms-container .CMSPageSettingsController .ss-tabset .ui-tabs-panel, .cms-container .CMSPageHistoryController .ss-tabset .ui-tabs-panel { background: #e6eaed; }
/** -------------------------------------------- Page Settings Controller -------------------------------------------- */
.cms-container .CMSMain.CMSPageSettingsController .tab#Root_Settings .optionset li { white-space: nowrap; padding-top: 8px; }
@ -757,14 +765,14 @@ li.class-ErrorPage > a .jstree-pageicon { background-position: 0 -112px; }
.cms-tree a.jstree-loading .jstree-pageicon { background: url(../images/throbber.gif) top left no-repeat; }
/** Styles for the left hand side menu and header for the admin panels. Take into consideration CSS selector performance. @package framework @subpackage admin */
.cms-logo-header { background-color: #00111d; position: relative; padding: 9px 8px 0 4px; line-height: 24px; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzAwMTExZCIvPjxzdG9wIG9mZnNldD0iNTAlIiBzdG9wLWNvbG9yPSIjMDAzMDUwIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDAxMTFkIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0idXJsKCNncmFkKSIgLz48L3N2Zz4g'); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #00111d), color-stop(50%, #003050), color-stop(100%, #00111d)); background-image: -webkit-linear-gradient(#00111d, #003050, #00111d); background-image: -moz-linear-gradient(#00111d, #003050, #00111d); background-image: -o-linear-gradient(#00111d, #003050, #00111d); background-image: -ms-linear-gradient(#00111d, #003050, #00111d); background-image: linear-gradient(#00111d, #003050, #00111d); }
.cms-logo-header span { color: white; white-space: nowrap; text-overflow: ellipsis; display: block; }
.cms-logo-header { background-color: #00111d; position: relative; padding: 0 8px 0 4px; line-height: 24px; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzAwMTExZCIvPjxzdG9wIG9mZnNldD0iNTAlIiBzdG9wLWNvbG9yPSIjMDAzMDUwIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDAxMTFkIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0idXJsKCNncmFkKSIgLz48L3N2Zz4g'); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #00111d), color-stop(50%, #003050), color-stop(100%, #00111d)); background-image: -webkit-linear-gradient(#00111d, #003050, #00111d); background-image: -moz-linear-gradient(#00111d, #003050, #00111d); background-image: -o-linear-gradient(#00111d, #003050, #00111d); background-image: -ms-linear-gradient(#00111d, #003050, #00111d); background-image: linear-gradient(#00111d, #003050, #00111d); }
.cms-logo-header span { color: white; display: block; }
.cms-logo-header span a { color: #3ebae0; display: inline; }
.cms-logo { border-bottom: 1px solid #03090c; height: 31px; overflow: hidden; padding: 0 0 0 4px; vertical-align: middle; font-size: 12px; }
.cms-logo { border-bottom: 1px solid #03090c; overflow: hidden; padding: 8px 0; position: relative; vertical-align: middle; font-size: 12px; }
.cms-logo .version { display: none; }
.cms-logo a { display: inline-block; height: 24px; width: 24px; float: left; margin-right: 8px; background: url("../images/logo_small.png") no-repeat; text-indent: -9999em; padding-right: 7px; border-right: 1px solid #19435c; }
.cms-logo span { font-weight: bold; font-size: 14px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; -o-text-overflow: ellipsis; padding-top: 1px; }
.cms-logo a { position: absolute; top: 8px; bottom: 8px; left: 4px; display: block; width: 24px; background: url("../images/logo_small.png") no-repeat left center; text-indent: -9999em; padding-right: 7px; border-right: 1px solid #19435c; }
.cms-logo span { font-weight: bold; font-size: 14px; line-height: 20px; padding: 2px 0; margin-left: 44px; }
.cms-login-status { border-top: 1px solid #19435c; height: 24px; padding: 7px 4px 0 4px; overflow: hidden; line-height: 16px; font-size: 11px; }
.cms-login-status .logout-link { display: inline-block; height: 16px; width: 16px; float: left; margin: 0 8px 0 3px; background: url('../images/sprites-32x32-sb47394f892.png') 0 -76px no-repeat; text-indent: -9999em; }

View File

@ -163,9 +163,10 @@
// which means the browser auto-selects the first available form button.
// This might be an unrelated button of the form field,
// or a destructive action (if "save" is not available, or not on first position).
if(button) this.closest('.cms-container').submitForm(this, button);
return false;
if(this.prop("target") != "_blank") {
if(button) this.closest('.cms-container').submitForm(this, button);
return false;
}
},
/**

View File

@ -67,7 +67,7 @@
if(this.is('.is-collapsed')) return;
// var url = ui.xmlhttp.getResponseHeader('x-frontend-url');
var url = $('.cms-edit-form').find(':input[name=StageURLSegment]').val();
var url = $('.cms-edit-form').find(':input[name=PreviewURL],:input[name=StageURLSegment]').val();
if(url) {
this.loadUrl(url);
this.unblock();
@ -286,7 +286,7 @@
onclick: function(e) {
e.preventDefault();
var preview = $('.cms-preview'), url = $('.cms-edit-form').find(':input[name=StageURLSegment]').val();
var preview = $('.cms-preview'), url = $('.cms-edit-form').find(':input[name=PreviewURL],:input[name=StageURLSegment]').val();
if(url) {
preview.loadUrl(url);
preview.unblock();

View File

@ -23,6 +23,7 @@ jQuery.noConflict();
var applyChosen = function(el){
if(el.outerWidth()){
el.chosen({
'disable_search_threshold' : 20,
'allow_single_deselect': true
}).addClass("has-chzn");
// Copy over title attribute if required
@ -162,6 +163,9 @@ jQuery.noConflict();
if(abort) return;
}
// Save tab selections so we can restore them later
this.saveTabState();
if(window.History.enabled) {
// Active menu item is set based on X-Controller ajax header,
// which matches one class on the menu
@ -208,12 +212,6 @@ jQuery.noConflict();
return false;
}
// save tab selections in order to reconstruct them later
var selectedTabs = [];
form.find('.cms-tabset').each(function(i, el) {
if($(el).attr('id')) selectedTabs.push({id:$(el).attr('id'), selected:$(el).tabs('option', 'selected')});
});
// get all data from the form
var formData = form.serializeArray();
// add button action
@ -223,6 +221,9 @@ jQuery.noConflict();
// as automatic browser ajax response redirects seem to discard the hash/fragment.
formData.push({name: 'BackURL', value:History.getPageUrl()});
// Save tab selections so we can restore them later
this.saveTabState();
// Standard Pjax behaviour is to replace the submitted form with new content.
// The returned view isn't always decided upon when the request
// is fired, so the server might decide to change it based on its own logic,
@ -242,19 +243,8 @@ jQuery.noConflict();
var newContentEls = self.handleAjaxResponse(data, status, xhr);
if(!newContentEls) return;
var newForm = newContentEls.filter('form');
// Re-init tabs (in case the form tag itself is a tabset)
if(newForm.hasClass('cms-tabset')) newForm.removeClass('cms-tabset').addClass('cms-tabset');
// re-select previously saved tabs
$.each(selectedTabs, function(i, selectedTab) {
newForm.find('#' + selectedTab.id).tabs('select', selectedTab.selected);
});
newForm.trigger('aftersubmitform', {status: status, xhr: xhr, formData: formData});
},
dataType: 'json'
newContentEls.filter('form').trigger('aftersubmitform', {status: status, xhr: xhr, formData: formData});
}
}, ajaxOptions));
return false;
@ -288,7 +278,8 @@ jQuery.noConflict();
var self = this, h = window.History, state = h.getState(),
fragments = state.data.pjax || 'Content', headers = {},
contentEls = this._findFragments(fragments.split(','));
fragmentsArr = fragments.split(','),
contentEls = this._findFragments(fragmentsArr);
// For legacy IE versions (IE7 and IE8), reload without ajax
// as a crude way to fix memory leaks through whole window refreshes.
@ -299,6 +290,14 @@ jQuery.noConflict();
return;
}
// If any of the requested Pjax fragments don't exist in the current view,
// fetch the "Content" view instead, which is the "outermost" fragment
// that can be reloaded without reloading the whole window.
if(contentEls.length < fragmentsArr.length) {
fragments = 'Content', fragmentsArr = ['Content'];
contentEls = this._findFragments(fragmentsArr);
}
this.trigger('beforestatechange', {state: state, element: contentEls});
// Set Pjax headers, which can declare a preference for the returned view.
@ -329,7 +328,7 @@ jQuery.noConflict();
* Can be hooked into an ajax 'success' callback.
*/
handleAjaxResponse: function(data, status, xhr) {
var self = this;
var self = this, url, selectedTabs;
// Pseudo-redirects via X-ControllerURL might return empty data, in which
// case we'll ignore the response
@ -393,8 +392,14 @@ jQuery.noConflict();
if(origVisible) newContentEl.css('visibility', 'visible');
});
// Re-init tabs (in case the form tag itself is a tabset)
var newForm = newContentEls.filter('form');
if(newForm.hasClass('cms-tabset')) newForm.removeClass('cms-tabset').addClass('cms-tabset');
this.redraw();
this.restoreTabState();
return newContentEls;
},
@ -427,6 +432,50 @@ jQuery.noConflict();
$(window).trigger('statechange');
$(this).redraw();
},
/**
* Save tab selections in order to reconstruct them later.
* Requires HTML5 sessionStorage support.
*/
saveTabState: function() {
if(typeof(window.sessionStorage)=="undefined") return;
var selectedTabs = [], url = this._tabStateUrl();
this.find('.cms-tabset,.ss-tabset').each(function(i, el) {
var id = $(el).attr('id');
if(!id) return; // we need a unique reference
if(!$(el).data('tabs')) return; // don't act on uninit'ed controls
if($(el).data('ignoreTabState')) return; // allow opt-out
selectedTabs.push({id:id, selected:$(el).tabs('option', 'selected')});
});
if(selectedTabs) window.sessionStorage.setItem('tabs-' + url, JSON.stringify(selectedTabs));
},
/**
* Re-select previously saved tabs.
* Requires HTML5 sessionStorage support.
*/
restoreTabState: function() {
if(typeof(window.sessionStorage)=="undefined") return;
var self = this, url = this._tabStateUrl(),
data = window.sessionStorage.getItem('tabs-' + url),
selectedTabs = data ? JSON.parse(data) : false;
if(selectedTabs) {
$.each(selectedTabs, function(i, selectedTab) {
var el = self.find('#' + selectedTab.id);
if(!el.data('tabs')) return; // don't act on uninit'ed controls
el.tabs('select', selectedTab.selected);
});
}
},
_tabStateUrl: function() {
return History.getState().url
.replace(/\?.*/, '')
.replace(/#.*/, '')
.replace($('base').attr('href'), '');
}
});
@ -670,7 +719,8 @@ jQuery.noConflict();
showDetailView: function(url) {
// Include any GET parameters from the current URL, as the view state might depend on it.
// For example, a list prefiltered through external search criteria might be passed to GridField.
if(window.location.search) url += window.location.search;
var params = window.location.search.replace(/^\?/, '');
if(params) url = $.path.addSearchParams(url, params);
$('.cms-container').loadPanel(url);
}
});

View File

@ -241,6 +241,14 @@ form.small .field, .field.small {
max-width: ($grid-x * 12);
}
input.month, input.day, input.year {
width: ($grid-x * 7);
}
input.time {
width: ($grid-x * 8); // smaller time field, since input is restricted
}
/* Hides borders in settings/access. Activated from JS */
&.remove-splitter {
border-bottom: none;

View File

@ -10,7 +10,7 @@
.cms-logo-header {
background-color: darken($color-dark-bg, 10%);
position: relative;
padding: $grid-y + 1 8px 0 4px;
padding: 0 8px 0 4px;
line-height: 24px;
@include background-image(
@ -19,8 +19,6 @@
span {
color: $color-text-light;
white-space: nowrap;
text-overflow: ellipsis;
display: block;
a {
@ -32,9 +30,9 @@
.cms-logo {
border-bottom: 1px solid darken($color-dark-separator, 20%);
height: 31px;
overflow: hidden;
padding: 0 0 0 4px;
padding: $grid-y 0;
position: relative;
vertical-align: middle;
font-size: $font-base-size;
@ -43,12 +41,13 @@
}
a {
display: inline-block;
height: 24px;
position: absolute;
top: $grid-y;
bottom: $grid-y;
left: 4px;
display: block;
width: 24px;
float: left;
margin-right: 8px;
background: $application-logo-small no-repeat;
background: $application-logo-small no-repeat left center;
text-indent: -9999em;
padding-right: 7px;
border-right: 1px solid $color-dark-separator;
@ -57,8 +56,9 @@
span {
font-weight: bold;
font-size: 14px;
@include hide-text-overflow();
padding-top:1px;
line-height: 20px;
padding: 2px 0;
margin-left: 44px;
}
}

View File

@ -186,6 +186,50 @@ body.cms {
}
}
}
.cms-content-fields .ui-tabs-nav {
border-bottom: none;
float: right;
margin: $grid-y 0 -1px 0;
padding: 0 $grid-x*3 0 0;
li {
float: left;
a {
font-weight: bold;
line-height: $grid-y * 2;
padding: $grid-y $grid-x*2.5 $grid-y;
}
}
.ui-state-default,
.ui-widget-content .ui-state-default,
.ui-widget-header .ui-state-default {
border:1px solid $color-button-generic-border;
}
.ui-state-active,
.ui-widget-content .ui-state-active,
.ui-widget-header .ui-state-active {
padding-bottom:1px;
background: $tab-panel-texture-background;
border:1px solid $color-button-generic-border;
}
}
.ss-tabset {
.ss-tabset {
position: static;
}
.ui-tabs-panel {
border-top:1px solid $color-button-generic-border;
clear: both;
}
&.ss-tabset-tabshidden .ui-tabs-panel {
border-top: none;
}
}
}
/** --------------------------------------------
@ -418,6 +462,9 @@ body.cms {
color:#fff;
@include text-shadow(1px -1px 0 $color-button-constructive);
@include border-radius(3px 3px 3px 3px);
a {
text-shadow:none;
}
}
p {
@ -481,59 +528,63 @@ body.cms {
overflow: auto;
}
#PageType li {
float: none;
width: 100%;
padding: 9px 0 9px 15px;
overflow: hidden;
border-bottom-width: 2px;
border-bottom: 2px groove lighten($color-shadow-light, 95%);
-webkit-border-image: url(../images/textures/bg_fieldset_elements_border.png) 2 stretch stretch;
border-image: url(../images/textures/bg_fieldset_elements_border.png) 2 stretch stretch;
#PageType {
ul {
padding-left: 20px;
li {
float: none;
width: 100%;
padding: 9px 0 9px 15px;
overflow: hidden;
border-bottom-width: 2px;
border-bottom: 2px groove lighten($color-shadow-light, 95%);
-webkit-border-image: url(../images/textures/bg_fieldset_elements_border.png) 2 stretch stretch;
border-image: url(../images/textures/bg_fieldset_elements_border.png) 2 stretch stretch;
&:last-child {
border-bottom: none;
}
&:last-child {
border-bottom: none;
}
&:hover, &.selected {
background-color: $color-highlight-opacity;
}
&:hover, &.selected {
background-color: $color-highlight-opacity;
}
&.disabled {
color: $color-text-disabled;
&:hover {
background: none;
&.disabled {
color: $color-text-disabled;
&:hover {
background: none;
}
}
input {
margin: inherit;
}
label {
padding-left: 0;
padding-bottom: 0;
}
input, label, .page-icon, .title {
float: left;
line-height: 1.3em;
}
.page-icon {
margin: 0 4px;
}
.title {
width: 120px;
font-weight: bold;
padding-right: 10px;
}
.description {
font-style: italic;
}
}
}
input {
margin: inherit;
}
label {
padding-left: 0;
padding-bottom: 0;
}
input, label, .page-icon, .title {
float: left;
line-height: 1.3em;
}
.page-icon {
margin: 0 4px;
}
.title {
width: 120px;
font-weight: bold;
padding-right: 10px;
}
.description {
font-style: italic;
}
}
}
@ -1280,8 +1331,14 @@ form.member-profile-form {
background-image: none;
}
.cms .ui-dialog .ss-ui-dialog.ui-dialog-content {
padding-top: 0px; //removes padding so that tabs are flush with header
.cms .ui-dialog{
min-width:570px;
.htmleditorfield-dialog{
min-width:570px;
}
.ss-ui-dialog.ui-dialog-content {
padding-top: 0px; //removes padding so that tabs are flush with header
}
}
// Elements with this class can either frame inline markup or an iframe,
@ -1544,7 +1601,7 @@ body.cms-dialog {
padding:0;
}
}
.ss-insert-media, .Actions{
.ss-insert-media, .Actions, .ss-insert-link{
padding:$grid-y $grid-x*2 ;
}
.details{
@ -1774,54 +1831,22 @@ form.import-form {
.cms-container {
.CMSPageEditController, .CMSPageSettingsController, .CMSPageHistoryController {
// Fix pixel gap between nav tree and main page header
margin-left: -1px; // Removed to close gap far right of right tabs?
//Styling of tabs on page edit
.cms-edit-form {
background:darken($color-widget-bg, 2%);
margin-left: -1px; // Removed to close gap far right of right tabs?
.cms-edit-form {
background:darken($color-widget-bg, 2%);
.ui-tabs-nav {
border-bottom: none;
float: right;
margin: $grid-y 0 -1px 0;
padding: 0 $grid-x*3 0 0;
li {
float: left;
a {
font-weight: bold;
line-height: $grid-y * 2;
padding: $grid-y $grid-x*2.5 $grid-y;
}
}
.ui-state-default,
.ui-widget-content .ui-state-default,
.ui-widget-header .ui-state-default {
border:1px solid $color-button-generic-border;
}
.ui-state-active,
.ui-widget-content .ui-state-active,
.ui-widget-header .ui-state-active {
padding-bottom:1px;
background:darken($color-widget-bg, 2%);
border:1px solid $color-button-generic-border;
}
}
}
.ss-tabset {
.ss-tabset {
position: static;
}
.ui-tabs-panel {
background: darken($color-widget-bg, 2%);
border-top:1px solid $color-button-generic-border;
clear: both;
}
&.ss-tabset-tabshidden .ui-tabs-panel {
border-top: none;
}
.ss-tabset .ui-tabs-panel {
background: darken($color-widget-bg, 2%);
}
}
}

View File

@ -40,3 +40,12 @@
.col-buttons{
width:18px;
}
/* fix for actions buttons on edit page content overlapping */
.cms-content-actions {
.ss-ui-buttonset {
button {
margin-right: 0;
}
}
}

View File

@ -2,14 +2,14 @@
<%-- Tab nav is rendered in CMSEditForm.ss --%>
<% loop Tabs %>
<div $AttributesHTML>
<% if Tabs %>
$FieldHolder
<% else %>
<% loop Fields %>
$FieldHolder
<% end_loop %>
<% end_if %>
</div>
<% if Tabs %>
$FieldHolder
<% else %>
<div $AttributesHTML>
<% loop Fields %>
$FieldHolder
<% end_loop %>
</div>
<% end_if %>
<% end_loop %>
</div>

View File

@ -29,7 +29,7 @@
$EditFormTools
<% end_with %>
<div class="cms-content-fields center cms-panel-padded">
<div class="cms-content-fields center <% if not $Fields.hasTabset %>cms-panel-padded<% end_if %>">
<% if Message %>
<p id="{$FormName}_error" class="message $MessageType">$Message</p>
<% else %>

View File

@ -325,6 +325,7 @@ Copyright (c) 2011 by Harvest
this.container_id = this.form_field.id.length ? this.form_field.id.replace(/(:|\.)/g, '_') : this.generate_field_id();
this.container_id += "_chzn";
this.f_width = this.form_field_jq.outerWidth();
if (this.f_width==0) this.f_width = this.form_field_jq.css("width");
this.default_text = this.form_field_jq.data('placeholder') ? this.form_field_jq.data('placeholder') : this.default_text_default;
container_div = $("<div />", {
id: this.container_id,

View File

@ -8,6 +8,7 @@
* @subpackage integration
*/
class RSSFeed extends ViewableData {
/**
* Casting information for this object's methods.
* Let's us use $Title.XML in templates
@ -80,6 +81,11 @@ class RSSFeed extends ViewableData {
*/
protected $etag;
/**
* @var string
*/
protected $template = 'RSSFeed';
/**
* Constructor
*
@ -203,10 +209,29 @@ class RSSFeed extends ViewableData {
function feedContent() {
$prevState = SSViewer::get_source_file_comments();
SSViewer::set_source_file_comments(false);
$content = str_replace('&nbsp;', '&#160;', $this->renderWith('RSSFeed'));
$content = str_replace('&nbsp;', '&#160;', $this->renderWith($this->getTemplate()));
SSViewer::set_source_file_comments($prevState);
return $content;
}
/**
* Set the name of the template to use. Actual template will be resolved
* via the standard template inclusion process.
*
* @param string
*/
public function setTemplate($template) {
$this->template = $template;
}
/**
* Returns the name of the template to use.
*
* @return string
*/
public function getTemplate() {
return $this->template;
}
}
/**

View File

@ -37,7 +37,8 @@ chdir(dirname($_SERVER['SCRIPT_FILENAME']));
*/
if(isset($_SERVER['argv'][2])) {
$args = array_slice($_SERVER['argv'],2);
$_GET = array();
if(!isset($_GET)) $_GET = array();
if(!isset($_REQUEST)) $_REQUEST = array();
foreach($args as $arg) {
if(strpos($arg,'=') == false) {
$_GET['args'][] = $arg;
@ -47,7 +48,7 @@ if(isset($_SERVER['argv'][2])) {
$_GET = array_merge($_GET, $newItems);
}
}
$_REQUEST = $_GET;
$_REQUEST = array_merge($_REQUEST, $_GET);
}
// Set 'url' GET parameter

View File

@ -12,18 +12,91 @@ class Cookie {
*/
static $report_errors = true;
/**
* @var string cookie class
*/
static $cookie_class = 'Cookie';
private static $inst = null;
public static function get_inst() {
if(is_null(self::$inst)) {
self::$inst = new self::$cookie_class();
}
return self::$inst;
}
/**
* Set a cookie variable
*
* @param string $name The variable name
* @param string $value The variable value.
* @param mixed $value The variable value.
* @param int $expiry The expiry time, in days. Defaults to 90.
* @param string $path See http://php.net/set_session
* @param string $domain See http://php.net/set_session
* @param boolean $secure See http://php.net/set_session
* @param boolean $httpOnly See http://php.net/set_session
*/
static function set($name, $value, $expiry = 90, $path = null, $domain = null, $secure = false, $httpOnly = false) {
public static function set($name, $value, $expiry = 90, $path = null, $domain = null, $secure = false, $httpOnly = false) {
return self::get_inst()->inst_set($name, $value, $expiry, $path, $domain, $secure, $httpOnly);
}
/**
* Get a cookie variable.
*
* @param string
* @return mixed
*/
public static function get($name) {
return self::get_inst()->inst_get($name);
}
/**
* @param string
* @param string
* @param string
*/
public static function forceExpiry($name, $path = null, $domain = null) {
Deprecation::notice('3.1', 'Use Cookie::force_expiry instead.');
return self::force_expiry($name, $path, $domain);
}
/**
* @param string
* @param string
* @param string
*/
public static function force_expiry($name, $path = null, $domain = null) {
return self::get_inst()->inst_force_expiry($name, $path, $domain);
}
/**
* @param bool
*/
public static function set_report_errors($reportErrors) {
self::get_inst()->inst_set_report_errors($reportErrors);
}
/**
* @return bool
*/
public static function report_errors() {
return self::get_inst()->inst_report_errors();
}
/**
* Set a cookie variable
*
* @param string $name The variable name
* @param mixed $value The variable value.
* @param int $expiry The expiry time, in days. Defaults to 90.
* @param string $path See http://php.net/set_session
* @param string $domain See http://php.net/set_session
* @param boolean $secure See http://php.net/set_session
* @param boolean $httpOnly See http://php.net/set_session
*/
protected function inst_set($name, $value, $expiry = 90, $path = null, $domain = null, $secure = false, $httpOnly = false) {
if(!headers_sent($file, $line)) {
$expiry = $expiry > 0 ? time()+(86400*$expiry) : $expiry;
$path = ($path) ? $path : Director::baseURL();
@ -36,23 +109,33 @@ class Cookie {
}
/**
* Get a cookie variable
* @param string
* @return mixed
*/
static function get($name) {
protected function inst_get($name) {
return isset($_COOKIE[$name]) ? $_COOKIE[$name] : null;
}
static function forceExpiry($name, $path = null, $domain = null) {
/**
* @param string
*/
protected function inst_force_expiry($name, $path = null, $domain = null) {
if(!headers_sent($file, $line)) {
self::set($name, null, -20, $path, $domain);
}
}
static function set_report_errors($reportErrors) {
/**
* @param bool
*/
protected function inst_set_report_errors($reportErrors) {
self::$report_errors = $reportErrors;
}
static function report_errors() {
/**
* @return bool
*/
protected function inst_report_errors() {
return self::$report_errors;
}
}

View File

@ -42,7 +42,7 @@ class Director implements TemplateGlobalProvider {
*/
static function addRules($priority, $rules) {
if ($priority != 100) {
Deprecation::notice('3.0', 'Priority argument is now ignored - use the default of 100. You should really be setting routes via _config yaml fragments though.');
Deprecation::notice('3.0', 'Priority argument is now ignored - use the default of 100. You should really be setting routes via _config yaml fragments though.', Deprecation::SCOPE_GLOBAL);
}
Config::inst()->update('Director', 'rules', $rules);
@ -134,15 +134,7 @@ class Director implements TemplateGlobalProvider {
$res = Injector::inst()->get('RequestProcessor')->postRequest($req, $response, $model);
if ($res !== false) {
// ?debug_memory=1 will output the number of bytes of memory used for this request
if(isset($_REQUEST['debug_memory']) && $_REQUEST['debug_memory']) {
Debug::message(sprintf(
"Peak memory usage in bytes: %s",
number_format(memory_get_peak_usage(),0)
));
} else {
$response->output();
}
$response->output();
} else {
// @TODO Proper response here.
throw new SS_HTTPResponse_Exception("Invalid response");
@ -485,27 +477,41 @@ class Director implements TemplateGlobalProvider {
}
/**
* Turns an absolute URL or folder into one that's relative to the root of the site.
* This is useful when turning a URL into a filesystem reference, or vice versa.
*
* @todo Implement checking across http/https protocols
* Turns an absolute URL or folder into one that's relative to the root of
* the site. This is useful when turning a URL into a filesystem reference,
* or vice versa.
*
* @param string $url Accepts both a URL or a filesystem path
* @return string Either a relative URL if the checks succeeded, or the original (possibly absolute) URL.
* @return string Either a relative URL if the checks succeeded, or the
* original (possibly absolute) URL.
*/
static function makeRelative($url) {
// Allow for the accidental inclusion of a // in the URL
$url = preg_replace('#([^:])//#', '\\1/', $url);
$url = trim($url);
public static function makeRelative($url) {
// Allow for the accidental inclusion whitespace and // in the URL
$url = trim(preg_replace('#([^:])//#', '\\1/', $url));
$base1 = self::absoluteBaseURL();
$baseDomain = substr($base1, strlen(self::protocol()));
// Only bother comparing the URL to the absolute version if $url looks like a URL.
if(preg_match('/^https?[^:]*:\/\//',$url)) {
$base1 = self::absoluteBaseURL();
if(preg_match('/^https?[^:]*:\/\//',$url,$matches)) {
$urlProtocol = $matches[0];
$urlWithoutProtocol = substr($url, strlen($urlProtocol));
// If we are already looking at baseURL, return '' (substr will return false)
if($url == $base1) return '';
else if(substr($url,0,strlen($base1)) == $base1) return substr($url,strlen($base1));
// Convert http://www.mydomain.com/mysitedir to ''
else if(substr($base1,-1)=="/" && $url == substr($base1,0,-1)) return "";
if($url == $base1) {
return '';
}
else if(substr($url,0,strlen($base1)) == $base1) {
return substr($url,strlen($base1));
}
else if(substr($base1,-1)=="/" && $url == substr($base1,0,-1)) {
// Convert http://www.mydomain.com/mysitedir to ''
return "";
}
if(substr($urlWithoutProtocol,0,strlen($baseDomain)) == $baseDomain) {
return substr($urlWithoutProtocol,strlen($baseDomain));
}
}
// test for base folder, e.g. /var/www
@ -514,7 +520,14 @@ class Director implements TemplateGlobalProvider {
// Test for relative base url, e.g. mywebsite/ if the full URL is http://localhost/mywebsite/
$base3 = self::baseURL();
if(substr($url,0,strlen($base3)) == $base3) return substr($url,strlen($base3));
if(substr($url,0,strlen($base3)) == $base3) {
return substr($url,strlen($base3));
}
// Test for relative base url, e.g mywebsite/ if the full url is localhost/myswebsite
if(substr($url,0,strlen($baseDomain)) == $baseDomain) {
return substr($url, strlen($baseDomain));
}
// Nothing matched, fall back to returning the original URL
return $url;
@ -936,8 +949,4 @@ class Director implements TemplateGlobalProvider {
'BaseHref' => 'absoluteBaseURL', //@deprecated 3.0
);
}
}

View File

@ -228,10 +228,30 @@ class SS_HTTPRequest implements ArrayAccess {
}
/**
* Returns the URL used to generate the page
*
* @param bool $includeGetVars whether or not to include the get parameters\
*
* @return string
*/
function getURL() {
return ($this->getExtension()) ? $this->url . '.' . $this->getExtension() : $this->url;
function getURL($includeGetVars = false) {
$url = ($this->getExtension()) ? $this->url . '.' . $this->getExtension() : $this->url;
if ($includeGetVars) {
// if we don't unset $vars['url'] we end up with /my/url?url=my/url&foo=bar etc
$vars = $this->getVars();
unset($vars['url']);
if (count($vars)) {
$url .= '?' . http_build_query($vars);
}
}
else if(strpos($url, "?") !== false) {
$url = substr($url, 0, strpos($url, "?"));
}
return $url;
}
/**

View File

@ -409,7 +409,8 @@ function increase_memory_limit_to($memoryLimit = -1) {
// Check hard maximums
$max = get_increase_memory_limit_max();
if($max != -1 && translate_memstring($memoryLimit) > translate_memstring($max)) return false;
if($max && $max != -1 && trANSLATE_MEMSTRING($memoryLimit) > translate_memstring($max)) return false;
// Increase the memory limit if it's too low
if($memoryLimit == -1 || translate_memstring($memoryLimit) > translate_memstring($curLimit)) {

View File

@ -332,7 +332,7 @@ abstract class Object {
* @return mixed
*/
public static function get_static($class, $name, $uncached = false) {
Deprecation::notice('3.1.0', 'get_static is deprecated, replaced by Config#get');
Deprecation::notice('3.1.0', 'Replaced by Config#get');
return Config::inst()->get($class, $name, Config::FIRST_SET);
}
@ -344,7 +344,7 @@ abstract class Object {
* @param mixed $value
*/
public static function set_static($class, $name, $value) {
Deprecation::notice('3.1.0', 'set_static is deprecated, replaced by Config#update');
Deprecation::notice('3.1.0', 'Replaced by Config#update');
Config::inst()->update($class, $name, $value);
}
@ -356,7 +356,7 @@ abstract class Object {
* @return mixed
*/
public static function uninherited_static($class, $name, $uncached = false) {
Deprecation::notice('3.1.0', 'uninherited_static is deprecated, replaced by Config#get');
Deprecation::notice('3.1.0', 'Replaced by Config#get');
return Config::inst()->get($class, $name, Config::UNINHERITED);
}
@ -373,7 +373,7 @@ abstract class Object {
public static function combined_static($class, $name, $ceiling = false) {
if ($ceiling) throw new Exception('Ceiling argument to combined_static is no longer supported');
Deprecation::notice('3.1.0', 'combined_static is deprecated, replaced by Config#get');
Deprecation::notice('3.1.0', 'Replaced by Config#get');
return Config::inst()->get($class, $name);
}
@ -385,7 +385,7 @@ abstract class Object {
* @param bool $replace replace existing static vars
*/
public static function addStaticVars($class, $properties, $replace = false) {
Deprecation::notice('3.1.0', 'addStaticVars is deprecated, replaced by Config#update');
Deprecation::notice('3.1.0', 'Replaced by Config#update');
foreach($properties as $prop => $value) self::add_static_var($class, $prop, $value, $replace);
}
@ -406,7 +406,7 @@ abstract class Object {
* @param bool $replace completely replace existing static values
*/
public static function add_static_var($class, $name, $value, $replace = false) {
Deprecation::notice('3.1.0', 'add_static_var is deprecated, replaced by Config#remove and Config#update');
Deprecation::notice('3.1.0', 'Replaced by Config#remove and Config#update');
if ($replace) Config::inst()->remove($class, $name);
Config::inst()->update($class, $name, $value);

View File

@ -172,8 +172,9 @@ class PaginatedList extends SS_ListDecorator {
*/
public function getIterator() {
if($this->limitItems) {
$tmptList = clone $this->list;
return new IteratorIterator(
$this->list->limit($this->pageLength, $this->getPageStart())
$tmptList->limit($this->pageLength, $this->getPageStart())
);
} else {
return new IteratorIterator($this->list);

View File

@ -45,7 +45,7 @@
.cms table.ss-gridfield-table tbody td.col-getTreeTitle span.badge.deletedonlive { color: #636363; border: 1px solid #E49393; background-color: #F2DADB; }
.cms table.ss-gridfield-table tbody td.col-getTreeTitle span.badge.removedfromdraft { color: #636363; border: 1px solid #E49393; background-color: #F2DADB; }
.cms table.ss-gridfield-table tbody td.col-getTreeTitle span.badge.workflow-approval { color: #56660C; border: 1px solid #7C8816; background-color: #DAE79A; }
.cms table.ss-gridfield-table tbody td button { border: none; background: none; margin: 0 0 0 2px; padding: 6px 0; width: auto; text-shadow: none; }
.cms table.ss-gridfield-table tbody td button { border: none; background: none; margin: 0 0 0 2px; padding: 1px 0; width: auto; text-shadow: none; }
.cms table.ss-gridfield-table tbody td button.ui-state-hover { background: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; }
.cms table.ss-gridfield-table tbody td button.ui-state-active { border: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; }
.cms table.ss-gridfield-table tbody td button.gridfield-button-delete { width: 20px; margin: 0; }

33
css/debug.css Normal file
View File

@ -0,0 +1,33 @@
body { background-color: #eee; margin: 0; overflow-x: hidden; padding: 0; font-family: Helvetica,Arial,sans-serif; }
.info { margin: 0 0 6px 0; padding: 18px; background-color: #003050; position: relative; line-height: 24px; color: #fff; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #002137), color-stop(10%, #003050), color-stop(90%, #003050), color-stop(100%, #002137)); background-image: -webkit-linear-gradient(#002137, #003050 10%, #003050 90%, #002137); background-image: -moz-linear-gradient(#002137, #003050 10%, #003050 90%, #002137); background-image: -o-linear-gradient(#002137, #003050 10%, #003050 90%, #002137); background-image: -ms-linear-gradient(#002137, #003050 10%, #003050 90%, #002137); background-image: linear-gradient(#002137, #003050 10%, #003050 90%, #002137); }
.info h1 { margin: 0 0 6px 0; padding: 0 32px 0 0; color: #fff; font-size: 24px; text-shadow: 0 1px #002137; line-height: 30px; background: url(../admin/images/logo_small.png) no-repeat right 3px; }
.info h3 { color: #7da4be; font-size: 16px; line-height: 18px; font-weight: normal; }
.info p { margin: 0; font-size: 14px; color: #fff; }
.info a { color: #fff; font-weight: bold; text-decoration: none; }
.info a:hover, .info a:active { color: #fff; text-decoration: underline; }
.header { margin: 0; border-bottom: 6px solid #ccdef3; height: 23px; background-color: #666673; padding: 4px 0 2px 6px; }
.trace, .build, .options { padding: 6px 12px; }
.trace li, .build li, .options li { font-size: 14px; margin: 6px 0; }
a { color: #666; }
a:hover { color: #222; }
a:active { color: #111; }
p { margin-bottom: 6px; }
pre { margin-bottom: 20px; background-color: #f5f5f5; border: 1px solid #eee; border: 1px solid rgba(0, 0, 0, 0.08); color: #333; padding: 11px; overflow: auto; -webkit-border-radius: 4px; -moz-border-radius: 4px; -ms-border-radius: 4px; -o-border-radius: 4px; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); }
pre span { color: #999; }
pre .error { color: #f00; }
h2 { margin: 0 0 12px 0; }
h3 { margin: 0 0 6px 0; color: #333; font-size: 18px; line-height: 24px; }
ul { margin: 0 0 18px 0; padding: 0 0 0 18px; }
.pass { margin-top: 18px; padding: 2px 20px 2px 40px; color: #006600; background: #E2F9E3; border: 1px solid #8DD38D; border-radius: 4px; }
.fail { margin-top: 18px; padding: 2px 20px 2px 40px; color: #C80700; background: #FFE9E9; border: 1px solid #C80700; border-radius: 4px; }

View File

@ -380,12 +380,15 @@ class Debug {
*/
static function showError($errno, $errstr, $errfile, $errline, $errcontext, $errtype) {
if(!headers_sent()) {
$errText = "$errtype: \"$errstr\" at line $errline of $errfile";
$errText = "$errtype at line $errline of $errfile";
$errText = str_replace(array("\n","\r")," ",$errText);
if(!headers_sent()) header($_SERVER['SERVER_PROTOCOL'] . " 500 $errText");
// if error is displayed through ajax with CliDebugView, use plaintext output
if(Director::is_ajax()) header('Content-Type: text/plain');
if(Director::is_ajax()) {
header('Content-Type: text/plain');
}
}
// Legacy error handling for customized prototype.js Ajax.Base.responseIsSuccess()

View File

@ -90,21 +90,16 @@ class DebugView extends Object {
ENT_COMPAT,
'UTF-8'
);
$debugCSS = Controller::join_links(
Director::absoluteBaseURL(),
FRAMEWORK_DIR,
'css/debug.css'
);
echo '<!DOCTYPE html><html><head><title>' . $url . '</title>';
echo '<style type="text/css">';
echo 'body { background-color:#eee; margin:0; padding:0; font-family:Helvetica,Arial,sans-serif; }';
echo '.info { margin:15px 0 0 0; padding:6px 12px; }';
echo '.info h1 { margin:0; padding:0; color:#333; letter-spacing:-2px; }';
echo '.header { margin:0; border-bottom:6px solid #ccdef3; height:23px; background-color:#666673; padding:4px 0 2px 6px; background-image:url('.Director::absoluteBaseURL().'cms/images/mainmenu/top-bg.gif); }';
echo '.trace { padding:6px 12px; }';
echo '.trace li { font-size:14px; margin:6px 0; }';
echo 'pre { margin-left:18px; }';
echo 'pre span { color:#999;}';
echo 'pre .error { color:#f00; }';
echo '.pass { margin-top:18px; padding:2px 20px 2px 40px; color:#006600; background:#E2F9E3 url('.Director::absoluteBaseURL() .'cms/images/alert-good.gif) no-repeat scroll 7px 50%; border:1px solid #8DD38D; border-radius:4px; }';
echo '.fail { margin-top:18px; padding:2px 20px 2px 40px; color:#C80700; background:#FFE9E9 url('.Director::absoluteBaseURL() .'cms/images/alert-bad.gif) no-repeat scroll 7px 50%; border:1px solid #C80700; border-radius:4px; }';
echo '.failure span { color:#C80700; font-weight:bold; }';
echo '</style></head>';
echo '<link rel="stylesheet" type="text/css" href="'. $debugCSS .'" />';
echo '</head>';
echo '<body>';
}

View File

@ -32,6 +32,10 @@
*/
class Deprecation {
const SCOPE_METHOD = 1;
const SCOPE_CLASS = 2;
const SCOPE_GLOBAL = 4;
/**
*
* @var string
@ -119,9 +123,10 @@ class Deprecation {
* @static
* @param $string - The notice to raise
* @param $atVersion - The version at which this notice should start being raised
* @param Boolean $scope - Notice relates to the method or class context its called in.
* @return void
*/
public static function notice($atVersion, $string = '') {
public static function notice($atVersion, $string = '', $scope = Deprecation::SCOPE_METHOD) {
// Never raise deprecation notices in a live environment
if(Director::isLive()) return;
@ -139,9 +144,16 @@ class Deprecation {
// Check the version against the notice version
if ($checkVersion && version_compare($checkVersion, $atVersion, '>=')) {
// Get the calling method
if (!$backtrace) $backtrace = debug_backtrace(0);
$caller = self::get_called_method_from_trace($backtrace);
// Get the calling scope
if($scope == Deprecation::SCOPE_METHOD) {
if (!$backtrace) $backtrace = debug_backtrace(0);
$caller = self::get_called_method_from_trace($backtrace);
} elseif($scope == Deprecation::SCOPE_CLASS) {
if (!$backtrace) $backtrace = debug_backtrace(0);
$caller = isset($backtrace[1]['class']) ? $backtrace[1]['class'] : '(unknown)';
} else {
$caller = false;
}
// Get the level to raise the notice as
$level = self::$notice_level;
@ -152,7 +164,12 @@ class Deprecation {
$string .= " Called from " . self::get_called_method_from_trace($backtrace, 2) . '.';
user_error($caller.' is deprecated.'.($string ? ' '.$string : ''), $level);
if($caller) {
user_error($caller.' is deprecated.'.($string ? ' '.$string : ''), $level);
} else {
user_error($string, $level);
}
}
}

View File

@ -97,8 +97,10 @@ class DevelopmentAdmin extends Controller {
$base = Director::baseURL();
echo '<div class="options"><ul>';
$evenOdd = "odd";
foreach($actions as $action => $description) {
echo "<li><a href=\"{$base}dev/$action\"><b>/dev/$action:</b> $description</a></li>\n";
echo "<li class=\"$evenOdd\"><a href=\"{$base}dev/$action\"><b>/dev/$action:</b> $description</a></li>\n";
$evenOdd = ($evenOdd == "odd") ? "even" : "odd";
}
$renderer->writeFooter();
@ -134,8 +136,7 @@ class DevelopmentAdmin extends Controller {
$renderer = DebugView::create();
$renderer->writeHeader();
$renderer->writeInfo("Environment Builder", Director::absoluteBaseURL());
echo "<div style=\"margin: 0 2em\">";
echo "<div class=\"status pending\"><h2 class='buildProgress'>Database is building.... Check below for any errors</h2><h2 class='buildCompleted'>Database has been built successfully</h2></div>";
echo "<div class=\"build\">";
$da = DatabaseAdmin::create();
return $da->handleRequest($request, $this->model);

View File

@ -58,6 +58,7 @@ class SS_LogFileWriter extends Zend_Log_Writer_Abstract {
$this->setFormatter($formatter);
}
$message = $this->_formatter->format($event);
if(!file_exists(dirname($this->path))) mkdir(dirname($this->path), 0755, true);
error_log($message, $this->messageType, $this->path, $this->extraHeaders);
}

View File

@ -10,6 +10,8 @@
/**
* Execution time profiler.
*
* @deprecated 3.1 The Profiler class is deprecated, use third party tools like XHProf instead
*
* @package framework
* @subpackage misc
*/
@ -51,6 +53,7 @@ class Profiler {
// Public Methods
static function init() {
Deprecation::notice('3.1', 'The Profiler class is deprecated, use third party tools like XHProf instead');
if(!self::$inst) self::$inst = new Profiler(true,true);
}

View File

@ -39,14 +39,15 @@ class TaskRunner extends Controller {
$renderer->writeInfo("SilverStripe Development Tools: Tasks", Director::absoluteBaseURL());
$base = Director::absoluteBaseURL();
echo "<div class=\"options\">";
echo "<ul>";
foreach($tasks as $task) {
echo "<li>";
echo "<li><p>";
echo "<a href=\"{$base}dev/tasks/" . $task['class'] . "\">" . $task['title'] . "</a><br />";
echo "<span class=\"description\">" . $task['description'] . "</span>";
echo "</li>\n";
echo "</p></li>\n";
}
echo "</ul>";
echo "</ul></div>";
$renderer->writeFooter();
// CLI mode

View File

@ -106,6 +106,14 @@ class TestRunner extends Controller {
if(!PhpUnitWrapper::has_php_unit()) {
die("Please install PHPUnit using pear");
}
if(!isset($_GET['flush']) || !$_GET['flush']) {
Debug::message(
"WARNING: Manifest not flushed. " .
"Add flush=1 as an argument to discover new classes or files.\n",
false
);
}
}
public function Link() {

View File

@ -85,18 +85,29 @@ class YamlFixture extends Object {
*/
protected $fixtureDictionary;
/**
* String containing fixture
*
* @var String
*/
protected $fixtureString;
/**
* @param String Absolute file path, or relative path to {@link Director::baseFolder()}
*/
function __construct($fixtureFile) {
if(!Director::is_absolute($fixtureFile)) $fixtureFile = Director::baseFolder().'/'. $fixtureFile;
function __construct($fixture) {
if(false !== strpos($fixture, "\n")) {
$this->fixtureString = $fixture;
} else {
if(!Director::is_absolute($fixture)) $fixture = Director::baseFolder().'/'. $fixture;
if(!file_exists($fixtureFile)) {
throw new InvalidArgumentException('YamlFixture::__construct(): Fixture path "' . $fixtureFile . '" not found');
if(!file_exists($fixture)) {
throw new InvalidArgumentException('YamlFixture::__construct(): Fixture path "' . $fixture . '" not found');
}
$this->fixtureFile = $fixture;
}
$this->fixtureFile = $fixtureFile;
parent::__construct();
}
@ -107,6 +118,13 @@ class YamlFixture extends Object {
return $this->fixtureFile;
}
/**
* @return String Fixture string
*/
function getFixtureString() {
return $this->fixtureString;
}
/**
* Get the ID of an object from the fixture.
* @param $className The data class, as specified in your fixture file. Parent classes won't work
@ -162,7 +180,11 @@ class YamlFixture extends Object {
DataObject::set_validation_enabled(false);
$parser = new Spyc();
$fixtureContent = $parser->loadFile($this->fixtureFile);
if (isset($this->fixtureString)) {
$fixtureContent = $parser->load($this->fixtureString);
} else {
$fixtureContent = $parser->loadFile($this->fixtureFile);
}
$this->fixtureDictionary = array();
foreach($fixtureContent as $dataClass => $items) {
@ -228,12 +250,12 @@ class YamlFixture extends Object {
}
}
$obj->write();
//If LastEdited was set in the fixture, set it here
if (array_key_exists('LastEdited', $fields)) {
$manip = array($dataClass => array("command" => "update", "id" => $obj->id,
"fields" => array("LastEdited" => "'".$this->parseFixtureVal($fields['LastEdited'])."'")));
DB::manipulate($manip);
}
//If LastEdited was set in the fixture, set it here
if (array_key_exists('LastEdited', $fields)) {
$manip = array($dataClass => array("command" => "update", "id" => $obj->id,
"fields" => array("LastEdited" => "'".$this->parseFixtureVal($fields['LastEdited'])."'")));
DB::manipulate($manip);
}
}
}

View File

@ -16,8 +16,10 @@
// speed up mysql_connect timeout if the server can't be found
ini_set('mysql.connect_timeout', 5);
// Don't die half was through installation; that does more harm than good
ini_set('max_execution_time', 0);
// Prevent a white-screen-of-death
ini_set('display_errors', 'on');
error_reporting(E_ALL | E_STRICT);

View File

@ -1,33 +1,104 @@
# 3.0.0 (unreleased) #
# 3.0.0 (Released 28 June 2012) #
## Overview ##
* New template engine
* New CMS interface design
* Image/Link insertion moved into a modal dialog instead of a sidebar
### CMS
* New CMS interface design more geared towards complex content solutions
* List view for pages (sortable and filterable)
* More powerful media and link insertion (including auto-embedding of external sources)
* Batch actions on site tree moved to an "Edit Tree" view
* "Add pages" dropdown now an "Add new" button which goes to a more descriptive page
* Renaming of sapphire to SilverStripe framework
* FormField classes now have their own HTML templates
* Allow usage of SilverStripe framework without the "cms" module
* "Add pages" shows a dedicated interface with more info about the page type
* CMS JavaScript moved to [jQuery.entwine](https://github.com/hafriedlander/jquery.entwine)
* CMS stylesheets are generated by SCSS to provide more flexible and robust styling
### Framework
* Renaming of "sapphire" to SilverStripe "framework"
* Allow usage of SilverStripe framework without the "cms" module
* New template engine with more powerful syntax
* New ORM layer with expressive and fluent syntax
* New GridField component to replace ComplexTableField
* FormField classes now have their own HTML templates
* Moved functionality to modules: Widget, RestfulServer, SapphireSoapServer, Translatable, IPRestrictions, PageComment, HomepageForDomain
## Detailed change logs ##
The detailed change logs are broken down by pre-release:
* [3.0.0-rc3](/changelogs/rc/3.0.0-rc3) - 27 June 2012
* [3.0.0-rc2](changelogs/rc/3.0.0-rc2) - 26 June 2012
* [3.0.0-rc1](/changelogs/rc/3.0.0-rc1) - 18 June 2012
* [3.0.0-beta3](/changelogs)/beta/3.0.0-beta3) - 28 May 2012
* [3.0.0-beta2](/changelogs/beta/3.0.0-beta2) - 20 April 2012
* [3.0.0-beta1](/changelogs/beta/3.0.0-beta1) - 12 March 2012
* [3.0.0-alpha2](/changelogs/alpha/3.0.0-alpha2) - 12 January 2012
* [3.0.0-alpha1](/changelogs/alpha/3.0.0-alpha1) - 1 November 2011
* [3.0.0-pr1](/changelogs/pr/3.0.0-pr1) - 2 May 2011
## Upgrading ##
### Common Upgrade Tasks
* Rename foder from `sapphire/`to `framework/`, replace own paths with `FRAMEWORK_DIR` (in PHP) or `$ModulePath(framework)` (in templates). Update paths in `.htaccess` or `web.config` ([more](#sapphire-rename))
* Replace `<% control %>` in your templates with `<% loop %>` and `<% with %>` ([more](/reference/templates-upgrading-guide#control))
* Replace `DataObjectSet` with `DataList` or `ArrayList` ([more](#deprecated-classes))
* Rewrite `ComplexTableField` and `DataObjectManager` instances to `GridField`
* Rewrite `Director::redirect()` and `Director::redirectBack()` calls ([more] (#director-static-functions-deprecated-director-redirect-and-director-redirectback-in-particular)
* Use `<MyModel>::get()` rather than `DataObject::get()` ([more](#new-orm-datalist))
* Use new syntax for `DataObjectDecorator::extraStatics` ([more](#extensions))
* Change CMS tab paths from `Root.Content.Main` to `Root.Main`, move some field changes to new `SiteTree->getSettingsFields()` method ([more](#tab-paths))
* Add new modules if using specific core features like Widget, RestfulServer, PageComment or Translatable
### sapphire renamed to framework {#sapphire-rename}
`sapphire` has been renamed to `framework`.
Please ensure the framework now resides in the new folder when upgrading.
Here's a list of steps to check:
The `sapphire` module has been renamed to `framework`. Please ensure the framework now resides in the new folder when upgrading. Here's a list of steps to check:
* Remove your existing `sapphire` directory, and replace with `framework` from the new SilverStripe 3.0 package
* Rename references of `sapphire` to `framework` in `.htaccess`, `web.config` and `/usr/bin/sake` (the last is only necessary if you use `sake`)
* Find and replace any references to `sapphire` in your custom code to `framework`. In your PHP code, you can use the constant `FRAMEWORK_DIR`,
which points to the framework directory, and in the templates you can use `$ModulePath(framework)`
### GridField: Replacement for TableListField and ComplexTableField [gridfield]###
We have a new component for managing lists of objects: The `[GridField](/topics/grid-field)`.
It's a substantial rewrite of the features previously captured by `TableListField`,
`ComplexTableField`, `HasManyComplexTableField` and `ManyManyComplexTableField`.
The legacy fields remain operational for now, although a switch to `GridField` is strongly encouraged,
for stability, interface and performance reasons. The `HasManyComplexTableField` and `ManyManyComplexTableField`
are no longer maintained, for those you do have to make the switch.
The `TableField` class will be deprecated soon, but we don't have an adequate replacement for it yet.
Upgrade example: Record listing
:::php
// before
$field = new TableListField('Companies', 'Company');
$field->setPageSize(20);
// after
$field = new GridField('Companies', null, Company::get());
$field->getConfig()->getComponentByType('GridFieldPaginator')->setItemsPerPage(20);
Upgrade example: Record listing with view/edit interface
:::php
// before
$field = new ComplexTableField($myController, 'Companies', 'Company');
// after
$field = new GridField('Companies', null, Company::get(), GridFieldConfig_RecordEditor::create());
Upgrade example: Relationship editing
:::php
// before
$field = new HasManyComplexTableField($myController, 'MyRelation', 'MyRelationObject');
// after
$field = new GridField('MyRelation', null, $myRecord->MyRelation(), GridFieldConfig_RelationEditor::create());
More information is available in the [GridField documentation](/topics/grid-field).
### Object static functions replaced with new Config class {#new-config}
Static functions for getting a static variable on the `Object` class have been deprecated,
in favour of using the new `Config` class instead.
@ -52,21 +123,17 @@ Note the different options for the third parameter of `get()`:
If you don't set an option, it will get all the values for the static, including inherited ones.
This was previously known as `Object::combined_static()`.
### Director static functions deprecated, Director::redirect() and Director::redirectBack() in particular
### Director static functions deprecated (e.g. redirect() and redirectBack())
`Director::redirect()` and `Director::redirectBack()` are now marked as deprecated.
`Director::redirect()` and `Director::redirectBack()` are now marked as deprecated. If you have a `Controller` instance and need to redirect, call `redirect()` or `redirectBack()` on the instance
instead, e.g. `$controller->redirect()` or `$controller->redirectBack()`. Most of the time, form action handler methods on a controller need only call `$this->redirect()` or `$this->redirectBack()`.
If you have a `Controller` instance and need to redirect, call `redirect()` or `redirectBack()` on the instance
instead, e.g. `$controller->redirect()` or `$controller->redirectBack()`. Most of the time, form action handler
methods on a controller need only call `$this->redirect()` or `$this->redirectBack()`.
Use `Controller::curr()->redirect()` and `Controller::curr()->redirectBack()` if you need to redirect in contexts
where a controller might not be immediately available.
Use `Controller::curr()->redirect()` and `Controller::curr()->redirectBack()` if you need to redirect in contexts where a controller might not be immediately available.
### DataExtension and deprecated extraStatics on extension classes {#extensions}
`DataObjectDecorator` has been renamed to `DataExtension`. Any classes that extend `DataObjectDecorator`
should now extend `DataExtension` instead.
`DataObjectDecorator` has been renamed to `DataExtension`. Please extend this class in case you
have written your own extensions.
`extraStatics()` on extensions is now deprecated.
@ -75,41 +142,36 @@ Instead of using `extraStatics()`, you can simply define static variables on you
If you need custom logic, e.g. checking for a class before applying the statics on the extension,
you can use `add_to_class()` as a replacement to `extraStatics()`.
Given the original `extraStatics` function:
:::php
class MyExtension extends Extension {
<?php
//...
function extraStatics($class, $extensionClass) {
if($class == 'MyClass') {
return array(
'db' => array(
'Title' => 'Varchar'
// before
function extraStatics($class, $extensionClass) {
if($class == 'MyClass') {
return array(
'db' => array(
'Title' => 'Varchar'
);
);
);
}
}
// after
static $db = array(
'Title' => 'Varchar'
);
// advanced syntax
static function add_to_class($class, $extensionClass, $args = null) {
if($class == 'MyClass') {
Config::inst()->update($class, 'db', array(
'Title' => 'Varchar'
));
}
parent::add_to_class($class, $extensionClass, $args);
}
}
This would now become a static function `add_to_class`, and calls `update()` with an array
instead of returning it. It also needs to call `parent::add_to_class()`:
<?php
//...
static function add_to_class($class, $extensionClass, $args = null) {
if($class == 'MyClass') {
Config::inst()->update($class, 'db', array(
'Title' => 'Varchar'
));
}
parent::add_to_class($class, $extensionClass, $args);
}
Alternatively, you can define statics on the extension directly, like this:
<?php
//...
static $db = array(
'Title' => 'Varchar'
);
### New ORM: More flexible and expressive querying via `DataList` {#new-orm-datalist}
@ -152,6 +214,7 @@ for the presence of records, please call the count() method on the `DataList`:
// after
if(!DataObject::get('SiteTree', '"ParentID" = 5')->count()) echo "Page 5 has no children";
Beware that `DataList->remove()` will delete an entry from the database.
See the ["datamodel" documentation](../../topics/datamodel) for more details.
### New ORM: Changes to manipulation of SQL queries {#new-orm-sql-queries}
@ -193,23 +256,10 @@ The abstract `RelationList` class and its implementations `ManyManyList` and `Ha
are replacing the `ComponentSet` API, which is only relevant if you have instanciated these manually.
Relations are retrieved through the same way (e.g. `$myMember->Groups()`).
### Aggregate changes for partial caching in templates ###
`DataObject::Aggregate()` and `DataObject::RelationshipAggregate()` are now deprecated. To replace your deprecated aggregate calls
in PHP code, you should query with something like `Member::get()->max('LastEdited')`, that is, calling the aggregate on the `DataList` directly.
The same concept applies for replacing `RelationshipAggregate()`, just call the aggregate method on the relationship instead,
so something like `Member::get()->Groups()->max('LastEdited')`.
For partial caching in templates, the syntax `<% cached Aggregate(Page).Max(LastEdited) %>` has been deprecated. The new syntax is similar,
except you use `List()` instead of `Aggregate()`, and the aggregate call `Max()` is now lowercase, as in `max()`.
An example of the new syntax is `<% cached List(Page).max(LastEdited) %>`. Check `DataList` class for more aggregate methods to use.
### `SQLQuery` changes ###
`SQLQuery` has been changed so direct access to internal properties `$from`, `$select`, `$orderby` is
now deprecated.
Instead, there are now methods you can call which allow you to get and set SQL clauses instead.
now deprecated. Instead, there are now methods you can call which allow you to get and set SQL clauses instead.
* `$from` getter is `getFrom()` and setters `setFrom()` and `addFrom()`
* `$select` getter is `getSelect()` and setters `setSelect()` and `addSelect()`
@ -221,23 +271,19 @@ Instead, there are now methods you can call which allow you to get and set SQL c
* `$distinct` getter is `getDistinct()` and setter `setDistinct()`
* `$delete` getter is `getDelete()` and setter `setDelete()`
* `$connective` getter is `getConnective()` and settter `setConnective()`
* `innerJoin()` has been renamed to `addInnerJoin()`
* `leftJoin()` has been renamed to `addLeftJoin()`
### TinyMCE upgraded to 3.5 ###
### Aggregate changes for partial caching in templates ###
TinyMCE has been upgraded to version 3.5.
`DataObject::Aggregate()` and `DataObject::RelationshipAggregate()` are now deprecated. To replace your deprecated aggregate calls
in PHP code, you should query with something like `Member::get()->max('LastEdited')`, that is, calling the aggregate on the `DataList` directly.
The same concept applies for replacing `RelationshipAggregate()`, just call the aggregate method on the relationship instead,
so something like `Member::get()->Groups()->max('LastEdited')`.
This change should be transparent to most people upgrading, but if you're using custom plugins for TinyMCE,
please ensure they are still working correctly with the new version.
If you're upgrading from an SS 3.0 beta, TinyMCE HTML source editor and other popups might be blank.
This is caused by the TinyMCE compressor leaving stale cache files in the system temp folder from an earlier
version.
To resolve this problem, simply delete the `{hash}.gz` files within your temp location (defined by `sys_get_temp_dir()` in PHP.)
These cache files will be regenerated next time the CMS is opened.
For partial caching in templates, the syntax `<% cached Aggregate(Page).Max(LastEdited) %>` has been deprecated. The new syntax is similar,
except you use `List()` instead of `Aggregate()`, and the aggregate call `Max()` is now lowercase, as in `max()`.
An example of the new syntax is `<% cached List(Page).max(LastEdited) %>`. Check `DataList` class for more aggregate methods to use.
### InnoDB driver for existing and new tables on MySQL (instead of MyISAM) [innodb]###
@ -251,7 +297,7 @@ unless the `FullTextSearch` feature is enabled. In order to disable this behavio
you have to add the following code to your `_config.php` BEFORE running a `dev/build`:
:::php
DataObject::$create_table_options['MySQLDatabase] = 'ENGINE=MyISAM';
DataObject::$create_table_options['MySQLDatabase'] = 'ENGINE=MyISAM';
As with any SilverStripe upgrade, we recommend database backups before calling `dev/build`.
See [mysql.com](http://dev.mysql.com/doc/refman/5.5/en/converting-tables-to-innodb.html) for details on the conversion.
@ -259,12 +305,9 @@ Note: MySQL has made InnoDB the default engine in its [5.5 release](http://dev.m
### Convert::json2array() changes [raw2json]###
Convert JSON functions have been changed to use built-in json PHP functions `json_decode()` and `json_encode()`
Convert JSON functions have been changed to use built-in json PHP functions `json_decode()` and `json_encode()`.
Because `json_decode()` will convert nested JSON structures to arrays as well, this has changed the way it worked,
as before nested structures would be converted to an object instead.
So, given the following JSON input to `Convert::json2array()`:
as before nested structures would be converted to an object instead. So, given the following JSON input to `Convert::json2array()`:
{"Joe":"Bloggs","Tom":"Jones","My":{"Complicated":"Structure"}}
@ -288,47 +331,6 @@ Now in SilverStripe 3.x, nested structures are arrays:
)
)
### GridField: Replacement for TableListField and ComplexTableField [gridfield]###
We have a new component for managing lists of objects: The `[GridField](/topics/grid-field)`.
It's a substantial rewrite of the features previously captured by `TableListField`,
`ComplexTableField`, `HasManyComplexTableField` and `ManyManyComplexTableField`.
The legacy fields remain operational for now, although a switch to `GridField` is strongly encouraged,
for stability, interface and performance reasons. The `HasManyComplexTableField` and `ManyManyComplexTableField`
are no longer maintained, for those you do have to make the switch.
The `TableField` class will be deprecated soon, but we don't have an adequate replacement for it yet.
Upgrade example: Record listing
:::php
// before
$field = new TableListField('Companies', 'Company');
$field->setPageSize(20);
// after
$field = new GridField('Companies', null, Company::get());
$field->getConfig()->getComponentByType('GridFieldPaginator')->setItemsPerPage(20);
Upgrade example: Record listing with view/edit interface
:::php
// before
$field = new ComplexTableField($myController, 'Companies', 'Company');
// after
$field = new GridField('Companies', null, Company::get(), GridFieldConfig_RecordEditor::create());
Upgrade example: Relationship editing
:::php
// before
$field = new HasManyComplexTableField($myController, 'MyRelation', 'MyRelationObject');
// after
$field = new GridField('MyRelation', null, $myRecord->MyRelation(), GridFieldConfig_RelationEditor::create());
More information is available in the [GridField documentation](/topics/grid-field).
### New template engine [templates]###
The template engine has been completely rewritten, and although it is generally backward compatible, there are new features
@ -362,6 +364,18 @@ The page tree moved from a bespoke tree library to [JSTree](http://jstree.com),
which required changes to markup of the tree and its JavaScript architecture.
This includes changes to `TreeDropdownField` and `TreeMultiSelectField`.
### TinyMCE upgraded to 3.5 ###
TinyMCE has been upgraded to version 3.5.
This change should be transparent to most people upgrading, but if you're using custom plugins for TinyMCE,
please ensure they are still working correctly with the new version.
If you're upgrading from an SS 3.0 beta, TinyMCE HTML source editor and other popups might be blank.
This is caused by the TinyMCE compressor leaving stale cache files in the system temp folder from an earlier
version. To resolve this problem, simply delete the `{hash}.gz` files within your temp location (defined by `sys_get_temp_dir()` in PHP.)
These cache files will be regenerated next time the CMS is opened.
### Settings-related fields move from SiteTree->getCMSFields() to new SiteTree->getSettingsFields() [getcmsfields]###
The fields and tabs are now split into two separate forms, which required a structural
@ -480,10 +494,9 @@ Please use the appropriate setters on the form field instance instead.
### EmailField now uses type "email" instead of type "text" {#email-form-field}
EmailField now uses "email" for the `type` attribute, which integrates better with HTML5 features like
form validation in the browser.
If you want to change this back to "text", use `setAttribute()` when constructing the field:
form validation in the browser. If you want to change this back to "text", use `setAttribute()` when constructing the field:
:::php
$field = new EmailField('Email');
$field->setAttribute('type', 'text');
@ -491,7 +504,6 @@ If you want to change this back to "text", use `setAttribute()` when constructin
In order to make the SilverStripe framework useable without the `cms` module,
we've moved some files around.
CMS base functionality which is not directly related to content pages (`SiteTree`)
has been moved from the `cms` module into a new "sub-module" located in `framework/admin`.
This includes generic management interfaces like "Files & Images" (`AssetAdmin`),
@ -616,7 +628,7 @@ Note that its just necessary if SilverStripe is used in a language other than th
The SS_Report::register() method is deprecated. You no longer need to explicitly register reports. The CMS now
automatically picks up and adds all Report classes to the list of reports in ReportAdmin. You can choose to exclude
certain reports by using the SS_Report::add_excluded_reports() method.
fe
### Removed the ability use a SQLQuery object in a SS_Report
You can no longer create reports that using the deprecated DataObject->buildSQL and DataObject->extendedSQL

View File

@ -0,0 +1,9 @@
# 3.1.0 (unreleased)
## Overview ##
## Upgrading
* Deprecated `Profiler` class, use third-party solutions like [xhprof](https://github.com/facebook/xhprof/)
* Removed defunct or unnecessary debug GET parameters:
`debug_profile`, `debug_memory`, `profile_trace`, `debug_javascript`, `debug_behaviour`

View File

@ -9,7 +9,10 @@ For information on how to upgrade to newer versions consult the [upgrading](/ins
## Stable Releases
* [3.0.0](3.0.0) - unreleased
* [3.0.0](3.0.0) - 28 June 2012
* [2.4.7](2.4.7) - 1 February 2012
* [2.4.6](2.4.6) - 18 October 2011
* [2.4.5](2.4.5) - 2 February 2011
@ -18,10 +21,16 @@ For information on how to upgrade to newer versions consult the [upgrading](/ins
* [2.4.2](2.4.2) - 22 September 2010
* [2.4.1](2.4.1) - 23 July 2010
* [2.4.0](2.4.0)
* [2.3.13](2.3.13) - 1 February 2012
* [2.3.12](2.3.12) - 17 October 2011
* [2.3.11](2.3.11) - 2 February 2011
* [2.3.10](2.3.10) - 21 December 2010
* [2.3.9](2.3.9) - 11 November 2010
* [2.3.8](2.3.8) - 23 July 2010
* [2.3.7](2.3.7) - 18 March 2010
@ -32,26 +41,40 @@ For information on how to upgrade to newer versions consult the [upgrading](/ins
* [2.3.2](2.3.2) - 18 June 2009
* [2.3.1](2.3.1) - 19 March 2009
* [2.3.0](2.3.0) - 23 February 2009
* [2.2.4](2.2.4) - 20 March 2009
* [2.2.3](2.2.3) - ~31 October 2008
* [2.2.2](2.2.2) - 22 May 2008
* [2.2.1](2.2.1) - 21 December 2007
* [2.2.0](2.2.0) - 28 November 2007
* [2.1.1](2.1.1) - 2 November 2007
* [2.1.0](2.1.0) - 2 October 2007
* [2.0.2](2.0.2) - 14 July 2007
* [2.0.1](2.0.1) - 17 April 2007
* 2.0.0 - 3 February 2007 (initial release)
## Alpha/beta/release candidate ##
* [3.0.0-rc1](beta/3.0.0-rc1)
* [3.0.0-rc2](rc/3.0.0-rc3) - 27 June 2012
* [3.0.0-rc2](rc/3.0.0-rc2) - 26 June 2012
* [3.0.0-rc1](rc/3.0.0-rc1) - 18 June 2012
* [3.0.0-beta3](beta/3.0.0-beta3) - 28 May 2012
* [3.0.0-beta2](beta/3.0.0-beta2) - 20 April 2012
* [3.0.0-beta1](beta/3.0.0-beta1) - 12 March 2012
* [3.0.0-alpha2](alpha/3.0.0-alpha2) - 12 January 2012
* [3.0.0-alpha1](alpha/3.0.0-alpha1) - 1 November 2011
* [3.0.0-pr1](pr/3.0.0-pr1) - unreleased
* [3.0.0-pr1](pr/3.0.0-pr1) - 2 May 2011
* [2.4.5-rc1](rc/2.4.5-rc1) - 31 January 2011
* [2.4.4-rc2](rc/2.4.4-rc2) - 20 December 2010
* [2.4.4-rc1](rc/2.4.4-rc1) - 10 December 2010
@ -67,6 +90,9 @@ For information on how to upgrade to newer versions consult the [upgrading](/ins
* [2.4.0-beta2](beta/2.4.0-beta2) - 17 March 2010
* [2.4.0-beta1](beta/2.4.0-beta1) - 29 January 2010
* [2.4.0-alpha1](alpha/2.4.0-alpha1) - 11 November 2009
* [2.3.11-rc1](rc/2.3.11-rc1) - 31 January 2011
* [2.3.10-rc2](rc/2.3.10-rc2) - 20 December 2010
* [2.3.10-rc1](rc/2.3.10-rc1) - 10 December 2010

View File

@ -1,4 +1,4 @@
# 3.0.0-rc1 #
# 3.0.0-rc1 #
## Overview ##

View File

@ -0,0 +1,64 @@
# 3.0.0-rc2 #
## Overview ##
### CMS ###
* Updated translations (from new getlocalization.com source, in new YML format). Big thanks to all the translators!
* Fixed UI inconsistencies around tab display and media insertion
### Framework ###
* Fixed dependency regressions to cms module, allow running module standalone again
* Fixed nested field controller usage, e.g. UploadField inside a GridField
## Upgrading ##
See [3.0.0](/changelogs/3.0.0) for previous details.
## Changelog ##
### Features and Enhancements
* 2012-06-22 [00f66e2](https://github.com/silverstripe/silverstripe-installer/commit/00f66e2) getlocalization build support (Ingo Schommer)
* 2012-06-19 [d82b67c](https://github.com/silverstripe/sapphire/commit/d82b67c) remove dependencies between framework tests and cms module. (Will Rossiter)
### Bugfixes
* 2012-06-24 [119da09](https://github.com/silverstripe/sapphire/commit/119da09) ed DataList filtering and excluding by ID. (Andrew Short)
* 2012-06-22 [682a6a0](https://github.com/silverstripe/sapphire/commit/682a6a0) "Insert media" loading indicator (fixes #7542) (Ingo Schommer)
* 2012-06-22 [5713a37](https://github.com/silverstripe/sapphire/commit/5713a37) Alignment of http label in insert media (Naomi Guyer)
* 2012-06-22 [cffb952](https://github.com/silverstripe/sapphire/commit/cffb952) Tab colour htmleditor in IE7 (Naomi Guyer)
* 2012-06-22 [f3933aa](https://github.com/silverstripe/sapphire/commit/f3933aa) Make entire tab clickable in htmleditor (fixes #7407) (Naomi Guyer)
* 2012-06-22 [8c05f35](https://github.com/silverstripe/silverstripe-cms/commit/8c05f35) Add batch handler status messages (fixes #7427) (Hamish Friedlander)
* 2012-06-22 [0346923](https://github.com/silverstripe/sapphire/commit/0346923) Add batch handler status messages (fixes #7427) (Hamish Friedlander)
* 2012-06-22 [0bea697](https://github.com/silverstripe/sapphire/commit/0bea697) Make themedCSS use {theme}_{module}/css/{name}.css files if they exist (Hamish Friedlander)
* 2012-06-22 [daa226a](https://github.com/silverstripe/sapphire/commit/daa226a) Fix trac ticket #7476 (Hamish Friedlander)
* 2012-06-21 [6503090](https://github.com/silverstripe/sapphire/commit/6503090) Add validation to fix open.silverstripe.org ticket #7494 (Hamish Friedlander)
* 2012-06-20 [c6039ae](https://github.com/silverstripe/sapphire/commit/c6039ae) When updating the tree from EditForm, ensure we only change the text of the tree node for the first .text element, instead of the nested ones. (Sean Harvey)
* 2012-06-20 [d55eb13](https://github.com/silverstripe/silverstripe-cms/commit/d55eb13) Ensure that we only select the first item when updating tree nodes, otherwise we change the title for all nested node's text as well. (Sean Harvey)
* 2012-06-20 [ef11a0d](https://github.com/silverstripe/sapphire/commit/ef11a0d) Fix trac ticket 7081 (Hamish Friedlander)
* 2012-06-18 [42d40a7](https://github.com/silverstripe/sapphire/commit/42d40a7) Turn off filters on IE Nav icons (fixes #7471) (Naomi Guyer)
* 2012-06-18 [50ed4f5](https://github.com/silverstripe/sapphire/commit/50ed4f5) Show text for collapsed side panel in IE8 and 9 (fixes #7469) (Naomi Guyer)
### Minor changes
* 2012-06-25 [fd881d6](https://github.com/silverstripe/sapphire/commit/fd881d6) Add test using a namespaced class for DataList::filter() and DataList::exclude() (Sam Minnee)
* 2012-06-25 [9f7ec96](https://github.com/silverstripe/silverstripe-cms/commit/9f7ec96) Updated translations (Ingo Schommer)
* 2012-06-25 [2a3d387](https://github.com/silverstripe/sapphire/commit/2a3d387) Updated translations (Ingo Schommer)
* 2012-06-22 [17303c6](https://github.com/silverstripe/silverstripe-installer/commit/17303c6) getlocalization API URL (Ingo Schommer)
* 2012-06-22 [acb0e94](https://github.com/silverstripe/silverstripe-cms/commit/acb0e94) Updated translations master (Ingo Schommer)
* 2012-06-22 [86a2ff5](https://github.com/silverstripe/sapphire/commit/86a2ff5) Updated translations master (Ingo Schommer)
* 2012-06-20 [3a7128d](https://github.com/silverstripe/sapphire/commit/3a7128d) Fixed phpdocs (Ingo Schommer)
* 2012-06-20 [211ce61](https://github.com/silverstripe/silverstripe-cms/commit/211ce61) move route to cms module (Will Rossiter)
* 2012-06-19 [facc8ba](https://github.com/silverstripe/silverstripe-cms/commit/facc8ba) restore backlinkcount into cms module. (removed from framework in https://github.com/willrossi/sapphire/commit/266a61221cbf3d128f8aa0248726352dba91c19b) (Will Rossiter)
* 2012-06-19 [8b43780](https://github.com/silverstripe/sapphire/commit/8b43780) remove dependency on RootURLController and show a default Controller template as a failback. (Will Rossiter)
* 2012-06-19 [eb2a042](https://github.com/silverstripe/sapphire/commit/eb2a042) exclude functional tests when running just framework module tests (Will Rossiter)
* 2012-06-18 [0352a91](https://github.com/silverstripe/sapphire/commit/0352a91) Fixed filename glitch in changelog (Sam Minnee)
### Other
* 2012-06-22 [1b57689](https://github.com/silverstripe/sapphire/commit/1b57689) BUG: exclude() clears previously selected filters() (Trac #7529) (Sam Minnee)
* 2012-06-22 [3fbf572](https://github.com/silverstripe/silverstripe-cms/commit/3fbf572) REMOVE: Remove action handler for widgets. (Trac #7174) (Sam Minnee)
* 2012-06-22 [6be8602](https://github.com/silverstripe/silverstripe-cms/commit/6be8602) BUG: Update MenuTitle whenever Title is changed and the value of Title used to be. Bubble the change to update LHS tree. (Trac #7507) (Sam Minnee)
* 2012-06-19 [3eff92a](https://github.com/silverstripe/sapphire/commit/3eff92a) ChangedBUG FIX: IE filter buttons (fixes #7501) (Naomi Guyer)

View File

@ -0,0 +1,25 @@
# 3.0.0-rc3 #
## Overview ##
RC3 introduces a small fixrd to the installer and to some language packs
## Upgrading ##
See [3.0.0](/changelogs/3.0.0) for previous details.
## Changelog ##
### Features and Enhancements
* 2012-06-27 [2598f65](https://github.com/silverstripe/sapphire/commit/2598f65) Enable display_errors = on in the installer to assist with diagnosis. (Sam Minnee)
### Bugfixes
* 2012-06-26 [6ef4f9a](https://github.com/silverstripe/sapphire/commit/6ef4f9a) Fix increase_memory_limit_to() to reduce installation errors. (Sam Minnee)
### Other
* 2012-06-27 [1ca61a5](https://github.com/silverstripe/silverstripe-installer/commit/1ca61a5) Updated 'phing changelog' to work with new commit tags (Ingo Schommer)
* 2012-06-25 [bfa436b](https://github.com/silverstripe/sapphire/commit/bfa436b) Updated translations (Ingo Schommer)
* 2012-06-25 [25ee305](https://github.com/silverstripe/silverstripe-cms/commit/25ee305) Updated translations (Ingo Schommer)

View File

@ -184,9 +184,7 @@ Sample implementation of a custom loader. Assumes a CSV-file in a certain format
$obj->LastName = $parts[1];
}
public static function getTeamByTitle(&$obj, $val, $record) {
$SQL_val = Convert::raw2sql($val);
return DataObject::get_one(
'FootballTeam', "Title = '{$SQL_val}'"
return FootballTeam::get()->filter('Title', $val)->First();
);
}
}

View File

@ -2,7 +2,7 @@
The [api:DataObject::$defaults] array allows you to specify simple static values to be the default value for when a
record is created, but in many situations default values needs to be dynamically calculated. In order to do this, the
[api:DataObjectSet->populateDefaults()] method will need to be overloaded.
`[api:DataObject->populateDefaults()]` method will need to be overloaded.
This method is called whenever a new record is instantiated, and you must be sure to call the method on the parent
object!
@ -13,7 +13,6 @@ A simple example is to set a field to the current date and time:
/**
* Sets the Date field to the current date.
*/
public function populateDefaults() {
$this->Date = date('Y-m-d');
parent::populateDefaults();
@ -27,7 +26,6 @@ methods. For example:
* This method combines the Title of the parent object with the Title of this
* object in the FullTitle field.
*/
public function populateDefaults() {
if($parent = $this->Parent()) {
$this->FullTitle = $parent->Title . ': ' . $this->Title;

View File

@ -85,10 +85,9 @@ Create a new file called `zzz_admin/code/BookmarkedPageExtension.php` and insert
:::php
<?php
class BookmarkedPageExtension extends DataExtension {
public function extraStatics() {
return array('db' => array('IsBookmarked' => 'Boolean'));
}
public function updateCMSFields(&$fields) {
public static $db = array('IsBookmarked' => 'Boolean');
public function updateCMSFields(FieldList $fields) {
$fields->addFieldToTab('Root.Main',
new CheckboxField('IsBookmarked', "Show in CMS bookmarks?")
);
@ -115,7 +114,7 @@ Add the following code to a new file `zzz_admin/code/BookmarkedLeftAndMainExtens
<?php
class BookmarkedPagesLeftAndMainExtension extends LeftAndMainExtension {
public function BookmarkedPages() {
return DataList::create('Page')->where('"IsBookmarked" = 1');
return Page::get()->filter("IsBookmarked", 1);
}
}
@ -130,9 +129,9 @@ and replace it with the following:
:::ss
<ul>
<% control BookmarkedPages %>
<% loop BookmarkedPages %>
<li><a href="admin/page/edit/show/$ID">Edit "$Title"</a></li>
<% end_control %>
<% end_loop %>
</ul>
## Summary

View File

@ -1,15 +1,22 @@
# Grouping Data Object Sets
# Grouping lists of records
The [api:DataObjectSet] class has a number of methods useful for grouping objects by fields. Together with sorting this
can be used to break up long lists of data into more manageable sub-sections.
The [api:SS_List] class is designed to return a flat list of records.
These lists can get quite long, and hard to present on a single list.
[Pagination](/howto/pagination) is one way to solve this problem,
by splitting up the list into multiple pages.
The [api:DataObjectSet->groupBy()] method takes a field name as the single argument, and breaks the set up into a number
of arrays, where each array contains only objects with the same value of that field. The [api:DataObjectSet->GroupedBy()]
method builds on this and returns the same data in a template-friendly format.
In this howto, we present an alternative to pagination:
Grouping a list by various criteria, through the `[api:GroupedList]` class.
This class is a `[api:SS_ListDecorator]`, which means it wraps around a list,
adding new functionality.
It provides a `groupBy()` method, which takes a field name, and breaks up the managed list
into a number of arrays, where each array contains only objects with the same value of that field.
Similarly, the `GroupedBy()` method builds on this and returns the same data in a template-friendly format.
## Grouping Sets By First Letter
This example deals with breaking up a [api:DataObjectSet] into sub-headings by the first letter.
This example deals with breaking up a [api:SS_List] into sub-headings by the first letter.
Let's say you have a set of Module objects, each representing a SilverStripe module, and you want to output a list of
these in alphabetical order, with each letter as a heading; something like the following list:
@ -23,31 +30,27 @@ these in alphabetical order, with each letter as a heading; something like the f
* Database Plumber
* ...
The first step is to set up the basic data model, along with a method that returns the first letter of the title. This
The first step is to set up the basic data model,
along with a method that returns the first letter of the title. This
will be used both for grouping and for the title in the template.
:::php
class Module extends DataObject {
public static $db = array(
'Title' => 'Varchar(255)'
'Title' => 'Text'
);
// ...
/**
* Returns the first letter of the module title, used for grouping.
*
* @return string
*/
public function getTitleFirstLetter() {
return $this->Title[0];
}
}
The next step is to create a method or variable that will contain/return all the Module objects, sorted by title. For
this example this will be a method on the Page class.
The next step is to create a method or variable that will contain/return all the objects,
sorted by title. For this example this will be a method on the `Page` class.
:::php
class Page extends SiteTree {
@ -56,88 +59,89 @@ this example this will be a method on the Page class.
/**
* Returns all modules, sorted by their title.
*
* @return DataObjectSet
* @return GroupedList
*/
public function getModules() {
return DataObject::get('Module', null, '"Title"');
public function getGroupedModules() {
return GroupedList::create(Module::get()->sort('Title'));
}
}
The final step is to render this into a template. The [api:DataObjectSet->GroupedBy()] method breaks up the set into
a number of sets, grouped by the field that is passed as the parameter. In this case, the getTitleFirstLetter method
defined earlier is used to break them up.
The final step is to render this into a template. The `GroupedBy()` method breaks up the set into
a number of sets, grouped by the field that is passed as the parameter.
In this case, the `getTitleFirstLetter()` method defined earlier is used to break them up.
:::ss
// Modules list grouped by TitleFirstLetter
<%-- Modules list grouped by TitleFirstLetter --%>
<h2>Modules</h2>
<% control Modules.GroupedBy(TitleFirstLetter) %>
<% loop GroupedModules.GroupedBy(TitleFirstLetter) %>
<h3>$TitleFirstLetter</h3>
<ul>
<% control Children %>
<% loop Children %>
<li>$Title</li>
<% end_control %>
<% end_loop %>
</ul>
<% end_control %>
<% end_loop %>
## Grouping Sets By Month
Grouping a set by month is a very similar process. The only difference would be to sort the records by month name, and
then create a method on the DataObject that returns the month name, and pass that to the [api:DataObjectSet->GroupedBy()]
call.
Grouping a set by month is a very similar process.
The only difference would be to sort the records by month name, and
then create a method on the DataObject that returns the month name,
and pass that to the [api:GroupedList->GroupedBy()] call.
Again, the first step is to create a method on the class in question that will be displayed in a list. For this example,
a [api:DataObject] called NewsItem will be used. This will have a method which returns the month it was posted in:
We're reusing our example `Module` object,
but grouping by its built-in `Created` property instead,
which is automatically set when the record is first written to the database.
This will have a method which returns the month it was posted in:
:::php
class NewsItem extends DataObject {
public static $db = array(
'Title' => 'Varchar(255)',
'Date' => 'Date'
);
class Module extends DataObject {
// ...
/**
* Returns the month name this news item was posted in.
*
* @return string
*/
public function getMonthPosted() {
return date('F', strtotime($this->Date));
public function getMonthCreated() {
return date('F', strtotime($this->Created));
}
}
The next step is to create a method that will return all the News records that exist, sorted by month name from
January to December. This can be accomplshed by sorting by the Date field:
The next step is to create a method that will return all records that exist,
sorted by month name from January to December. This can be accomplshed by sorting by the `Created` field:
:::php
class Page extends SiteTree {
// ...
/**
* Returns all news items, sorted by the month they were posted
*
* @return DataObjectSet
* @return GroupedList
*/
public function getNewsItems() {
return DataObject::get('NewsItem', null, '"Date"');
public function getGroupedModulesByDate() {
return GroupedList::create(Module::get()->sort('Created'));
}
}
The final step is the render this into the template using the [api:DataObjectSet->GroupedBy()] method.
The final step is the render this into the template using the [api:GroupedList->GroupedBy()] method.
:::ss
// Modules list grouped by the Month Posted
<h2>Modules</h2>
<% control NewsItems.GroupedBy(MonthPosted) %>
<h3>$MonthPosted</h3>
<% loop GroupedModulesByDate.GroupedBy(MonthCreated) %>
<h3>$MonthCreated</h3>
<ul>
<% control Children %>
<li>$Title ($Date.Nice)</li>
<% end_control %>
<% loop Children %>
<li>$Title ($Created.Nice)</li>
<% end_loop %>
</ul>
<% end_control %>
<% end_loop %>
## Related
* [Howto: "Pagination"](/howto/pagination)

View File

@ -8,7 +8,7 @@ the language and functions which are used in the guides.
* [Import CSV Data](csv-import). Build a simple CSV importer using either [api:ModelAdmin] or a custom controller
* [Dynamic Default Fields](dynamic-default-fields). Pre populate a [api:DataObject] with data.
* [Grouping DataObjectSets](grouping-dataobjectsets). Group results in a [api:DataObjectSet] to create sub sections.
* [Grouping Lists](grouping-dataobjectsets). Group results in a [api:SS_List] to create sub sections.
* [PHPUnit Configuration](phpunit-configuration). How to setup your testing environment with PHPUnit
* [Extend the CMS Interface](extend-cms-interface).
* [How to customize CMS Tree](customize-cms-tree).

View File

@ -1,11 +1,11 @@
# Paginating A List
Adding pagination to a `[api:DataList]` or `[DataObjectSet]` is quite simple. All
Adding pagination to a `[api:SS_List]` is quite simple. All
you need to do is wrap the object in a `[api:PaginatedList]` decorator, which takes
care of fetching a sub-set of the total list and presenting it to the template.
In order to create a paginated list, you can create a method on your controller
that first creates a `DataList` that will return all pages, and then wraps it
that first creates a `SS_List` that will return all pages, and then wraps it
in a `[api:PaginatedList]` object. The `PaginatedList` object is also passed the
HTTP request object so it can read the current page information from the
"?start=" GET var.
@ -18,10 +18,13 @@ information.
* Returns a paginated list of all pages in the site.
*/
public function PaginatedPages() {
$pages = DataList::create('Page');
return new PaginatedList($pages, $this->request);
return new PaginatedList(Page::get(), $this->request);
}
Note that the concept of "pages" used in pagination does not necessarily
mean that we're dealing with `Page` classes, its just a term to describe
a sub-collection of the list.
## Setting Up The Template
Now all that remains is to render this list into a template, along with pagination
@ -33,9 +36,9 @@ The first step is to simply list the objects in the template:
:::ss
<ul>
<% control PaginatedPages %>
<% loop PaginatedPages %>
<li><a href="$Link">$Title</a></li>
<% end_control %>
<% end_loop %>
</ul>
By default this will display 10 pages at a time. The next step is to add pagination
@ -46,7 +49,7 @@ controls below this so the user can switch between pages:
<% if PaginatedPages.NotFirstPage %>
<a class="prev" href="$PaginatedPages.PrevLink">Prev</a>
<% end_if %>
<% control PaginatedPages.Pages %>
<% loop PaginatedPages.Pages %>
<% if CurrentBool %>
$PageNum
<% else %>
@ -56,7 +59,7 @@ controls below this so the user can switch between pages:
...
<% end_if %>
<% end_if %>
<% end_control %>
<% end_loop %>
<% if PaginatedPages.NotLastPage %>
<a class="next" href="$PaginatedPages.NextLink">Next</a>
<% end_if %>
@ -72,3 +75,7 @@ list will already contain only the items that you wish to display on the current
page. In this situation the automatic limiting done by `[api:PaginatedList]`
will break the pagination. You can disable automatic limiting using the
`[api:PaginatedList->setLimitItems()]` method when using custom lists.
## Related
* [Howto: "Grouping Lists"](/howto/grouping-dataobjectsets)

View File

@ -1,40 +1,41 @@
# Upgrading
Usually an update or upgrade your SilverStripe installation just means overwriting files and updating your
database-schema. Please see your [upgrade notes and changelogs](/changelogs).
Usually an update or upgrade your SilverStripe installation just means
overwriting files and updating your database-schema.
See our [upgrade notes and changelogs](/changelogs) for release-specific information.
## Process
Never update a website on the live server without trying it on a development copy first.
* Check if any modules (e.g. blog or forum) in your installation are compatible and need to be upgraded as well
* Backup your database
* Backup your website
* Backup your database content
* Backup your webroot files
* Download the new release and uncompress it to a temporary folder
* Leave custom folders like *mysite* or *themes* in place.
* Identify system folders in your webroot (`cms`, `framework` and any additional modules).
* Delete existing system folders (or move them outside of your webroot)
* Extract and replace system folders from your download (Deleting instead of "copying over" existing folders
ensures that files removed from the new SilverStripe release are not persisting in your installation)
* Identify system folders in your webroot (`cms`, `framework`, `sapphire` and any additional modules).
* Delete existing system folders (or move them outside of your webroot)
* Extract and replace system folders from your download (Deleting instead of "copying over" existing folders ensures that files removed from the new SilverStripe release are not persisting in your installation)
* Visit http://yoursite.com/dev/build/?flush=1 to rebuild the website database
* Check if you need to adapt your code to changed PHP APIs
* Check if you have overwritten any core templates or styles which might need an update
* See [common-problems](common-problems) for a list of likely mistakes that could happen during an upgrade.
* Visit http://yoursite.com/dev/build/?flush=1 to rebuild the website Database
* Check if you need to adapt your code to changed APIs
* Check if you need to adapt your code to changed CSS/HTML/JS
<div class="warning" markdown="1">
Never update a website on the live server without trying it on a development copy first.
</div>
* See [common-problems](common-problems) for a list of likely mistakes that could happen during an upgrade.
## Decision Helpers
How easy will it be to update my project? It's a fair question, and sometimes a difficult one to answer. This page is
intended to help you work out how hard it will be to upgrade your site.
How easy will it be to update my project? It's a fair question, and sometimes a difficult one to answer.
* If you've made custom branches of the core, or of a module, it's going to be harder to upgrade.
* The more custom features you have, the harder it will be to upgrade. You will have to re-test all of those features
and some of them may have broken.
* Customisations of a well defined type - such as custom page types or custom blog widgets - are going to be easier to
upgrade than customisations that use sneaky tricks, such as the subsites module.
* "Micro" releases (x.y.z) are explicitly backwards compatible, "minor" and "major" releases can deprecate features and change APIs (see our [/misc/release-process](release process) for details)
* If you've made custom branches of SilverStripe core, or any thirdparty module, it's going to be harder to upgrade.
* The more custom features you have, the harder it will be to upgrade. You will have to re-test all of those features, and adapt to API changes in core.
* Customisations of a well defined type - such as custom page types or custom blog widgets - are going to be easier to upgrade than customisations that modify deep system internals like rewriting SQL queries.
## Related
* [Release Announcements](http://groups.google.com/group/silverstripe-announce/)
* [Blog posts about releases on silverstripe.org](http://silverstripe.org/blog/tag/release)
* [/misc/release-process](Release Process)

View File

@ -403,7 +403,7 @@ Example:
* This method returns something cool. {@link MyParentMethod} has other cool stuff in it.
*
* @param string $colour The colour of cool things that you want
* @return DataObjectSet A list of everything cool
* @return DataList A list of everything cool
*/
public function myMethod($foo) {}
@ -420,7 +420,7 @@ Put code into the classes in the following order (where applicable).
* Commonly used methods like `getCMSFields()`
* Accessor methods (`getMyField()` and `setMyField()`)
* Controller action methods
* Template data-access methods (methods that will be called by a `$MethodName` or `<% control MethodName %>` construct in a template somewhere)
* Template data-access methods (methods that will be called by a `$MethodName` or `<% loop MethodName %>` construct in a template somewhere)
* Object methods
### SQL Format
@ -429,7 +429,7 @@ If you have to use raw SQL, make sure your code works across databases make sure
with the column or table name escaped with double quotes and values with single quotes.
:::php
DataObject::get("MyClass", "\"Title\" = 'my title'");
MyClass::get()->where("\"Title\" = 'my title'");
Use [ANSI SQL](http://en.wikipedia.org/wiki/SQL#Standardization) format where possible.

View File

@ -75,15 +75,17 @@ This ensures commits are easy to browse, and look nice on github.com
(more info about [proper git commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)).
As we automatically generate [changelogs](http://doc.silverstripe.org/sapphire/en/trunk/changelogs/) from them, we need a way to categorize and filter.
Please prefix **all** commit messages with one of the following tags:
Please prefix **noteworthy** commit messages with one of the following tags:
* `API CHANGE`: You've added or modified the functions available to developers writing custom PHP.
* `ENHANCEMENT`: You've added something to the user-visible aspects of SilverStripe.
* `BUGFIX`: You've fixed something that was broken.
* `MINOR` Mark things that are so trivial they're not worth attention by most developers;
specifically, to prevent adding clutter to our automatically generated changelogs.
For example, adding unit tests or documentation would be considered "minor".
Same goes for version control plumbing like merges, file renames or reverts.
* `NEW`: New feature or major enhancement (both for users and developers)
* `API`: Addition of a new API, or modification/removal/deprecation of an existing API.
Includes any change developers should be aware of when upgrading.
* `BUG`: Bugfix or minor enhancement on something developers or users are likely to encounter.
All other commits should not be tagged if they are so trivial that most developers
can ignore them during upgrades or when reviewing changes to the codebase.
For example, adding unit tests or documentation would not be considered "noteworthy".
Same goes for version control plumbing like merges, file renames or reverts.
Further guidelines:

View File

@ -8,7 +8,6 @@ sections.
* [Module release process](module-release-process): Creating and looking after a module
* [Release process](release-process): Describes the Framework and CMS release process
* [SS markdown](ss-markdown): Markdown syntax for our technical documentation
* [Subversion](subversion): Describes SilverStripe-specific information on how to handle subversion
## Feedback

View File

@ -1,175 +0,0 @@
## Subversion
Subversion [website](http://subversion.tigris.org) is a **version control system**.
You can browse our Subversion "code repository" [here](http://open.silverstripe.com/browser), however we now use GIT to
manage our modules (see [Contributing](contributing)).
This page only describes SilverStripe-specific information on how to handle subversion. For a general introduction,
please read the [Official Subversion Book](http://svnbook.red-bean.com/) (available free online).
Start with the ["Basic Usage" chapter](http://svnbook.red-bean.com/en/1.5/svn.tour.html).
### Clients
Make sure you have an updated [subversion client](http://subversion.tigris.org/links.html#clients) installed.
Subversion 1.5 or higher is required for features such as relative externals and merge tracking.
### Checkout / Download
See [Download SilverStripe](http://silverstripe.org/download) and the
["Update your working copy" chapter](http://svnbook.red-bean.com/en/1.5/svn.tour.cycle.html#svn.tour.cycle.update).
### Committing
The SilverStripe core and modules require permission to commit code.
Please have a look at our [contributors guidelines](contributing) to find out how you can gain access.
### Configuring subversion for correct newline handling
You should configure your subversion client to set the ''svn:eol-style'' property to ''native'' on all text files checked into the system. This will ensure that we don't run into troubles, getting non-unix newlines inside our repository.
To do this, edit your ''~/.subversion/config'' file on your development machine, and add the following settings. Note that if you already have ''[miscellany]'' and ''[auto-props]'' sections, you should combine these settings with the existing sections, rather than doing a blind copy & paste to the end of the file.
:::php
[miscellany]
enable-auto-props = yes
// Section for configuring automatic properties.
[auto-props]
*.js = svn:eol-style=native
*.html = svn:eol-style=native
*.inc = svn:eol-style=native
*.css = svn:eol-style=native
*.php = svn:eol-style=native
*.xml = svn:eol-style=native
*.csv = svn:eol-style=native
*.htm = svn:eol-style=native
*.ini = svn:eol-style=native
*.json = svn:eol-style=native
*.php5 = svn:eol-style=native
*.py = svn:eol-style=native
*.ss = svn:eol-style=native
*.yml = svn:eol-style=native
*.yaml = svn:eol-style=native
*.xhtml = svn:eol-style=native
Note that if the repository gets out of whack, the following commands run on a linux box will correct things in the current working copy:
find | grep -v \._ | \
grep "\.\(js\|php\|css\|inc\|html\|html\|php5\|py\|json\|ini\|xml\|csv\)"\$ | \
xargs svn propset svn:eol-style native
find | grep -v \._ | \
grep "\.\(js\|php\|css\|inc\|html\|html\|php5\|py\|json\|ini\|xml\|csv\)"\$ | \
xargs dos2unix
### Feature Branches
For more complicated bugfixes or longer-term development, you may want to create a Feature Branch. For example, you might want
to add support for other rich-text-editors within the CMS - a complex task which can't be contained in a single patch.
Feature branches are a copy of trunk, and usually have a short lifetime in which active development happens.
**The feature branch maintainer is responsible for keeping his branch in sync with trunk and reintegrate when development
is complete.**
More information about ["Feature Branches"](http://svnbook.red-bean.com/en/1.5/svn.branchmerge.commonpatterns.html#svn.branchmerge.commonpatterns.feature),
[merging changes](http://svnbook.red-bean.com/en/1.5/svn.branchmerge.html) and [resolving conflicts](http://svnbook.red-bean.com/en/1.5/svn.tour.cycle.html#svn.tour.cycle.resolve).
#### Example: The 'nestedurls' branch
Example for a feature branch for the ''sapphire'' module called ''nestedurls''.
Creating the branch is a simple matter of running the ''svn cp'' command (see [SVN Book: "Creating a Branch"](http://svnbook.red-bean.com/en/1.5/svn.branchmerge.using.html#svn.branchmerge.using.create)).
svn cp http://svn.silverstripe.com/open/modules/sapphire/trunk http://svn.silverstripe.com/open/modules/sapphire/branches/nestedurls
After creating a feature branch, you commit your changes as usual, but also merge in any new changes from trunk
(see [SVN Book: "Keeping your Branch in Sync"](http://svnbook.red-bean.com/en/1.5/svn.branchmerge.basicmerging.html#svn.branchemerge.basicmerging.stayinsync)).
cd /your/nestedurls/working/copy
svn merge http://svn.silverstripe.com/open/modules/sapphire/trunk
Once you've finished your feature development (usually a timeframe around 4-8 weeks), you "reintegrate" your branch with the
trunk repository. This activity happens only once, and the feature branch has to be removed afterwards.
cd /your/trunk/working/copy
svn merge --reintegrate http://svn.silverstripe.com/open/modules/sapphire/branches/nestedurls
You can get information about the merge process (see
[SVN Book: "Mergeinfo and Previews"](http://svnbook.red-bean.com/en/1.5/svn.branchmerge.basicmerging.html#svn.branchmerge.basicmerging.mergeinfo)).
cd /your/nestedurls/working/copy
# revisions which are you still have to merge from trunk to your branch
svn mergeinfo http://svn.silverstripe.com/open/modules/sapphire/trunk --show-revs eligible
# revisions which are already merged from trunk to your branch
svn mergeinfo http://svn.silverstripe.com/open/modules/sapphire/trunk
### Troubleshooting
#### SVN for your own websites
Here is a step-by-step guide on how to work with SVN on your own site. It oversimplifies some aspects, but it is a good
introduction. NOTE: you will need SSH and SVN installed and available on your server.
* Install LAMP / WAMP and an SVN application (Tortoise SVN on Windows)
* Buy an SVN repository: http://www.SVNrepository.com/ OR set one up on your own server or look at freeby alternatives.
* Go to your SVN repository server and create a new repository - I would recommend to name it after the site you are about to setup, e.g. myfirstsite.com.
* Create / go to web folder on local LAMP/WAMP
* SVN "checkout" your empty repository (e.g. http://sunny.SVNrepository.com/SVN/myaccountname/myfirstsite.com).
* SVN "propedit" SVN:externals, add the following SVN property to the root directory:
SVN:externals (also add your own general modules here, etc... etc... this should be a long list):
cms http://SVN.silverstripe.com/open/modules/cms/branches/2.4
sapphire http://SVN.silverstripe.com/open/modules/sapphire/branches/2.4
* SVN "update" your project. All the various files should be imported now.
* SVN "commit" your externals.
* In the root directory, create the following files: _ss_environment.php.sample, .htacess.sample and "commit" these to your repository.
* Copy in local LAMP / WAMP environment: _ss_environment.php.sample to _ss_environment.php; and .htacess.sample to .htacess
* Make sure that .htaccess and _ss_environment.php files are excluded from any commits. This is another SVN property attached to the root directory:
SVN:ignore:
.htaccess
_ss_environment.php
assets
* Create assets folder in root directory
* Your site should now be ready on your local WAMP / LAMP. You can now create a mysite and themes folder, add all your files, edit, bla bla until your site is ready. Then SVN "add" and SVN "commit" all your files to your repository (usually, you do many commits)
* Once you have created the site locally and committed it to your repository you should SSH to your web server (recommended application for windows is PUTTY)
* Once logged in to your web server, browse to the root web folder
* Make sure SVN is installed on the server (just type SVN help or something to check).
* SSH prompt, type:
SVN checkout http://sunny.SVNrepository.com/SVN/myaccountname/myfirstsite.com .
<div class="hint" markdown="1">
Add a DOT at the end to check out files to current directory
</div>
* You should now have all your files on your webserver
* Copy on the server (using SSH)
cp _ss_environment.php.sample _ss_environment.php
cp .htacess.sample .htacess
* Edit these files: _ss_environment.php.sample , .htacess.sample, using the following SSH commands (if the nano application is not available then try pico):
nano _ss_environment.php.sample
nano .htacess.sample
* Create a folder called assets and make sure permissions are set correctly
* Website should now be up and running. (run dev/build to start).
#### A few point additional points:
* The whole concept of tags and branches have been left out, just to simplify.
* We have also left out the idea of a test and live site. The instructions here can be used to setup a test site. Once you are happy with a test site you can move the code to a live site using the SVN "merge" command. In principle, this is how it works: open SSH and go to the root directory of the live site. Then type:
svn merge http://mysvnrepository/branches/live/ http://mysvnrepository/branches/test/
* If you want to update a repository, but you want the repository on the webserver to be locked to a version then you need set the svn:externals as follows:
cms -r1234567 http://svn.silverstripe.com/open/modules/cms/branches/2.4
where 1234567 is the revision of your repository that you want to use
* If you want to get a better understanding of what is going on in a repository then use the following SVN commands: SVN "status" and SVN "info".
* You can not and should not make any changes to any of the core modules and other modules added with svn:externals
### Related
* [contributing](contributing)
* [release process](release-process)

View File

@ -88,6 +88,23 @@ and merge it back manually to your 'master' as required.
cp build.properties # Add your own getlocalization config to 'build.properties'
phing -Dmodule=<yourmodule> -propertyfile build.properties translations-sync
### Merge back existing translations
Since the latest translations are downloaded into a "translations-staging"
branch, you need to get them back into your main project repository.
This depends on your release strategy: For simpler modules,
just merge back to master:
git checkout master
git merge translations-staging
In case you are maintaining release branches, its a bit more complicated:
The "translations-staging" branch is (correctly) based off master,
but you don't want to merge all other master changes into your release branch.
Use the following task to copy & commit the specific files instead:
phing -Dmodule=<yourmodule> translations-mergeback
### Converting your language files from 2.4 PHP format
The conversion from PHP format to YML is taken care of by a module
@ -107,9 +124,9 @@ Special characters (such as german umlauts) need to be entered in their native f
Currently translated entities are not directly factored into code (for security reasons and release/review-control), so you can't see them straight away.
It is strongly encouraged that you check your translation this way, as its a good way to doublecheck your translation works in the right context.
Please use our [daily-builds](http://www.silverstripe.org/daily-builds/) for your local installation, to ensure you're looking at the most up to date interface.
Please use our [daily-builds](http://www.silverstripe.org/daily-builds/) for your local installation, to ensure you're looking at the most up to date interface. See "Download Translations" above
to find out how to retrieve the latest translation files.
### Can I change a translation just for one SilverStripe version?

View File

@ -1,347 +0,0 @@
# Built-in Page Controls
Ever wonder when you use `$Title` and `<% Control Children %>` what else you can call in the templates?. This page is
here to help with a guide on what template controls you can call.
**Note for advanced users:** These built-in page controls are defined in the [api:SiteTree] classes, which are the
'root' data-object and controller classes for all the sites. So if you're dealing with something that isn't a sub-class
of one of these, our handy reference to 'built-in page controls' won't be so relevant.
## Page controls that can't be nested
These page controls are defined on the **controller** which means they can only be used at a top level, not nested
within another page control.
### Controlling Menus Datafeeds
#### <% control Menu(1) %>, <% control Menu(2) %>, ...
Returns a fixed level menu. Because this only works in the top level, you can't use it for nested menus. Use
`<% control Children %>` instead. You can nest `<% control Children %>`.
#### <% control ChildrenOf(page-url) %>
This will create a datafeed of the children of the given page. Handy if you want a list of the subpages under staff (eg
the staff) on the homepage etc
### Controlling Certain Pages
#### <% control Level(1) %>, <% control Level(2) %>, $Level(1).Title, $Level(2).Content, etc
Returns the current section of the site that we're in, at the level specified by the numbers. For example, imagine
you're on the page __about us > staff > bob marley__:
* `<% control Level(1) %>` would return the about us page
* `<% control Level(2) %>` would return the staff page
* `<% control Level(3) %>` would return the bob marley page
#### <% control Page(my-page) %>$Title<% end_control %>
"Page" will return a single page from the site tree, looking it up by URL. You can use it in the `<% control %>` format.
Can't be called using `$Page(my-page).Title`.
## Page controls that can be used anywhere
These are defined in the data-object and so can be used as nested page controls. Lucky us! we can control Children of
Children of Children for example.
### Conditional Logic
SilverStripe supports a simple set of conditional logic
:::ss
<% if Foo %>
// if Foo is true or an object do this
<% else_if Bar %>
// if Bar is true or an object do this
<% else %>
// then do this by default
<% end_if %>
See more information on conditional logic on [templates](/topics/templates).
### Site wide settings
Since 2.4.0, SilverStripe provides a generic interface for accessing global properties such as *Site name* or *Site tag
line*. This interface is implemented by the [api:SiteConfig] class.
### Controlling Parents and Children
#### <% control Children %>
This will return the children of the current page as a nested datafeed. Useful for nested navigations such as pop-out
menus.
#### <% control AllChildren %>
This will show all children of a page even if the option 'show in menus?' is unchecked in the tab panel behaviour.
#### <% control Parent %> or $Parent.Title, $Parent.Content, etc
This will return the parent page. The $ variable format lets us reference an attribute of the parent page directly.
### Site Navigation - Breadcrumbs
#### $Breadcrumbs
This will return a breadcrumbs widget for the current page. You can call this on any SiteTree descendant, so, for
example, you could display the breadcrumbs of every search result if you wanted. The Breadcrumbs method returns a string
of text, so this can't be used as a control block (that is, you can't usefully say "<% control Breadcrumbs %>"). You can
limit the number of items in the breadcrumbs, as well as whether the breadcrumb items are links.
#### $Breadcrumbs(3)
This returns a maximum of 3 pages in the breadcrumb list, which can be handy if you want to limit the size of your
breadcrumbs to conform to your page design.
#### <% control Breadcrumbs(3, true) %>
This returns the same, but without any links. This is handy if you want to put the breadcrumb list into another link
tag.
### Links and Classes
#### $LinkingMode, $LinkOrCurrent and $LinkOrSection
These return different linking modes. $LinkingMode provides the greatest control, outputting 3 different strings:
* link: Neither this page nor any of its children are current open.
* section: A child of this page is currently open, which means that we're currently in this section of the site.
* current: This page is currently open.
A useful way of using this is in your menus. You can use the following code below to generate class="current" or
class="section" on your links. Take the following code
:::ss
<li><a href="$Link" class="$LinkingMode">$Title</a></li>
When viewed on the Home page it will render like this
:::ss
<li><a href="home/" class="current">Home</a></li>
`$LinkOrCurrent` ignores the section status, returning link instead. `$LinkOrSection` ignores the current status, returning section instead. Both of these options can simplify your CSS when you only have 2 different cases to consider.
#### <% if LinkOrCurrent = current %>
This is an alternative way to set up your menus - if you want different HTML for the current menu item, you can do
something like this:
:::ss
<% if LinkOrCurrent = current %>
<strong>$Title</strong>
<% else %>
<a href="$Link">$Title</a>
<% end_if %>
#### <% if LinkOrSection = section %>
Will return true if you are on the current page OR a child page of the page. Useful for menus which you only want to
show a second level menu when you are on that page or a child of it
#### <% if InSection(page-url) %>
This if block will pass if we're currently on the page-url page or one of its children.
### Titles and CMS Defined Options
#### $MetaTags
This returns a segment of HTML appropriate for putting into the `<head>` tag. It will set up title, keywords and
description meta-tags, based on the CMS content. If you don't want to include the title-tag (for custom templating), use
**$MetaTags(false)**.
#### $MenuTitle
This is the title that you should put into navigation menus. CMS authors can choose to put a different menu title from
the main page title.
#### $Title
This is the title of the page which displays in the browser window and usually is the title of the page.
:::ss
<h1>$Title</h1>
#### $URLSegment
This returns the part of the URL of the page you're currently on. Could be handy to use as an id on your body-tag. (
when doing this, watch out that it doesn't create invalid id-attributes though.). This is useful for adding a class to
the body so you can target certain pages. Watch out for pages named clear or anything you might have used in your CSS
file
:::ss
<body class="$URLSegment">
#### $ClassName
Returns the ClassName of the PHP object. Eg if you have a custom HomePage page type with `$ClassName` in the template, it
will return "HomePage"
#### $BaseHref
Returns the base URL for the current site. This is used to populate the `<base>` tag by default, so if you want to
override `<% base_tag %>` with a specific piece of HTML, you can do something like `<base href="$BaseHref"></base>`
### Controlling Members and Visitors Data
#### <% control CurrentMember %>, <% if CurrentMember %> or $CurrentMember.FirstName
CurrentMember returns the currently logged in member, if there is one. All of their details or any special Member page
controls can be called on this. Alternately, you can use `<% if CurrentMember %>` to detect whether someone has logged
in. To Display a welcome message you can do
:::ss
<% if CurrentMember %>
Welcome Back, $CurrentMember.FirstName
<% end_if %>
If the user is logged in this will print out
:::ss
Welcome Back, Admin
#### <% if IsRepeatMember %>
Detect the visitor's previous experience with the site. `$IsRepeatMember` will return true if the visitor has signed up or logged in on the site before.
Note that as of version 2.4 `$PastVisitor` is deprecated. If you wish to check if a visitor has been to the site before, set a cookie with `Cookie::set()` and test for it with `Cookie::get()`.
Note that in 2.4 this variable was called `$PastMember`. This still works in 3.0 but is deprecated.
### Date and Time
#### $Now.Nice, $Now.Year
`$Now` returns the current date. You can call any of the methods from the [api:Date] class on
it.
#### $Created.Nice, $Created.Ago
`$Created` returns the time the page was created, `$Created.Ago` returns how long ago the page was created. You can also
call any of methods of the [api:Date] class on it.
#### $LastEdited.Nice, $LastEdited.Ago
`$LastEdited `returns the time the page was modified, `$LastEdited.Ago` returns how long ago the page was modified. You
can also call any of methods of the [api:Date] class on it.
### DataObjectSet Options
If you are using a DataObjectSet you have a wide range of methods you can call on it from the templates
#### <% if Even %>, <% if Odd %>, $EvenOdd
These controls can be used to do zebra-striping. `$EvenOdd` will return 'even' or 'odd' as appropriate.
#### <% if First %>, <% if Last %>, <% if Middle %>, $FirstLast
These controls can be used to set up special behaviour for the first and last records of a datafeed. `<% if Middle %>` is
set when neither first not last are set. `$FirstLast` will be 'first', 'last', or ''.
#### $Pos, $TotalItems
`$TotalItems` will return the number of items on this page of the datafeed, and `$Pos` will return a counter starting at 1.
#### $Top
When you're inside a control loop in your template, and want to reference methods on the current controller you're on,
breaking out of the loop to get it, you can use `$Top` to do so. For example:
:::ss
$URLSegment
<% control News %>
$URLSegment <!-- may not return anything, as you're requesting URLSegment on the News objects -->
$Top.URLSegment <!-- returns the same as $URLSegment above -->
<% end_control %>
## Properties of a datafeed itself, rather than one of its items
If we have a control such as `<% control SearchResults %>`, there are some properties, such as `$SearchResults.NextLink`,
that aren't accessible within `<% control SearchResults %>`. These can be used on any datafeed.
### Search Results
#### <% if SearchResults.MoreThanOnePage %>
Returns true when we have a multi-page datafeed, restricted with a limit.
#### $SearchResults.NextLink, $SearchResults.PrevLink
This returns links to the next and previous page in a multi-page datafeed. They will return blank if there's no
appropriate page to go to, so `$PrevLink` will return blank when you're on the first page. You can therefore use
`<% if PrevLink %>` to keep your template tidy.
#### $SearchResults.CurrentPage, $SearchResults.TotalPages
CurrentPage returns the number of the page you're currently on, and TotalPages returns the total number of pages.
#### $SearchResults.TotalItems
This returns the total number of items across all pages.
#### <% control SearchResults.First %>, <% control SearchResults.Last %>
These controls return the first and last item on the current page of the datafeed.
#### <% control SearchResults.Pages %>
This will return another datafeed, listing all of the pages in this datafeed. It will have the following data
available:
* **$PageNum:** page number, starting at 1
* **$Link:** a link straight to that page
* `<% if CurrentBool %>`:** returns true if you're currently on that page
`<% control SearchResults.Pages(30) %>` will show a maximum of 30 pages, useful in situations where you could get 100s of
pages returned.
#### $SearchResults.UL
This is a quick way of generating a `<ul>` containing an `<li>` and `<a>` for each item in the datafeed. Usually too
restricted to use in a final application, but handy for debugging stuff.
## Quick Reference
Below is a list of fields and methods that are typically available for templates (grouped by their source) - use this as
a quick reference (not all of them are described above):
### All methods available in Page_Controller
$NexPageLink, $Link, $RelativeLink, $ChildrenOf, $Page, $Level, $Menu, $Section2, $LoginForm, $SilverStripeNavigator,
$PageComments, $Now, $LinkTo, $AbsoluteLink, $CurrentMember, $PastVisitor, $PastMember, $XML_val, $RAW_val, $SQL_val,
$JS_val, $ATT_val, $First, $Last, $FirstLast, $MiddleString, $Middle, $Even, $Odd, $EvenOdd, $Pos, $TotalItems,
$BaseHref, $Debug, $Top
### All fields available in Page_Controller
$ID, $ClassName, $Created, $LastEdited, $URLSegment, $Title, $MenuTitle, $Content, $MetaTitle, $MetaDescription,
$MetaKeywords, $ShowInMenus, $ShowInSearch, $HomepageForDomain, $ProvideComments, $Sort, $LegacyURL, $HasBrokenFile,
$HasBrokenLink, $Status, $ReportClass, $ParentID, $Version, $EmailTo, $EmailOnSubmit, $SubmitButtonText,
$OnCompleteMessage, $Subscribe, $AllNewsletters, $Subject, $ErrorCode, $LinkedPageID, $RedirectionType, $ExternalURL,
$LinkToID, $VersionID, $CopyContentFromID, $RecordClassName
### All methods available in Page
$Link, $LinkOrCurrent, $LinkOrSection, $LinkingMode, $ElementName, $InSection, $Comments, $Breadcrumbs, $NestedTitle,
$MetaTags, $ContentSource, $MultipleParents, $TreeTitle, $CMSTreeClasses, $Now, $LinkTo, $AbsoluteLink, $CurrentMember,
$PastVisitor, $PastMember, $XML_val, $RAW_val, $SQL_val, $JS_val, $ATT_val, $First, $Last, $FirstLast, $MiddleString,
$Middle, $Even, $Odd, $EvenOdd, $Pos, $TotalItems, $BaseHref, $Top
### All fields available in Page
$ID, $ClassName, $Created, $LastEdited, $URLSegment, $Title, $MenuTitle, $Content, $MetaTitle, $MetaDescription,
$MetaKeywords, $ShowInMenus, $ShowInSearch, $HomepageForDomain, $ProvideComments, $Sort, $LegacyURL, $HasBrokenFile,
$HasBrokenLink, $Status, $ReportClass, $ParentID, $Version, $EmailTo, $EmailOnSubmit, $SubmitButtonText,
$OnCompleteMessage, $Subscribe, $AllNewsletters, $Subject, $ErrorCode, $LinkedPageID, $RedirectionType, $ExternalURL,
$LinkToID, $VersionID, $CopyContentFromID, $RecordClassName

View File

@ -2,6 +2,10 @@
## Introduction
<div class="warning" markdown="1">
This field is deprecated in favour of the new [GridField](/topics/grid-field) API.
</div>
Shows a group of DataObjects as a (readonly) tabular list (similiar to `[api:TableListField]`.)
You can specify limits and filters for the resultset by customizing query-settings (mostly the ID-field on the other
@ -129,22 +133,3 @@ Most of the time, you need to override the following methods:
* ComplexTableField->sourceItems() - querying
* ComplexTableField->DetailForm() - form output
* ComplexTableField_Popup->saveComplexTableField() - saving
### Examples
* `[api:AssetTableField]`
* `[api:MemberTableField]`
## API Documentation
`[api:ComplexTableField]`
## Todo
* Find a less fragile solution for accessing this field through the main controller and ReferencedField, e.g. build a
seperate CTF-instance (doesn't necessarly have to be connected to the original by ReferencedField)
* Control width/height of popup by constructor (hardcoded at the moment)
* Integrate search from MemberTableField.php directly on `[api:ComplexTableField]`
* Less performance-hungry implementation of detail-view paging (don't return all items on a single view)
* Use automatic has-many and many-many functions to return a ComponentSet rather than building the join manually
* Javascript/Ajax-Sorting (see [http://www.activewidgets.com/grid/](http://www.activewidgets.com/grid/) and [http://openrico.org/rico/livegrid.page](http://openrico.org/rico/livegrid.page))

View File

@ -45,56 +45,42 @@ For example above we want to override Member with a Custom Member so we would wr
### Adding extra database fields
Extra database fields can be added with a extension by defining an **extraStatics()** method. These will be added to the table of the base object - the extension will actually edit the $db, $has_one, etc static variables on load.
Extra database fields can be added with a extension in the same manner as if they
were placed on the `DataObject` class they're applied to. These will be added to the table of the base object - the extension will actually edit the $db, $has_one, etc static variables on load.
The function should return a map where the keys are the names of the static variables to update:
:::php
class CustomMember extends DataExtension {
public function extraStatics() {
return array(
'db' => array(
'AvatarURL' => 'Varchar',
),
'has_one' => array(
'RelatedMember' => 'Member',
),
);
}
static $db = array(
'AvatarURL' => 'Varchar',
);
static $has_one = array(
'RelatedMember' => 'Member',
);
}
*NOTE*
If you want to add has_one or db items to a particular class, then that class **must** have that static variable
explicitly defined, even if it's just a blank array. For example, the extension method above wouldn't work if you added
to a class that didn't have static $has_one explicitly declared on the object. This is because of PHP's crappy support
for statics.
### Modifying CMS Fields
The member class demonstrates an extension that allows you to update the default CMS fields for an object in a
extension:
The member class demonstrates an extension that allows you to update the default CMS fields for an
object in an extension:
:::php
public function getCMSFields() {
...
$this->extend('updateCMSFields', $fields);
return $fields;
// ...
$this->extend('updateCMSFields', $fields);
return $fields;
}
The $fields parameter is passed by reference, as it is an object.
The `$`fields parameter is passed by reference, as it is an object.
:::php
public function updateCMSFields(FieldList $fields) {
$fields->push(new TextField('Position', 'Position Title'));
$fields->push(new UploadField('Image', 'Profile Image'));
$fields->push(new TextField('Position', 'Position Title'));
$fields->push(new UploadField('Image', 'Profile Image'));
}
### Custom database generation
Some extensions are designed to transparently add more sophisticated data-collection capabilities to your data object.

View File

@ -2,53 +2,67 @@
## Introduction
A single database record & abstract class for the data-access-model.
The `[api:DataObject]` class represents a single row in a database table,
following the ["Active Record"](http://en.wikipedia.org/wiki/Active_record_pattern) design pattern.
## Usage
## Defining Properties
* [datamodel](/topics/datamodel): The basic pricinples
* [data-types](/topics/data-types): Casting and special property-parsing
* `[api:DataObject]`: A "container" for DataObjects
Properties defined through `DataObject::$db` map to table columns,
and can be declared as different [data-types](/topics/data-types).
## Basics
## Loading and Saving Records
The call to `DataObject->getCMSFields()` is the centerpiece of every data administration interface in SilverStripe,
which returns a `[api:FieldList]`''.
The basic principles around data persistence and querying for objects
is explained in the ["datamodel" topic](/topics/datamodel).
## Defining Form Fields
In addition to defining how data is persisted, the class can also
help with editing it by providing form fields through `DataObject->getCMSFields()`.
The resulting `[api:FieldList]` is the centrepiece of many data administration interfaces in SilverStripe.
Many customizations of the SilverStripe CMS interface start here,
by adding, removing or configuring fields.
Example getCMSFields implementation
:::php
class MyPage extends Page {
class MyDataObject extends DataObject {
$db = array(
'IsActive' => 'Boolean'
);
public function getCMSFields() {
return new FieldList(
new CheckboxField('IsActive')
);
}
}
There's various [form field types](/references/form-field-types), for editing text, dates,
restricting input to numbers, and much more.
## Scaffolding Form Fields
The ORM already has a lot of information about the data represented by a `DataObject`
through its `$db` property, so why not use it to create form fields as well?
If you call the parent implementation, the class will use `[api:FormScaffolder]`
to provide reasonable defaults based on the property type (e.g. a checkbox field for booleans).
You can then further customize those fields as required.
:::php
class MyDataObject extends DataObject {
// ...
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
$fields->fieldByName('IsActive')->setTitle('Is active?');
return $fields;
}
}
The `[ModelAdmin](/reference/modeladmin)` class uses this approach to provide
data management interfaces with very little custom coding.
## Scaffolding Formfields
These calls retrieve a `[api:FieldList]` for the area where you intend to work with the scaffolded form.
### For the CMS
:::php
$fields = singleton('MyDataObject')->getCMSFields();
### For the Frontend
Used for simple frontend forms without relation editing or `[api:TabSet] behaviour. Uses `scaffoldFormFields()` by
default. To customize, either overload this method in your subclass, or extend it by `DataExtension->updateFormFields()`.
:::php
$fields = singleton('MyDataObject')->getFrontEndFields();
## Customizing Scaffolded Fields
This section covers how to enhance the default scaffolded form fields from above. It is particularly useful when used
in conjunction with the `[api:ModelAdmin]` in the CMS to make relevant data administration interfaces.
You can also alter the fields of built-in and module `DataObject` classes through
your own `[DataExtension](/reference/dataextension)`, and a call to `[api:DataExtension->updateCMSFields()]`.
### Searchable Fields

View File

@ -1,98 +0,0 @@
# DataObjectSet
## Introduction
This class represents a set of `[api:DataObject]`s, such as the results of a query. It is the base for all
[datamodel](/topics/datamodel)-related querying. It implements the [Iterator
interface](http://php.net/manual/en/language.oop5.iterations.php) introduced in PHP5.
Relations (`has_many`/`many_many`) are described in `[api:ComponentSet]`, a subclass of `[api:DataObjectSet]`.
## Usage
### Getting the size
:::php
$mySet->Count();
### Getting an single element
:::php
$myFirstDataObject = $mySet->First();
$myLastDataObject = $mySet->Last();
### Getting multiple elements
:::php
$mySpecialDataObjects = $mySet->find('Status', 'special');
$startingFromTen = $mySet->getOffset(10);
$tenToTwenty = $mySet->getRange(10, 10);
### Getting one property
:::php
$myIDArray = $mySet->column('ID');
### Grouping
You can group a set by a specific column. Consider using `[api:SQLQuery]` with a *GROUP BY* statement for enhanced
performance.
:::php
$groupedSet = $mySet->groupBy('Lastname');
### Sorting
Sort a set by a specific column.
:::php
$mySet->sort('Lastname'); //ascending
$mySet->sort('Lastname', 'DESC'); //descending
This works on the object itself, so do NOT do something like this:
:::php
$sortedSet = $mySet->sort('Lastname'); //ascending
## Merge with other `[api:DataObjectSet]`s
:::php
$myFirstSet->merge($mySecondSet);
// $myFirstSet now contains all combined values
### Mapping for Dropdowns
When using `[api:DropdownField]` and its numerous subclasses to select a value from a set, you can easily map
the records to a compatible array:
:::php
$map = $mySet->toDropDownMap('ID', 'Title');
$dropdownField = new DropdownField('myField', 'my label', $map);
### Converting to array
:::php
$myArray = $mySet->toArray();
### Checking for existence
It is good practice to check for empty sets before doing any iteration.
:::php
$mySet = DataObject::get('Players');
if($mySet->exists()) foreach($mySet as $player)
### Paging
`[api:DataObject]`s have native support for dealing with **pagination**.
See *setPageLimits*, *setPageLength*, etc.
FIXME Complete pagination documentation
## API Documentation
`[api:DataObjectSet]`

View File

@ -2,76 +2,80 @@
## Introduction
`[api:Director]` is the first step in the "execution pipeline". It parses the URL, matching it to one of a number of patterns,
and determines the controller, action and any argument to be used. It then runs the controller, which will finally run
the viewer and/or perform processing steps.
## Best Practices
* Checking for an Ajax-Request: Use Director::is_ajax() instead of checking for $_REQUEST['ajax'].
## Redirection
The `[api:Director]` class has a number of methods to facilitate 301 and 302 HTTP redirection.
* **Director::redirect("action-name")**: If there's no slash in the URL passed to redirect, then it is assumed that you
want to go to a different action on the current controller.
* **Director::redirect("relative/url")**: If there is a slash in the URL, it's taken to be a normal URL. Relative URLs
will are assumed to be relative to the site-root; so Director::redirect("home/") will work no matter what the current
URL is.
* **Director::redirect("http://www.absoluteurl.com")**: Of course, you can pass redirect() absolute URL s too.
* **Director::redirectPerm("any-url")**: redirectPerm takes the same arguments as redirect, but it will send a 301
(permanent) instead of a 302 (temporary) header. It improves search rankings, so this should be used whenever the
following two conditions are true:
* Nothing happens server-side prior to the redirection
* The redirection will always occur
* **Director::redirectBack()**: This will return you to the previous page. There's no permanent version of
redirectBack().
`[api:Director]` is the first step in the "execution pipeline". It parses the
URL, matching it to one of a number of patterns, and determines the controller,
action and any argument to be used. It then runs the controller, which will
finally run the viewer and/or perform processing steps.
## Request processing
The `[api:Director]` is the entry point in Silverstring Framework for processing a request. You can read through
the execution steps in `[api:Director]``::direct()`, but in short
The `[api:Director]` is the entry point in Silverstring Framework for processing
a request. You can read through the execution steps in `[api:Director]``::direct()`,
but in short
* File uploads are first analysed to remove potentially harmful uploads (this will likely change!)
* File uploads are first analysed to remove potentially harmful uploads (this
will likely change!)
* The `[api:SS_HTTPRequest]` object is created
* The session object is created
* The `[api:Injector]` is first referenced, and asks the registered `[api:RequestProcessor]` to pre-process
the request object. This allows for analysis of the current request, and allow filtering of parameters
etc before any of the core of the application executes
* The `[api:Injector]` is first referenced, and asks the registered `[api:RequestProcessor]`
to pre-process the request object. This allows for analysis of the current
request, and allow filtering of parameters etc before any of the core of the
application executes.
* The request is handled and response checked
* The `[api:RequestProcessor]` is called to post-process the request to allow further filtering before
content is sent to the end user.
* The `[api:RequestProcessor]` is called to post-process the request to allow
further filtering before content is sent to the end user
* The response is output
The framework provides the ability to hook into the request both before and after it is handled to allow
developers to bind in their own custom pre- or post- request logic; see the `[api:RequestFilter]` to see how
this can be used to authenticate the request before the request is handled.
The framework provides the ability to hook into the request both before and
after it is handled to allow developers to bind in their own custom pre- or
post- request logic; see the `[api:RequestFilter]` to see how this can be used
to authenticate the request before the request is handled.
## Custom Rewrite Rules
## Routing
You can influence the way URLs are resolved one of 2 ways
You can influence the way URLs are resolved in the following ways
1. Adding rules to `[api:Director]` in `<yourproject>/_config.php` (See Default Rewrite Rules below for examples)
2. Adding rules in your extended `[api:Controller]` class via the *$url_handlers* static variable
1. Adding rules to `[api:Director]` in `<yourproject>/_config/routes.yml`
2. Adding rules to `[api:Director]` in `<yourproject>/_config.php (deprecated)
3. Adding rules in your extended `[api:Controller]` class via the *$url_handlers*
static variable
See [controller](/topics/controller) for examples and explanations on how the rules get processed for both 1 and 2 above.
* Static redirect for specific URL
:::php
Director::addRules(100, array(
'myPermanentRedirect' => 'redirect:http://www.mysite.com'
));
See [controller](/topics/controller) for examples and explanations on how the
rules get processed for those methods.
## Default Rewrite Rules
### Routing Rules
SilverStripe comes with certain rewrite rules (e.g. for *admin/assets*).
SilverStripe comes with certain rules which map a URI to a `[api:Controller]`
class (e.g. *dev/* -> DevelopmentAdmin). These routes are either stored in
a routes.yml configuration file located a `_config` directory or inside a
`_config.php` file (deprecated).
* [framework/_config.php](https://github.com/silverstripe/sapphire/blob/master/_config.php)
* [cms/_config.php](https://github.com/silverstripe/silverstripe-cms/blob/master/_config.php)
To add your own custom routes for your application create a routes.yml file
in `<yourproject>/_config/routes.yml` with the following format:
:::yaml
---
Name: customroutes
After: framework/routes#coreroutes
---
Director:
rules:
'subscriptions/$Action' : 'SubscriptionController'
The [Controller](/topics/controller) documentation has a wide range of examples
and explanations on how the rules get processed for those methods.
See:
* [framework/_config/routes.yml](https://github.com/silverstripe/sapphire/blob/master/_config/routes.yml)
* [cms/_config/routes.yml](https://github.com/silverstripe/silverstripe-cms/blob/master/_config/routes.yml)
## Best Practices
* Checking for an Ajax-Request: Use Director::is_ajax() instead of checking
for $_REQUEST['ajax'].
## Links

View File

@ -48,17 +48,14 @@ mod_rewrite works.
## main.php
All requests go through main.php, which sets up the environment and then hands control over to Director.
All requests go through `main.`php, which sets up the environment and then hands control over to `Director`.
**See:** The API documentation of `[api:Main]` for information about how main.php processes requests.
## Director and URL patterns
main.php relies on `[api:Director]` to work out which controller should handle this request. `[api:Director]` will instantiate that
controller object and then call `[api:Controller::run()]`.
**See:** The API documentation of `[api:Director]` for information about how Director parses URLs and hands control over to a controller object.
In general, the URL is build up as follows: page/action/ID/otherID - e.g. http://www.mysite.com/mypage/addToCart/12.
In general, the URL is build up as follows: `page/action/ID/otherID` - e.g. http://www.mysite.com/mypage/addToCart/12.
This will add an object with ID 12 to the cart.
When you create a function, you can access the ID like this:
@ -67,7 +64,7 @@ When you create a function, you can access the ID like this:
public function addToCart ($request) {
$param = $r->allParams();
echo "my ID = ".$param["ID"];
$obj = DataObject::get("myProduct", $param["ID"]);
$obj = MyProduct::get()->byID($param["ID"]);
$obj->addNow();
}

View File

@ -1,83 +1,73 @@
# Form Field Types
This is a highlevel overview of available `[apiFormField]` subclasses. An automatically generated list is available through our [API](api:FormField)
This is a highlevel overview of available `[api:apiFormField]` subclasses. An automatically generated list is available through our [API]
## Basic
* `[api:CheckboxField]`: Single checkbox field.
* `[api:DropdownField]`: A `<select>` tag. Can optionally save into has-one relationships.
* `[api:ReadonlyField]`: Read-only field to display a non-editable value with a label.
* `[api:TextareaField]`: Multi-line text field.
* `[api:TextField]`: Multi-line text field.
* `[api:PasswordField]`: Masked input field
## Actions
* `[api:FormAction]`: Button element for forms, both for `<input type="submit">` and `<button>`.
* `[api:ResetFormAction]`: Action that clears all fields on a form.
## Formatted Input
* `[AjaxUniqueTextField](api:AjaxUniqueTextField)`: Text field that automatically checks that the value entered is unique for
the given set of fields in a given set of tables
* `[AutocompleteTextField](api:AutocompleteTextField)`
* `[ConfirmedPasswordField](api:ConfirmedPasswordField)`: Shows two password-fields, and checks for matching passwords.
* `[CreditCardField](api:CreditCardField)`
* `[CurrencyField](api:CurrencyField)`
* `[EmailField](api:EmailField)`
* `[HTMLEditorField](api:HTMLEditorField)`: A WYSIWYG editor field, powered by tinymce.
* `[NumericField](api:NumericField)`: A Single Numeric field extending a typical TextField but with validation.
* `[PasswordField](api:PasswordField)`
* `[UniqueRestrictedTextField](api:UniqueRestrictedTextField)`: Text field that automatically checks that the value entered
is unique for the given set of fields in a given set of tables
* `[UniqueTextField](api:UniqueTextField)`: Text field that automatically checks that the value entered is unique for the
given set of fields in a given set of tables
* `[api:AjaxUniqueTextField]`: Text field that automatically checks that the value entered is unique for the given set of fields in a given set of tables
## Date/Time
* `[DateField](api:DateField)`: Represents a date in a textfield (New Zealand)
* `[DatetimeField](api:DatetimeField)`: Combined date- and time field
* `[TimeField](api:TimeField)`: Represents time in a textfield (New Zealand)
* `[api:ConfirmedPasswordField]`: Two masked input fields, checks for matching passwords.
* `[api:CountryDropdownField]`: A simple extension to dropdown field, pre-configured to list countries.
* `[api:CreditCardField]`: Allows input of credit card numbers via four separate form fields, including generic validation of its numeric values.
* `[api:CurrencyField]`: Text field, validating its input as a currency. Limited to US-centric formats, including a hardcoded currency symbol and decimal separators.
See `[api:MoneyField]` for a more flexible implementation.
* `[api:DateField]`: Represents a date in a single input field, or separated into day, month, and year. Can optionally use a calendar popup.
* `[api:DatetimeField]`: Combined date- and time field
* `[api:EmailField]`: Text input field with validation for correct email format according to RFC 2822.
* `[api:GroupedDropdownField]`: Grouped dropdown, using <optgroup> tags.
* `[api:HTMLEditorField].
* `[api:MoneyField]`: A form field that can save into a `[api:Money]` database field.
* `[api:NumericField]`: Text input field with validation for numeric values.
* `[api:OptionsetField]`: Set of radio buttons designed to emulate a dropdown.
* `[api:PhoneNumberField]`: Field for displaying phone numbers. It separates the number, the area code and optionally the country code and extension.
* `[api:SelectionGroup]`: SelectionGroup represents a number of fields which are selectable by a radio button that appears at the beginning of each item.
* `[api:TimeField]`: Input field with time-specific, localized validation.
## Structure
* `[CompositeField](api:CompositeField)`: Base class for all fields that contain other fields. Uses `<div>` in template, but
* `[api:CompositeField]`: Base class for all fields that contain other fields. Uses `<div>` in template, but
doesn't necessarily have any visible styling.
* `[FieldGroup](api:FieldGroup)`: Same as CompositeField, but has default styling (indentation) attached in CMS-context.
* `[api:FieldList]`: Basic container for sequential fields, or nested fields through CompositeField. Does NOT render a
`<fieldgroup>`.
* `[TabSet](api:TabSet)`
* `[Tab](api:Tab)`
## Actions
* `[api:Form]` for more info
* `[InlineFormAction](api:InlineFormAction)`: Render a button that will act as If you want to add custom behaviour, please
set {inlcudeDefaultJS} to false and work with behaviour.js.
* `[api:Image]`: Action that uses an image instead of a button
* `[InlineFormAction](api:InlineFormAction)`: Prevents placement of a button in the CMS-button-bar.
* `[api:FieldGroup] attached in CMS-context.
* `[api:FieldList]`: Basic container for sequential fields, or nested fields through CompositeField.
* `[api:TabSet]`: Collection of fields which is rendered as separate tabs. Can be nested.
* `[api:Tab]`: A single tab inside a `TabSet`
* `[api:ToggleCompositeField]`: Allows visibility of a group of fields to be toggled.
* `[api:ToggleField]`: ReadonlyField with added toggle-capabilities - will preview the first sentence of the contained text-value, and show the full content by a javascript-switch.
## Files
* `[FileField](api:FileField)`: Simple file upload dialog.
* `[UploadField](api:FileIFrameField)`: File uploads through HTML5 features, including upload progress, preview and relationship management.
* `[api:FileField]`: Simple file upload dialog.
* `[api:UploadField]`: File uploads through HTML5 features, including upload progress, preview and relationship management.
## Relations
* `[ComplexTableField](api:ComplexTableField)`: Provides a tabuar list in your form with view/edit/add/delete links to modify
records with a "has-one"-relationship (in a lightbox-popup).
* `[HasManyComplexTableField](api:HasManyComplexTableField)`
* `[HasOneComplexTableField](api:HasOneComplexTableField)`
* `[LanguageDropdownField](api:LanguageDropdownField)`: An extension to dropdown field, pre-configured to list languages.
Tied into i18n.
* `[ManyManyComplexTableField](api:ManyManyComplexTableField)`
* `[TableField](api:TableField)`
* `[api:TableListField]`
* `[TreeDropdownField](api:TreeDropdownField)`
* `[TreeMultiselectField](api:TreeMultiselectField)`: represents many-many joins using a tree selector shown in a
dropdown-like element
* `[api:WidgetArea]`
* `[GridField](/topics/grid-field)`
* `[api:CheckboxSetField]`: Displays a set of checkboxes as a logical group.
* `[api:TableField]`: In-place editing of tabular data.
* `[api:TreeDropdownField]`: Dropdown-like field that allows you to select an item from a hierarchical AJAX-expandable tree.
* `[api:TreeMultiselectField]`: Represents many-many joins using a tree selector shown in a dropdown-like element
* `[api:GridField](/topics/grid-field)`: Displays a `[api:SS_List]` in a tabular format. Versatile base class which can be configured to allow editing, sorting, etc.
* `[api:ListboxField]`: Multi-line listbox field, through `<select multiple>`.
## Utility
## Dataless/Utility
* `[DatalessField](api:DatalessField)` - Base class for fields which add some HTML to the form but don't submit any data or
* `[api:DatalessField]` - Base class for fields which add some HTML to the form but don't submit any data or
save it to the database
* `[HeaderField](api:HeaderField)`: Renders a simple `<h1>`-`<h6>` header
* `[HiddenField](api:HiddenField)`
* `[LabelField](api:LabelField)`
* `[LiteralField](api:LiteralField)`: Renders arbitrary HTML into a form.
## CMS Field Editor
Please see `[api:HTMLEditorField]` for in-depth documentation about custom forms created through a GUI in the CMS,
as well as the topic documentation about [Rich Text Editing](/topics/rich-text-editing)
* `[api:HeaderField]`: Renders a simple HTML header element.
* `[api:HiddenField]`
* `[api:LabelField]`: Simple label tag. This can be used to add extra text in your forms.
* `[api:LiteralField]`: Renders arbitrary HTML into a form.

View File

@ -72,10 +72,10 @@ You can also create your own functions by extending the image class, for example
public function Exif(){
//http://www.v-nessa.net/2010/08/02/using-php-to-extract-image-exif-data
$image = $this->AbsoluteURL;
$d=new DataObjectSet();
$d=new ArrayList();
$exif = exif_read_data($image, 0, true);
foreach ($exif as $key => $section) {
$a=new DataObjectSet();
$a=new ArrayList();
foreach ($section as $name => $val)
$a->push(new ArrayData(array("Title"=>$name,"Content"=>$val)));
$d->push(new ArrayData(array("Title"=>strtolower($key),"Content"=>$a)));

View File

@ -9,7 +9,6 @@ Reference articles complement our auto-generated [API docs](http://api.silverstr
* [Database Structure](database-structure): Conventions and best practices for database tables and fields
* [DataObject](dataobject): Base class for database records
* [DataExtension](dataextension): A "mixin" system allowing to extend core classes
* [DataObjectSet](dataobjectset): The base collection of database records in the ORM
* [Director](director): Routes URLs and handles HTTP requests
* [Execution Pipeline](execution-pipeline): Detailed look on the way an HTTP request takes through the system
* [Form Field Types](form-field-types): Highlevel overview of field classes
@ -32,8 +31,3 @@ Reference articles complement our auto-generated [API docs](http://api.silverstr
* [TableListField](tablelistfield): View and delete records in the CMS
* [Typography](typography): CSS file to enable WYSIWYG previews in the CMS
* [urlvariabletools](urlvariabletools): Debug and maintenance switches
* [Versioned](versioned): Extension for SiteTree and other classes to store old versions and provide "staging"
## Feedback
If you have a topic you would like covered in these section please ask for it on our [Bug Tracker](http://open.silverstripe.org)

View File

@ -113,9 +113,12 @@ things, you should add appropriate `[api:Permission::checkMember()]` calls to th
}
}
public function extraStatics() {
// Return an array containing keys 'db', 'has_one', 'many_many', 'belongs_many_many',
}
// define additional properties
static $db = array();
static $has_one = array();
static $has_many = array();
static $many_many = array();
static $belongs_many_many = array();
public function somethingElse() {
// You can add any other methods you like, which you can call directly on the member object.

View File

@ -23,7 +23,7 @@ The way you mark a section of the template as being cached is to wrap that secti
Each cache block has a cache key - an unlimited number of comma separated variables (in the same form as `if` and
`control` tag variables) and quoted strings.
`loop`/`with` tag variables) and quoted strings.
Every time the cache key returns a different result, the contents of the block are recalculated. If the cache key is the
same as a previous render, the cached value stored last time is used.
@ -207,7 +207,7 @@ could also write the last example as:
## The important rule
Currently cached blocks can not be contained within if or control blocks. The template engine will throw an error
Currently cached blocks can not be contained within if or loop blocks. The template engine will throw an error
letting you know if you've done this. You can often get around this using aggregates.
Failing example:
@ -215,11 +215,11 @@ Failing example:
:::ss
<% cached LastEdited %>
<% control Children %>
<% loop Children %>
<% cached LastEdited %>
$Name
<% end_cached %>
<% end_control %>
<% end_loop %>
<% end_cached %>
@ -231,9 +231,9 @@ Can be re-written as:
<% cached LastEdited %>
<% cached Children.max(LastEdited) %>
<% control Children %>
<% loop Children %>
$Name
<% end_control %>
<% end_loop %>
<% end_cached %>
<% end_cached %>

View File

@ -167,7 +167,7 @@ Put something like this code in mysite/code/Page.php inside class Page_Controlle
}
Put something like this code in mysite/templates/Layout/HomePage.ss:
Put something like this code in `themes/<your-theme>/templates/Layout/HomePage.ss`:
:::ss
<h3>My Latest Del.icio.us Links</h3>

View File

@ -57,7 +57,7 @@ something like this:
public function LatestUpdates() {
// 10 is the number of pages
return DataObject::get("Page", "", "LastEdited DESC", "", 10);
return Page::get()->sort("LastEdited", "DESC")->limit(10);
}
}

View File

@ -12,7 +12,7 @@ The default output of a `[api:SearchContext]` is either a `[api:SQLQuery]` objec
In case you need multiple contexts, consider namespacing your request parameters by using `FieldList->namespace()` on
the $fields constructor parameter.
`[api:SearchContext]` is mainly used by `[api:ModelAdmin]`, our generic data administration interface. Another
`[api:SearchContext]` is mainly used by `[ModelAdmin](/reference/modeladmin)`, our generic data administration interface. Another
implementation can be found in generic frontend search forms through the [genericviews](http://silverstripe.org/generic-views-module) module.
## Usage
@ -141,9 +141,9 @@ Results.PaginationSummary(4) defines how many pages the search will show in the
:::ss
<% if Results %>
<ul>
<% control Results %>
<li>$Titulo, $Autor</li>
<% end_control %>
<% loop Results %>
<li>$Title, $Autor</li>
<% end_loop %>
</ul>
<% else %>
<p>Sorry, your search query did not return any results.</p>
@ -157,7 +157,7 @@ Results.PaginationSummary(4) defines how many pages the search will show in the
<% end_if %>
<span>
<% control Results.PaginationSummary(4) %>
<% loop Results.PaginationSummary(4) %>
<% if CurrentBool %>
$PageNum
<% else %>
@ -167,7 +167,7 @@ Results.PaginationSummary(4) defines how many pages the search will show in the
&hellip;
<% end_if %>
<% end_if %>
<% end_control %>
<% end_loop %>
</span>
<% if Results.NotLastPage %>
@ -187,6 +187,6 @@ See `[api:SearchFilter]` API Documentation
## Related
* `[api:ModelAdmin]`
* [ModelAdmin](/reference/modeladmin)
* [RestfulServer module](https://github.com/silverstripe/silverstripe-restfulserver)
* [Tutorial: Site Search](/tutorials/4-site-search)

View File

@ -64,7 +64,7 @@ CustomSideReport.php
public function records() {
// the data the report returns all the dataobjects of type Page and sorted by title. See datamodel for more info
return DataObject::get("Page", "", "Title");
return Page::get()->sort("Title");
}
public function fieldsToShow() {

View File

@ -1,4 +1,4 @@
# SiteConfig
# SiteConfig: Global database content
## Introduction
@ -15,9 +15,9 @@ You can access `[api:SiteConfig]` options from any SS template by using the func
// or
<% control SiteConfig %>
<% loop SiteConfig %>
$Title $AnotherField
<% end_control %>
<% end_loop %>
Or if you want to access variables in the PHP you can do
@ -39,13 +39,9 @@ Create a mysite/code/CustomSiteConfig.php file.
class CustomSiteConfig extends DataExtension {
public function extraStatics() {
return array(
'db' => array(
'FooterContent' => 'HTMLText'
)
);
}
static $db = array(
'FooterContent' => 'HTMLText'
);
public function updateCMSFields(FieldList $fields) {
$fields->addFieldToTab("Root.Main", new HTMLEditorField("FooterContent", "Footer Content"));

View File

@ -1,11 +1,13 @@
# Sitetree
## Introduction
Basic data-object representing all pages within the site tree. The omnipresent *Page* class (located in
*mysite/code/Page.php*) is based on this class.
Basic data-object representing all pages within the site tree.
The omnipresent *Page* class (located in `mysite/code/Page.php`) is based on this class.
## Creating, Modifying and Finding Pages
See the ["datamodel" topic](/topics/datamodel).
## Linking
@ -15,6 +17,11 @@ Basic data-object representing all pages within the site tree. The omnipresent *
// right
$mylink = $mypage->Link(); // alternatively: AbsoluteLink(), RelativeLink()
In a nutshell, the nested URLs feature means that your site URLs now reflect the actual parent/child page structure of
your site. The URLs map directly to the chain of parent and child pages. The
below table shows a quick summary of what these changes mean for your site:
![url table](http://silverstripe.org/assets/screenshots/Nested-URLs-Table.png)
## Querying
@ -23,50 +30,76 @@ might consist of more than one *URLSegment*).
:::php
// wrong
$mypage = DataObject::get_one('SiteTree', '"URLSegment" = \'<mylink>\'');
$mypage = SiteTree::get()->filter("URLSegment", '<mylink>')->First();
// right
$mypage = SiteTree::get_by_link('<mylink>');
### Versioning
The `SiteTree` class automatically has an extension applied to it: `[Versioned](api:Versioned)`.
This provides the basis for the CMS to operate on different stages,
and allow authors to save their changes without publishing them to
website visitors straight away.
`Versioned` is a generic extension which can be applied to any `DataObject`,
so most of its functionality is explained in the `["versioning" topic](/topics/versioning)`.
Since `SiteTree` makes heavy use of the extension, it adds some additional
functionality and helpers on top of it.
Permission control:
:::php
class MyPage extends Page {
function canPublish($member = null) {
// return boolean from custom logic
}
function canDeleteFromLive($member = null) {
// return boolean from custom logic
}
}
Stage operations:
* `$page->doUnpublish()`: removes the "Live" record, with additional permission checks,
as well as special logic for VirtualPage and RedirectorPage associations
* `$page->doPublish()`: Inverse of doUnpublish()
* `$page->doRevertToLive()`: Reverts current record to live state (makes sense to save to "draft" stage afterwards)
* `$page->doRestoreToStage()`: Restore the content in the active copy of this SiteTree page to the stage site.
## Nested/Hierarchical URLs
Hierarchy operations (defined on `[api:Hierarchy]`:
In a nutshell, the nested URLs feature means that your site URLs now reflect the actual parent/child page structure of
your site. The URLs map directly to the chain of parent and child pages. The
below table shows a quick summary of what these changes mean for your site:
* `$page->liveChildren()`: Return results only from live table
* `$page->stageChildren()`: Return results from the stage table
* `$page->AllHistoricalChildren()`: Return all the children this page had, including pages that were deleted from both stage & live.
* `$page->AllChildrenIncludingDeleted()`: Return all children, including those that have been deleted but are still in live.
![url table](http://silverstripe.org/assets/screenshots/Nested-URLs-Table.png)
## Limiting Hierarchy
## Limiting Children/Parent
By default, any page type can be the child of any other page type. However, there are 4 static properties that can be
By default, any page type can be the child of any other page type.
However, there are static properties that can be
used to set up restrictions that will preserve the integrity of the page hierarchy.
Example: Restrict blog entry pages to nesting underneath their blog holder
:::php
class BlogHolder extends Page {
// Blog holders can only contain blog entries
static $allowed_children = array("BlogEntry");
static $default_child = "BlogEntry";
...
// ...
}
class BlogEntry extends Page {
// Blog entries can't contain children
static $allowed_children = "none";
static $default_parent = "blog";
static $can_be_root = false;
...
// ...
}
class Page extends SiteTree {
// Don't let BlogEntry pages be underneath Pages. Only underneath Blog holders.
static $allowed_children = array("*Page,", "BlogHolder");
}
@ -77,209 +110,33 @@ subclasses. Otherwise, the class and all its subclasses are allowed.
* **default_child:** If a page is allowed more than 1 type of child, you can set a default. This is the value that
will be automatically selected in the page type dropdown when you create a page in the CMS.
* **default_parent:** This should be set to the *URLSegment* of a specific page, not to a class name. If you have
asked to create a page of a particular type that's not allowed underneath the page that you have selected, then the
default_parent page will be selected. For example, if you have a gallery page open in the CMS, and you select add blog
entry, you can set your site up to automatically select the blog page as a parent.
* **can_be_root:** This is a boolean variable. It lets you specify whether the given page type can be in the top
level.
Note that there is no allowed_parents control. To set this, you will need to specify the allowed_children of all other
page types to exclude the page type in question. IMO this is less than ideal; it's possible that in a future release we
will add allowed_parents, but right now we're trying to limit the amount of mucking around with the API we do.
Note that there is no allowed_parents` control. To set this, you will need to specify the `allowed_children` of all other page types to exclude the page type in question.
Here is an overview of everything you can add to a class that extends sitetree. NOTE: this example will not work, but
it is a good starting point, for choosing your customisation.
## Permission Control
## Tree Display (Description, Icons and Badges)
The page tree in the CMS is a central element to manage page hierarchies,
hence its display of pages can be customized as well.
On a most basic level, you can specify a custom page icon
to make it easier for CMS authors to identify pages of this type,
when navigating the tree or adding a new page:
:::php
class Page extends SiteTree {
// tree customisation
static $icon = "";
static $allowed_children = array("SiteTree"); // set to string "none" or array of classname(s)
static $default_child = "Page"; //one classname
static $default_parent = null; // NOTE: has to be a URL segment NOT a class name
static $can_be_root = true; //
static $hide_ancestor = null; //dont show ancestry class
// extensions and functionality
static $versioning = array();
static $default_sort = "Sort";
/static $extensions = array();
public static $breadcrumbs_delimiter = " &raquo; ";
public function canCreate() {
//here is a trick to only allow one (e.g. holder) of a page
return !DataObject::get_one($this->class);
}
public function canDelete() {
return false;
}
public function getCMSFields() {
$fields = parent::getCMSFields();
return $fields;
}
## Recipes
### Automatic Child Selection
By default, `[api:SiteTree]` class to build a tree using the ParentID field. However, sometimes, you want to change
this default behaviour.
For example, in our e-commerce module, we use a many-to-many join, Product::Parents, to let you put Products in multiple
groups. Here's how to implement such a change:
* **Set up your new data model:** Create the appropriate many-many join or whatever it is that you're going to use to
store parents.
* **Define stageChildren method:** This method should return the children of the current page, for the current version.
If you use DataObject::get, the `[api:Versioned]` class will rewrite your query to access the live site when
appropriate.
* **Define liveChildren method:** The method should return the children of the current page, for the live site.
Both the CMS and the site's data controls will make use of this, so navigation, breadcrumbs, etc will be updated. If 1
node appears in the tree more than once, it will be represented differently.
**TO DO:** Work out this representation.
### Custom Children Getters
Returning custom children for a specific `SiteTree` subclass can be handy to influence the tree display within the
CMS. An example of custom children might be products which belong to multiple categories. One category would get its
products from a `$many_many` join rather than the default relations.
Children objects are generated from two functions `stageChildren()` and `liveChildren()` and the tree generation in
the CMS is calculated from `numChildren()`. Please keep in mind that the returned children should still be instances
of `SiteTree`.
Example:
:::php
class MyProduct extends Page {
static $belongs_many_many = array(
'MyCategories' => 'MyCategory'
);
}
class MyCategory extends Page {
static $many_many = array(
'MyProducts' => 'MyProduct'
);
public function stageChildren($showAll = false) {
// @todo Implement $showAll
return $this->MyProducts();
}
public function liveChildren($showAll = false) {
// @todo Implement $showAll
return $this->MyProducts();
}
public function numChildren() {
return $this->MyProducts()->Count();
}
} }
class StaggPage extends Page {
static $singular_name = 'Staff Directory';
static $plural_name = 'Staff Directories';
static $description = 'Two-column layout with a list of staff members';
static $icon = 'mysite/images/staff-icon.png';
// ...
}
### Multiple parents in the tree
The `[api:LeftAndMain]` tree supports multiple parents. We overload CMSTreeClasses and make it include "manyparents" in
the class list.
:::php
public function CMSTreeClasses($controller) {
return parent::CMSTreeClasses($controller) . ' manyparents';
}
Don't forget to define a new Parent() method that also references your new many-many join (or however it is you've set
up the hierarchy!
:::php
public function getParent() {
return $this->Parent();
}
public function Parent() {
$parents = $this->Parents();
if($parents) return $parents->First();
}
Sometimes, you don't want to mess with the CMS in this manner. In this case, leave stageChildren() and liveChildren()
as-is, and instead make another method, such as ChildProducts(), to get the data from your many-many join.
### Dynamic Grouping
Something that has been talked about [here](http://www.silverstripe.com/site-builders-forum/flat/15416#post15940) is the
concept of "dynamic grouping". In essence, it means adding navigational tree nodes to the tree that don't correspond to
a database record.
How would we do this? In our example, we're going to update BlogHolder to show BlogEntry children grouped into months.
We will create a class called BlogMonthTreeNode, which will extend ViewableData instead of DataRecord, since it's not
saved into the database. This will represent our dynamic groups.
### LeftAndMain::getSiteTreeFor()
Currently LeftAndMain::getSiteTreeFor() Calls LeftAndMain::getRecord($id) to get a new record. We need to instead
create a new public function getTreeRecord($id) which will be able to create BlogMonthTreeNode objects as well as look up
SiteTree records from the database.
The IDs don't **need** be numeric; so we can set the system to allow for 2 $id formats.
* (ID): A regular SiteTree object
* BlogMonthTreeNode-(BlogHolderID)-(Year)-(Month): A BlogMonthTreeNode object
To keep the code generic, we will assume that if the $id isn't numeric, then we should explode('-', $id), and use the
first part as the classname, and all the remaining parts as arguments to the constructor.
Your BlogMonthTreeNode constructor will then need to take $blogHolderID, $year, $month as arguments.
### Divorcing front-end site's Children() and the CMS's AllChildrenIncludingDeleted()
We need a way of cleanly specifying that there are two different child sources - children for the CMS tree, and children
for the front-end site.
* We currently have stageChildren() / liveChildren()
* We should probably add cmsStageChildren() and cmsLiveChildren() into the mix, for SiteTree.
AllChildrenIncludingDeleted() could then call the "cms..." versions of the functions, but if we were to to this, we
should probably rename AllChildrenIncludingDeleted() to CMSTreeChildren() or something like that.
### BlogHolder::cmsStageChildren() & BlogHolder::cmsLiveChildren()
We will need to define these methods, to
* Get the stage/live children of the page, grouped by month
* For each entry returned, generate a new BlogMonthTreeNode object.
* Return that as a dataobjectset.
### BlogMonthTreeNode
* Parameter 'ID': should return 'BlogMonthTreeNode-(BlogHolderID)-(Year)-(Month)'. You can do this by implementing
getID().
* Methods cmsStageChildren() and cmsLiveChildren(): These should return the blog-entries for that month.
After that, there will be some other things to tweak, like the tree icons.
### Where to from here?
This is a lot of work for the specific example of blog-entries grouped by month. Instead of BlogMonthTreeNode, you
could genericise this to a DynamicTreeGroup class, which would let you specify the parent node, the type of grouping,
and the specific group.
## TODO
Clean up this documentation
## API Documentation
`[api:Sitetree]`
You can also add custom "badges" to each page in the tree,
which denote status. Built-in examples are "Draft" and "Deleted" flags.
This is detailed in the ["Customize the CMS Tree" howto](/howto/customize-cms-tree).

View File

@ -2,207 +2,142 @@
## Introduction
An object representing a SQL query. It is easier to deal with object-wrappers than string-parsing a raw SQL-query. This
object is used by `[api:DataObject]`, though...
An object representing a SQL query, which can be serialized into a SQL statement.
It is easier to deal with object-wrappers than string-parsing a raw SQL-query.
This object is used by the SilverStripe ORM internally.
A word of caution: Dealing with low-level SQL is not encouraged in the SilverStripe [datamodel](/topics/datamodel) for various
reasons. You'll break the behaviour of:
Dealing with low-level SQL is not encouraged, since the ORM provides
powerful abstraction APIs (see [datamodel](/topics/datamodel).
Starting with SilverStripe 3, records in collections are lazy loaded,
and these collections have the ability to run efficient SQL
such as counts or returning a single column.
* Custom getters/setters
* DataObject::onBeforeWrite/onBeforeDelete
For example, if you want to run a simple `COUNT` SQL statement,
the following three statements are functionally equivalent:
:::php
// Through raw SQL
$count = DB::query('SELECT COUNT(*) FROM "Member"')->value();
// Through SQLQuery abstraction layer
$query = new SQLQuery();
$count = $query->setFrom('Member')->setSelect('COUNT(*)')->value();
// Through the ORM
$count = Member::get()->count();
If you do use raw SQL, you'll run the risk of breaking
various assumptions the ORM and code based on it have:
* Custom getters/setters (object property can differ from database column)
* DataObject hooks like onBeforeWrite() and onBeforeDelete()
* Automatic casting
* Default-setting through object-model
* `[api:DataObject]`
* Default values set through objects
* Database abstraction
We'll explain some ways to use *SELECT* with the full power of SQL, but still maintain a connection to the SilverStripe
[datamodel](/topics/datamodel).
We'll explain some ways to use *SELECT* with the full power of SQL,
but still maintain a connection to the ORM where possible.
<div class="warning" markdown="1">
Please read our ["security" topic](/topics/security) to find out
how to sanitize user input before using it in SQL queries.
</div>
## Usage
### SELECT
:::php
$sqlQuery = new SQLQuery();
$sqlQuery->select = array(
'Firstname AS Name',
'YEAR(Birthday) AS BirthYear'
);
$sqlQuery->from = "
Player
LEFT JOIN Team ON Player.TeamID = Team.ID
";
$sqlQuery->where = "
YEAR(Birthday) = 1982
";
// $sqlQuery->orderby = "";
// $sqlQuery->groupby = "";
// $sqlQuery->having = "";
// $sqlQuery->limit = "";
// $sqlQuery->distinct = true;
$sqlQuery->setFrom('Player');
$sqlQuery->selectField('FieldName', 'Name');
$sqlQuery->selectField('YEAR("Birthday")', 'Birthyear');
$sqlQuery->addLeftJoin('Team','"Player"."TeamID" = "Team"."ID"');
$sqlQuery->addWhere('YEAR("Birthday") = 1982');
// $sqlQuery->setOrderBy(...);
// $sqlQuery->setGroupBy(...);
// $sqlQuery->setHaving(...);
// $sqlQuery->setLimit(...);
// $sqlQuery->setDistinct(true);
// get the raw SQL
// Get the raw SQL (optional)
$rawSQL = $sqlQuery->sql();
// execute and return a Query-object
// Execute and return a Query object
$result = $sqlQuery->execute();
### DELETE
:::php
// ...
$sqlQuery->delete = true;
### INSERT/UPDATE
(currently not supported -see below for alternative solutions)
## Working with results
The result is an array lightly wrapped in a database-specific subclass of `[api:Query]`. This class implements the
*Iterator*-interface defined in PHP5, and provides convenience-methods for accessing the data.
### Iterating
:::php
// Iterate over results
foreach($result as $row) {
echo $row['BirthYear'];
}
The result is an array lightly wrapped in a database-specific subclass of `[api:Query]`.
This class implements the *Iterator*-interface, and provides convenience-methods for accessing the data.
### Quick value checking
Raw SQL is handy for performance-optimized calls.
### DELETE
:::php
class Team extends DataObject {
public function getPlayerCount() {
$sqlQuery = new SQLQuery(
"COUNT(Player.ID)",
"Team LEFT JOIN Player ON Team.ID = Player.TeamID"
);
return $sqlQuery->execute()->value();
}
$sqlQuery->setDelete(true);
Way faster than dealing with `[api:DataObject]`s, but watch out for premature optimisation:
### INSERT/UPDATE
Currently not supported through the `SQLQuery` class, please use raw `DB::query()` calls instead.
:::php
$players = $myTeam->Players();
echo $players->Count();
DB::query('UPDATE "Player" SET "Status"=\'Active\'');
### Value Checks
### Mapping
Raw SQL is handy for performance-optimized calls,
e.g. when you want a single column rather than a full-blown object representation.
Useful for creating dropdowns.
:::php
$sqlQuery = new SQLQuery(
array('YEAR(Birthdate)', 'Birthdate'),
'Player'
);
$map = $sqlQuery->execute()->map();
$field = new DropdownField('Birthdates', 'Birthdates', $map);
### "Raw" SQL with DB::query()
This is not recommended for most cases, but you can also use the SilverStripe database-layer to fire off a raw query:
:::php
DB::query("UPDATE Player SET Status='Active'");
One example for using a raw DB::query is when you are wanting to order twice in the database:
:::php
$records = DB::query('SELECT *, CASE WHEN "ThumbnailID" = 0 THEN 2 ELSE 1 END AS "HasThumbnail" FROM "TempDoc" ORDER BY "HasThumbnail", "Name" ASC');
$items = singleton('TempDoc')->buildDataObjectSet($records);
This CASE SQL creates a second field "HasThumbnail" depending if "ThumbnailID" exists in the database which you can then
order by "HasThumbnail" to make sure the thumbnails are at the top of the list and then order by another field "Name"
separately for both the items that have a thumbnail and then for those that don't have thumbnails.
### "Semi-raw" SQL with buildSQL()
You can gain some ground on the datamodel-side when involving the selected class for querying. You don't necessarily
need to call *buildSQL* from a specific object-instance, a *singleton* will do just fine.
:::php
$sqlQuery = singleton('Player')->buildSQL(
'YEAR(Birthdate) = 1982'
);
This form of building a query has the following advantages:
* Respects DataObject::$default_sort
* Automatically LEFT JOIN on all base-tables (see [database-structure](database-structure))
* Selection of *ID*, *ClassName*, *RecordClassName*, which are necessary to use *buildDataObjectSet* later on
* Filtering records for correct *ClassName*
### Transforming a result to `[api:DataObjectSet]`
This is a commonly used technique inside SilverStripe: Use raw SQL, but transfer the resulting rows back into
`[api:DataObject]`s.
Example: Get the count from a relationship.
:::php
$sqlQuery = new SQLQuery();
$sqlQuery->select = array(
'Firstname AS Name',
'YEAR(Birthday) AS BirthYear',
// IMPORTANT: Needs to be set after other selects to avoid overlays
'Player.ClassName AS ClassName',
'Player.ClassName AS RecordClassName',
'Player.ID AS ID'
);
$sqlQuery->from = array(
"Player",
"LEFT JOIN Team ON Player.TeamID = Team.ID"
);
$sqlQuery->where = array(
"YEAR(Player.Birthday) = 1982"
);
$sqlQuery->setFrom('Player');
$sqlQuery->addSelect('COUNT("Player"."ID")');
$sqlQuery->addWhere('"Team"."ID" = 99');
$sqlQuery->addLeftJoin('Team', '"Team"."ID" = "Player"."TeamID"');
$count = $sqlQuery->execute()->value();
$result = $sqlQuery->execute();
var_dump($result->first()); // array
Note that in the ORM, this call would be executed in an efficient manner as well:
// let Silverstripe work the magic
$myDataObjectSet = singleton('Player')->buildDataObjectSet($result);
var_dump($myDataObjectSet->First()); // DataObject
:::php
$count = $myTeam->Players()->count();
// this is where it gets tricky
$myFirstPlayer = $myDataObjectSet->First();
var_dump($myFirstPlayer->Name); // 'John'
var_dump($myFirstPlayer->Firstname); // undefined, as it was not part of the SELECT-clause;
var_dump($myFirstPlayer->Surname); // undefined, as it was not part of the SELECT-clause
### Mapping
// lets assume that class Player extends BasePlayer,
// and BasePlayer has a database-column "Status"
var_dump($myFirstPlayer->Status); // undefined, as we didn't LEFT JOIN the BasePlayer-table
Creates a map based on the first two columns of the query result.
This can be useful for creating dropdowns.
Example: Show player names with their birth year, but set their birth dates as values.
**CAUTION:** Depending on the selected columns in your query, you might get into one of the following scenarios:
:::php
$sqlQuery = new SQLQuery();
$sqlQuery->setFrom('Player');
$sqlQuery->setSelect('Birthdate');
$sqlQuery->selectField('CONCAT("Name", ' - ', YEAR("Birthdate")', 'NameWithBirthyear');
$map = $sqlQuery->execute()->map();
$field = new DropdownField('Birthdates', 'Birthdates', $map);
* Not all object-properties accessible: You need to take care of selecting the right stuff yourself
* Overlayed object-properties: If you *LEFT JOIN* a table which also has a column 'Birthdate' and do a global select on
this table, you might not be able to access original object-properties.
* You can't create `[api:DataObject]`s where no scalar record-data is available, e.g. when using *GROUP BY*
* Naming conflicts with custom getters: A getter like Player->getName() will overlay the column-data selected in the
above example
Note that going through SQLQuery is just necessary here
because of the custom SQL value transformation (`YEAR()`).
An alternative approach would be a custom getter in the object definition.
Be careful when saving back `[api:DataObject]`s created through *buildDataObjectSet*, you might get strange side-effects due to
the issues noted above.
## Using FormFields with custom SQL
Some subclasses of `[api:FormField]` for ways to create sophisticated report-tables based on SQL.
:::php
class Player extends DataObject {
static $db = array(
'Name' =>
'Birthdate' => 'Date'
);
function getNameWithBirthyear() {
return date('y', $this->Birthdate);
}
}
$players = Player::get();
$map = $players->map('Name', 'NameWithBirthyear');
## Related
* [datamodel](/topics/datamodel)
* `[api:DataObject]`
* [database-structure](database-structure)
## API Documentation
`[api:SQLQuery]`

View File

@ -32,7 +32,7 @@ publisher to generate folders and HTML-files.
$urls = array();
// memory intensive depending on number of pages
$pages = DataObject::get("SiteTree");
$pages = SiteTree::get();
foreach($pages as $page) {
$urls = array_merge($urls, (array)$page->subPagesToCache());
@ -78,7 +78,7 @@ you can also add an exclusion
:::php
public function allPagesToCache() {
$urls = array();
$pages = DataObject::get("SiteTree");
$pages = SiteTree::get();
// ignored page types
$ignored = array('UserDefinedForm');
@ -93,12 +93,12 @@ you can also add an exclusion
return $urls;
}
You can also pass the filtering to the original DataObject::get("SiteTree");
You can also pass the filtering to the original `SiteTree::get()`;
:::php
public function allPagesToCache() {
$urls = array();
$pages = DataObject::get("SiteTree", "ClassName != 'UserDefinedForm'");
$pages = SiteTree::get()->where("ClassName != 'UserDefinedForm'");
...
## Single server Caching

View File

@ -2,6 +2,10 @@
## Introduction
<div class="warning" markdown="1">
This field is deprecated in favour of the new [GridField](/topics/grid-field) API.
</div>
Form field that embeds a list of `[api:DataObject]`s into a form, such as a member list or a file list.
Provides customizeable columns, record-deletion by ajax, paging, sorting, CSV-export, printing, input by
`[api:DataObject]` or raw SQL.
@ -82,7 +86,7 @@ For more information on each of the features used in the example, you can read b
);
// custom DataObjectSet
$myProducts = DataObject::get('Product','Code = "MyCode"');
$myProducts = Product::get()->filter('Code', "MyCode");
$myTableListField->setCustomSourceItems($myProducts);
// custom SQL

View File

@ -2,9 +2,9 @@
These are the main changes to the SiverStripe 3 template language.
## Control
## Control blocks: Loops vs. Scope
The `<% control var %>...<% end_control %>` in SilverStripe prior to version 3 has two different meanings. Firstly, if the control variable is a collection (e.g. DataObjectSet), then `<% control %>` iterates over that set. If it's a non-iteratable object, however, `<% control %>` introduces a new scope, which is used to render the inner template code. This dual-use is confusing to some people, and doesn't allow a collection of objects to be used as a scope.
The `<% control var %>...<% end_control %>` in SilverStripe prior to version 3 has two different meanings. Firstly, if the control variable is a collection (e.g. DataList), then `<% control %>` iterates over that set. If it's a non-iteratable object, however, `<% control %>` introduces a new scope, which is used to render the inner template code. This dual-use is confusing to some people, and doesn't allow a collection of objects to be used as a scope.
In SilverStripe 3, the first usage (iteration) is replaced by `<% loop var %>`. The second usage (scoping) is replaced by `<% with var %>`

View File

@ -12,55 +12,38 @@ Here is a very simple template:
:::ss
<html>
<%-- This is my first template --%>
<head>
<% base_tag %>
<title>$Title</title>
$MetaTags
<% require themedCSS(screen) %>
</head>
<body>
<div id="Container">
<div id="Header">
<header>
<h1>Bob's Chicken Shack</h1>
<% with $CurrentMember %>
<p>You are logged in as $FirstName $Surname.</p>
<% end_if %>
</div>
<div id="Navigation">
<% if $Menu(1) %>
<ul>
<% loop $Menu(1) %>
<li><a href="$Link" title="Go to the $Title page" class="$LinkingMode">$MenuTitle</a></li>
<% end_loop %>
</ul>
<% end_if %>
</div>
<div class="typography">
$Layout
</div>
<div id="Footer">
<p>Copyright $Now.Year</p>
</div>
</div>
</header>
<% with $CurrentMember %>
<p>Welcome $FirstName $Surname.</p>
<% end_with %>
<% if Dishes %>
<ul>
<% loop Dishes %>
<li>$Title ($Price.Nice)</li>
<% end_loop %>
</ul>
<% end_if %>
<% include Footer %>
</body>
</html>
More sophisticated use of templates for pages managed in the CMS,
including template inheritance and navigation loops
is documented in the [page types](/topics/page-types) topic.
# Template elements
### Base Tag
The `<% base_tag %>` placeholder is replaced with the HTML base element. Relative links within a document (such as `<img
src="someimage.jpg" />`) will become relative to the URI specified in the base tag. This ensures the browser knows where
to locate your sites images and css files. So it is a must for templates!
It renders in the template as `<base href="http://www.mydomain.com" /><!--[if lte IE 6]></base><![endif]-->`
### Layout Tag
In every SilverStripe theme there is a default `Page.ss` file in the `/templates` folder. `$Layout` appears in this file
and is a core variable which includes a Layout template inside the `/templates/Layout` folder once the page is rendered.
By default the `/templates/Layout/Page.ss` file is included in the html template.
## Variables
Variables are things you can use in a template that grab data from the page and put in the HTML document. For example:
@ -68,7 +51,6 @@ Variables are things you can use in a template that grab data from the page and
:::ss
$Title
This inserts the value of the Title field of the page being displayed in place of `$Title`. This type of variable is called a **property**. It is often something that can be edited in the CMS. Variables can be chained together, and include arguments.
:::ss
@ -141,81 +123,90 @@ See [CSS](/topics/css) and [Javascript](/topics/javascript) topics for individua
[requirements](reference/requirements) for good examples of including both Javascript and CSS files.
## Conditional Logic
You can conditionally include markup in the output. That is, test for something that is true or false, and based on that test, control what gets output.
The simplest if block is to check for the presence of a value.
<% if $CurrentMember %>
<p>You are logged in as $CurrentMember.FirstName $CurrentMember.Surname.</p>
<% end_if %>
:::ss
<% if $CurrentMember %>
<p>You are logged in as $CurrentMember.FirstName $CurrentMember.Surname.</p>
<% end_if %>
The following compares a page property called `MyDinner` with the value in quotes, `kipper`, which is a **literal**. If true, the text inside the if-block is output.
<% if $MyDinner="kipper" %>
Yummy, kipper for tea.
<% end_if %>
:::ss
<% if $MyDinner="kipper" %>
Yummy, kipper for tea.
<% end_if %>
Note that inside a tag like this, variables should have a '$' prefix, and literals should have quotes. SilverStripe 2.4 didn't include the quotes or $ prefix, and while this still works, we recommend the new syntax as it is less ambiguous.
This example shows the use of the `else` option. The markup after `else` is output if the tested condition is *not* true.
<% if $MyDinner="kipper" %>
Yummy, kipper for tea
<% else %>
I wish I could have kipper :-(
<% end_if %>
:::ss
<% if $MyDinner="kipper" %>
Yummy, kipper for tea
<% else %>
I wish I could have kipper :-(
<% end_if %>
This example shows the user of `else\_if`. There can be any number of `else\_if` clauses. The conditions are tested from first to last, until one of them is true, and the markup for that condition is used. If none of the conditions are true, the markup in the `else` clause is used, if that clause is present.
<% if $MyDinner="quiche" %>
Real men don't eat quiche
<% else_if $MyDinner=$YourDinner %>
We both have good taste
<% else %>
Can I have some of your chips?
<% end_if %>
:::ss
<% if $MyDinner="quiche" %>
Real men don't eat quiche
<% else_if $MyDinner=$YourDinner %>
We both have good taste
<% else %>
Can I have some of your chips?
<% end_if %>
This example shows the use of `not` to negate the test.
<% if not $DinnerInOven %>
I'm going out for dinner tonight.
<% end_if %>
:::ss
<% if not $DinnerInOven %>
I'm going out for dinner tonight.
<% end_if %>
You can combine two or more conditions with `||` ("or"). The markup is used if *either* of the conditions is true.
<% if $MyDinner=="kipper" || $MyDinner=="salmon" %>
yummy, fish for tea
<% end_if %>
:::ss
<% if $MyDinner=="kipper" || $MyDinner=="salmon" %>
yummy, fish for tea
<% end_if %>
You can combine two or more conditions with `&&` ("and"). The markup is used if *both* of the conditions are true.
<% if $MyDinner=="quiche" && $YourDinner=="kipper" %>
Lets swap dinners
<% end_if %>
:::ss
<% if $MyDinner=="quiche" && $YourDinner=="kipper" %>
Lets swap dinners
<% end_if %>
As you'd expect, these can be nested:
<% if $MyDinner=="chicken" %>
<% if $Wine=="red" %>
You're doing it wrong
<% else %>
Perfect
<% end_if %>
<% end_if %>
## Looping Over Datasets
## Looping Over Lists
The `<% loop %>...<% end_loop %>` tag is used to **iterate** or loop over a collection of items. For example:
<ul>
<% loop $Children %>
<li>$Title</li>
<% end_loop %>
</ul>
:::ss
<ul>
<% loop $Children %>
<li>$Title</li>
<% end_loop %>
</ul>
This loops over the children of a page, and generates an unordered list showing the Title property from each one. Note that $Title <i>inside</i> the loop refers to the Title property on each object that is looped over, not the current page. (To refer to the current page's Title property inside the loop, you can do `$Up.Title`. More about `Up` later.
This loops over the children of a page, and generates an unordered list showing the `Title` property from each one. Note that `$Title` *inside* the loop refers to the `Title` property on each object that is looped over, not the current page. To refer to the current page's `Title` property inside the loop, you can do `$Up.Title`. More about `Up` later.
The value that given in the `<% loop %>` tags should be a collection variable.
### Position Indicators
Inside the loop scope, there are many variables at your disposal to determine the current position
in the list and iteration:
* `$Even`, `$Odd`: Returns boolean, handy for zebra striping
* `$EvenOdd`: Returns a string, either 'even' or 'odd'. Useful for CSS classes.
* `$First`, `$Last`, `$Middle`: Booleans about the position in the list
* `$FirstLast`: Returns a string, "first", "last", or "". Useful for CSS classes.
* `$Pos`: The current position in the list (integer). Will start at 1.
* `$TotalItems`: Number of items in the list (integer)
### Modulus and MultipleOf
@ -249,6 +240,8 @@ You can also use $MultipleOf(value, offset) to help build columned layouts. In t
In the `<% loop %>` section, we saw an example of two **scopes**. Outside the `<% loop %>...<% end_loop %>`, we were in the scope of the page. But inside the loop, we were in the scope of an item in the list. The scope determines where the value comes from when you refer to a variable. Typically the outer scope of a page type's layout template is the page that is currently being rendered. The outer scope of an included template is the scope that it was included into.
When we are in a scope, we sometimes want to refer to the scope outside the <% loop %> or <% with %>. We can do that easily by using `$Up`.
### With
The `<% with %>...<% end_with %>` tag lets you introduce a new scope. Consider the following example:
@ -265,31 +258,47 @@ Outside the `<% with %>...<% end_with %>`, we are in the page scope. Inside it,
returns the number of items in the $Children collection.
### Top
## Pagination
$Top.Title
Lists can be paginated, and looped over page-by-page.
For this to work, the list needs to be wrapped in a `[api:PaginatedList]`.
The process is explained in detail on the ["pagination" howto](/howto/pagination).
The list is split up in multiple "pages", each . Note that "page" is this context
does not necessarily refer to a `Page` class (although it often happens to be one).
### Up
* `$MoreThanOnePage`: Returns true when we have a multi-page list, restricted with a limit.
* `$NextLink`, `$PrevLink`: This returns links to the next and previous page in a multi-page datafeed. They will return blank if there's no appropriate page to go to, so `$PrevLink` will return blank when you're on the first page.
* `$CurrentPage`: Current page iterated on
* `$TotalPages`: Total number of pages
* `$TotalItems`: This returns the total number of items across all pages.
* `$Pages`: The actual (limited) list of records, use in an inner loop
* `$PageNum`: Page number, starting at 1 (within `$Pages`)
* `$Link`: Links to the current controller URL, setting this page as current via a GET parameter (within `$Pages`)
* `$CurrentBool`: Returns true if you're currently on that page (within `$Pages`)
When we are in a scope, we sometimes want to refer to the scope outside the <% loop %> or <% with %>. We can do that easily by using $Up.
## Formatting and Casting
$Up.Owner
Properties are usually auto-escaped in templates to ensure consistent representation,
and avoid format clashes like displaying unescaped ampersands in HTML.
By default, values are escaped as `XML`, which is equivalent to `HTML` for this purpose.
There's some exceptions to this rule, see the ["security" topic](/topics/security).
## Formatting Template Values
The following example takes the Title field of our object, casts it to a `[api:Varchar]` object, and then calls
the `$XML` object on that Varchar object.
In case you want to explicitly allow unescaped HTML input,
the property can be cast as `[api:HTMLText]`.
The following example takes the `Content` field in a `SiteTree` class,
which is of this type. It forces the content into an explicitly escaped format.
:::ss
<% with Title %>
$XML
<% end_with %>
$Content.XML // transforms e.g. "<em>alert</em>" to "&lt;em&gt;alert&lt;/em&gt;"
Note that this code can be more concisely represented as follows:
Apart from value formatting, there's many methods to transform them as well,
For example, the built in `$Now` placeholder is an instance of `[api:Date]`,
and returns the current date in a standard system format.
Since its an object, you can use the helper methods to return other formats:
:::ss
$Title.XML
$Now.Year // Current year
$Now.Nice // Localized date, based on i18n::get_locale()
See [data-types](/topics/data-types) for more information.
@ -311,49 +320,64 @@ Pulling apart this example we see:
Using standard HTML comments is supported. These comments will be included in the published site.
:::ss
$EditForm <!-- Some Comment About the Edit Form -->
$EditForm <!-- Some public comment about the form -->
However you can also use special SilverStripe comments which will be stripped out of the published site. This is useful
for adding notes for other developers but for things you don't want published in the public html.
:::ss
$EditForm <%-- This is Located in MemberEditForm.php --%>
$EditForm <%-- Some hidden comment about the form --%>
## Partial Caching
Partial caching lets you define blocks of your template that are cached for better performance. See [Partial Caching](/reference/partial-caching.md) for more information.
Partial caching lets you define blocks of your template that are cached for better performance. See [Partial Caching](/reference/partial-caching) for more information.
## Creating your own Template Variables and Controls
### Base Tag
The `<% base_tag %>` placeholder is replaced with the HTML base element. Relative links within a document (such as `<img
src="someimage.jpg" />`) will become relative to the URI specified in the base tag. This ensures the browser knows where
to locate your sites images and css files. So it is a must for templates!
It renders in the template as `<base href="http://www.mydomain.com" /><!--[if lte IE 6]></base><![endif]-->`
## CurrentMember
Returns the currently logged in member, if there is one.
All of their details or any special Member page controls can be called on this.
Alternately, you can use `<% if CurrentMember %>` to detect whether someone has logged
in.
:::ss
<% if CurrentMember %>
Welcome Back, $CurrentMember.FirstName
<% end_if %>
## Custom Template Variables and Controls
There are two ways you can extend the template variables you have available. You can create a new database field in your
`$db` or if you do not need the variable to be editable in the cms you can create a function which returns a value in your
`Page.php` class.
:::php
**mysite/code/Page.php**
...
// mysite/code/Page.php
public function MyCustomValue() {
return "Hi, this is my site";
return "Hi, this is my site";
}
Will give you the ability to call `$MyCustomValue` from anywhere in your template.
:::ss
I've got one thing to say to you: <i>$MyCustomValue</i>
// output "I've got one thing to say to you: <i>Hi, this is my site</i>"
Your function could return a single value as above or it could be a subclass of `[api:ArrayData]` for example a
`[api:DataObject]` with many values then each of these could be accessible via a control loop
:::php
..
// ...
public function MyCustomValues() {
return new ArrayData(array("Hi" => "Kia Ora", "Name" => "John Smith"));
return new ArrayData(array("Hi" => "Kia Ora", "Name" => "John Smith"));
}
@ -363,23 +387,17 @@ And now you could call these values by using
<% with MyCustomValues %>
$Hi , $Name
<% end_with %>
// output "Kia Ora , John Smith"
Or by using the dot notation you would have
:::ss
$MyCustomValues.Hi , $MyCustomValues.Name
// output "Kia Ora , John Smith"
### Side effects
All functions that provide data to templates must have no side effects, as the value is cached after first access.
For example, this Controller method
All functions that provide data to templates must have no side effects, as the value is cached after first access. For example, this controller method
:::php
private $counter = 0;
@ -396,22 +414,7 @@ and this template
$Counter, $Counter, $Counter
will give "1, 1, 1", not "1, 2, 3"
### Casting and Escaping
Method and variables names that deal with strings or arrays of strings should have one of the following 5 prefixes:
* **RAW_** Raw plain text, as a user would like to see it, without any HTML tags
* **XML_** Text suitable for insertion into an HTML or XML data-set. This may contain HTML content, for example if the
content came from a WYSIWYG editor.
* **JS_** Data that can safely be inserted into JavaScript code.
* **ATT_** Data that can safely be inserted into an XML or HTML attribute.
The same prefixes are used for both strings and arrays of strings. We did this to keep things simple: passing a string
with the wrong encoding is a far subtler a problem than passing an array instead of a string, and therefore much harder
to debug.
will render as "1, 1, 1", not "1, 2, 3"
## .typography style
@ -466,8 +469,7 @@ default if it exists and there is no action in the url parameters.
public function index() {
if(Director::is_ajax()) {
return $this->renderWith("myAjaxTemplate");
}
else {
} else {
return Array();// execution as usual in this case...
}
}
@ -508,7 +510,18 @@ situations, you can disable fragment link rewriting like so:
:::php
SSViewer::setOption('rewriteHashlinks', false);
### More Advanced Controls
Template variables and controls are just PHP properties and methods
on the underlying controllers and model classes.
We've just shown you the most common once, in practice
you can use any public API on those classes, and [extend](/reference/dataextension) them
with your own. To get an overview on what's available to you,
we recommend that you dive into the API docs for the following classes:
* `[api:Controller]`: Generic controller class
* `[api:DataObject]`: Generic model class
* `[api:ViewableData]`: Underlying object class for pretty much anything displayable
## Designing reusable templates
@ -516,7 +529,7 @@ Although SilverStripe is ultimately flexible in how you create your templates, t
will help you to design templates for modules, and make it easier for other site developers to integrate them into their
own base templates.
* Most of your templates should be Layout templates
* Most of your templates should be `Layout` templates
* Build your templates as a [Theme](/topics/themes) so you can easily re-use and exchange them
* Your layout template should include a standard markup structure (`<div id="Layout">$Layout</div>`)
* Layout templates only include content that could be completely replaced by another module (e.g. a forum thread). It
@ -524,7 +537,7 @@ might be infeasible to do this 100%, but remember that every piece of navigation
will mean that you have to customise templates when integrating the module.
* Any CSS applied to layout templates should be flexible width. This means the surrounding root template can set its
width independently.
* Don't include any navigation elements in your Layout templates, they should be contained in the root template.
* Don't include any navigation elements in your `Layout` templates, they should be contained in the root template.
* Break down your templates into groups of includes. Site integrators would then have the power to override individual
includes, rather than entire templates.

View File

@ -45,25 +45,6 @@ Append the option and corresponding value to your URL in your browser's address
| showqueries | | 1 | | List all SQL queries executed |
| previewwrite | | 1 | | List all insert / update SQL queries, and **don't** execute them. Useful for previewing writes to the database. |
## Profiling
| URL Variable | | Values | | Description |
| ------------ | | ------ | | ----------- |
| debug_memory | | 1 | | Output the number of bytes of memory used for this request |
| debug_profile | | 1 | | Enable the [profiler](/topics/debugging) for the duration of the request |
| profile_trace | | 1 | | Includes full stack traces, must be used with **debug_profile** |
| debug_behaviour | | 1 | | Get profiling of [Behaviour.js](http://bennolan.com/behaviour) performance (Firebug recommended) |
| debug_javascript | | 1 | | Force debug-output on live-sites |
## Misc
| URL Variable | | Values | | Description |
| ------------ | | ------ | | ----------- |
| forceFormat | | xhtml,html | | Force the content negotiator to deliver HTML or XHTML is allowed |
| showspam | | 1 | | Show comments marked as spam when viewing Comments on a Page (Saving spam to the database must be enabled) |
| ajax | | 1 | | Force request to process as AJAX request, useful for debugging from a browser |
| force_ajax | | 1 | | Similar to **ajax** |
## Security Redirects
You can set an URL to redirect back to after a [Security](/topics/security) action. See the section on [URL

View File

@ -1,17 +0,0 @@
# Versioned
The Versioned class is a `[api:DataObject]` that adds versioning and staging capabilities to the objects.
## Trapping the publication event
Sometimes, you'll want to do something whenever a particular kind of page is published. This example sends an email
whenever a blog entry has been published.
:::php
class Page extends SiteTree {
// ...
public function onAfterPublish() {
mail("sam@silverstripe.com", "Blog published", "The blog has been published");
parent::onAfterPublish();
}
}

View File

@ -8,7 +8,7 @@ itself.
This lack of a configuration-GUI is on purpose, as we'd like to keep developer-level options where they belong (into
code), without cluttering up the interface. See this core forum discussion ["The role of the
CMS"](http://www.silverstripe.com/core-team-discussion/flat/2723) for further reasoning.
CMS"](http://www.silverstripe.org/archive/show/532) for further reasoning.
In addition to these principle, some settings are
* Author-level configuration like interface language or date/time formats can be performed in the CMS "My Profile" section (`admin/myprofile`).
@ -36,9 +36,7 @@ incomplete - please add to it** *Try to keep it in alphabetical order too! :)*
| Authenticator::register_authenticator($authenticator);| | Enable an authentication method (for more details see [security](/topics/security)). |
| Authenticator::set_default_authenticator($authenticator); | | Modify tab-order on login-form.|
| BBCodeParser::disable_autolink_urls(); | | Disables plain hyperlinks from being turned into links when bbcode is parsed. |
| BlogEntry::allow_wysiwyg_editing(); | | Enable rich text editing for blog posts. |
| ContentNegotiator::set_encoding(string $encoding) | | The encoding charset to use - UTF-8 by default |
| ContentNegotiator::disable() | | Disables the negotiation of content type -usually used to stop it from rewriting the DOCTYPE of the document
| DataObject::$create_table_options['MySQLDatabase'] = 'ENGINE=MyISAM';| | Set the default database engine to MyISAM (versions 2.4 and below already default to MyISAM) |
| Debug::send_errors_to(string $email) | | Send live errors on your site to this address (site has to be in 'live' mode using Director::set_environment_type(live) for this to occur |
| Director::set_environment_type(string dev,test,live) | | Sets the environment type (e.g. dev site will show errors, live site hides them and displays a 500 error instead) |
| Director::set_dev_servers(array('localhost', 'dev.mysite.com)) | | Set servers that should be run in dev mode (see [debugging](debugging)) |
@ -47,13 +45,10 @@ incomplete - please add to it** *Try to keep it in alphabetical order too! :)*
| Email::send_all_emails_to(string $email) | | Sends all emails to this address. Useful for debugging your email sending functions |
| Email::cc_all_emails_to(string $email) | | Useful for CC'ing all emails to someone checking correspondence |
| Email::bcc_all_emails_to(string $email) | | BCC all emails to this address, similar to CC'ing emails (above) |
| MathSpamProtection::setEnabled() | | Adds a math spam question to all page comment forms |
| PageComment::enableModeration(); | | Enables comment moderation |
| Requirements::set_suffix_requirements(false); | | Disable appending the current date to included files |
| Security::encrypt_passwords($encrypt_passwords); | | Specify if you want store your passwords in clear text or encrypted (for more details see [security](/topics/security)) |
| Security::set_password_encryption_algorithm($algorithm, $use_salt);| | If you choose to encrypt your passwords, you can choose which algorithm is used to and if a salt should be used to increase the security level even more (for more details see [security](/topics/security)). |
| Security::setDefaultAdmin('admin','password'); | | Set default admin email and password, helpful for recovering your password |
| SSAkismet::setAPIKey(string $key) | | Enables use of the Akismet spam filter. The key must be a valid WordPress API key. |
| SSViewer::set_theme(string $themename) | | Choose the default theme for your site |
## Constants

View File

@ -1,11 +1,14 @@
# Controller
Base controller class. You will extend this to take granular control over the actions and url handling of aspects of
your SilverStripe site.
Base controller class. You will extend this to take granular control over the
actions and url handling of aspects of your SilverStripe site.
## Example
The following example is for a simple `[api:Controller]` class. If you're using
the cms module and looking at Page_Controller instances you won't need to setup
your own routes since the cms module handles these routes.
`mysite/code/Controllers/FastFood.php`
:::php
@ -17,17 +20,21 @@ your SilverStripe site.
}
}
?>
`mysite/_config/routes.yml`
:::yaml
---
Name: myroutes
After: framework/routes#coreroutes
---
Director:
rules:
'fastfood/$Action/$ID/$Name': 'FastFood_Controller'
`mysite/_config.php`
:::php
Director::addRules(50, array('fastfood/$Action/$ID/$Name' => 'FastFood_Controller'));
Request for `/fastfood/order/24/cheesefries` would result in the following to the $arguments above. If needed, use
`?flush=1` on the end of request after making any code changes to your controller.
Request for `/fastfood/order/24/cheesefries` would result in the following to
the $arguments above. If needed, use `?flush=1` on the end of request after
making any code changes to your controller.
:::ss
Array
@ -40,28 +47,31 @@ Request for `/fastfood/order/24/cheesefries` would result in the following to th
## URL Handling
In the above example the URLs were configured using the `[api:Director]` rules in the **_config.php** file.
Alternatively you can specify these in your Controller class via the **$url_handlers** static array (which gets
processed by the `[api:RequestHandler]`).
In the above example the URLs were configured using the `[api:Director]` rules
in the **routes.yml** file. Alternatively you can specify these in your
Controller class via the **$url_handlers** static array (which gets processed
by the `[api:RequestHandler]`).
This is useful when you want to subvert the fixed action mapping of `fastfood/order/*` to the function **order**. In
the case below we also want any orders coming through `/fastfood/drivethrough/` to use the same order function.
This is useful when you want to subvert the fixed action mapping of `fastfood/order/*`
to the function **order**. In the case below we also want any orders coming
through `/fastfood/drivethrough/` to use the same order function.
`mysite/code/Controllers/FastFood.php`
:::php
class FastFood_Controller extends Controller {
public static $url_handlers = array(
'drivethrough/$Action/$ID/$Name' => 'order'
);
'drivethrough/$Action/$ID/$Name' => 'order'
);
## URL Patterns
The `[api:RequestHandler]` class will parse all rules you specify against the following patterns.
The `[api:RequestHandler]` class will parse all rules you specify against the
following patterns.
**A rule must always start with alphabetical ([A-Za-z]) characters or a $Variable declaration**
**A rule must always start with alphabetical ([A-Za-z]) characters or a $Variable
declaration**
| Pattern | Description |
| ----------- | --------------- |
@ -88,6 +98,21 @@ after it. If the URLSegment is **order** then `/order/tag/34` and `/order/tag/a
You can use the `debug_request=1` switch from the [urlvariabletools](/reference/urlvariabletools) to see these in action.
## Redirection
Controllers facilitate HTTP redirection.
Note: These methods have been formerly located on the `[api:Director]` class.
* `redirect("action-name")`: If there's no slash in the URL passed to redirect, then it is assumed that you want to go to a different action on the current controller.
* `redirect("relative/url")`: If there is a slash in the URL, it's taken to be a normal URL. Relative URLs
will are assumed to be relative to the site-root.
* `redirect("http://www.absoluteurl.com")`: Of course, you can pass `redirect()` absolute URLs too.
* `redirectBack()`: This will return you to the previous page.
The `redirect()` method takes an optional HTTP status code,
either `301` for permanent redirects, or `302` for temporary redirects (default).
## API Documentation
`[api:Controller]`

View File

@ -1,26 +1,87 @@
# Data Types
# Data Types and Casting
These are the data-types that you can use when defining your data objects. They are all subclasses of `[api:DBField]`
for introducing their usage.
Properties on any SilverStripe object can be type casted automatically,
by transforming its scalar value into an instance of the `[api:DBField]` class,
providing additional helpers. For example, a string can be cast as
a `[api:Text]` type, which has a `FirstSentence()` method to retrieve the first
sentence in a longer piece of text.
## Available Types
## Types
* `[api:Varchar]`: A variable-length string of up to 255 characters, designed to store raw text
* `[api:Text]`: A variable-length string of up to 2 megabytes, designed to store raw text
* `[api:HTMLVarchar]`: A variable-length string of up to 255 characters, designed to store HTML
* `[api:HTMLText]`: A variable-length string of up to 2 megabytes, designed to store HTML
* `[api:Enum]`: An enumeration of a set of strings
* `[api:Boolean]`: A boolean field.
* `[api:Int]`: An integer field.
* `[api:Decimal]`: A decimal number.
* `[api:Currency]`: A number with 2 decimal points of precision, designed to store currency values.
* `[api:Percentage]`: A decimal number between 0 and 1 that represents a percentage.
* `[api:Date]`: A date field
* `[api:Decimal]`: A decimal number.
* `[api:Enum]`: An enumeration of a set of strings
* `[api:HTMLText]`: A variable-length string of up to 2 megabytes, designed to store HTML
* `[api:HTMLVarchar]`: A variable-length string of up to 255 characters, designed to store HTML
* `[api:Int]`: An integer field.
* `[api:Percentage]`: A decimal number between 0 and 1 that represents a percentage.
* `[api:SS_Datetime]`: A date / time field
* `[api:Text]`: A variable-length string of up to 2 megabytes, designed to store raw text
* `[api:Time]`: A time field
* `[api:Varchar]`: A variable-length string of up to 255 characters, designed to store raw text
## HTMLText vs. Text, and HTMLVarchar vs. Varchar
## Casting arbitrary values
On the most basic level, the class can be used as simple conversion class
from one value to another, e.g. to round a number.
:::php
DBField::create_field('Double', 1.23456)->Round(2); // results in 1.23
Of course that's much more verbose than the equivalent PHP call.
The power of `[api:DBField]` comes with its more sophisticated helpers,
like showing the time difference to the current date:
:::php
DBField::create_field('Date', '1982-01-01')->TimeDiff(); // shows "30 years ago"
## Casting ViewableData
Most objects in SilverStripe extend from `[api:ViewableData]`,
which means they know how to present themselves in a view context.
Through a `$casting` array, arbitrary properties and getters can be casted:
:::php
class MyObject extends ViewableData {
static $casting = array(
'MyDate' => 'Date'
);
function getMyDate() {
return '1982-01-01';
}
}
$obj = new MyObject;
$obj->getMyDate(); // returns string
$obj->MyDate; // returns string
$obj->obj('MyDate'); // returns object
$obj->obj('MyDate')->InPast(); // returns boolean
## Casting DataObject
The `[api:DataObject]` class uses `DBField` to describe the types of its
properties which are persisted in database columns, through the `[$db](api:DataObject::$db)` property.
In addition to type information, the `DBField` class also knows how to
define itself as a database column. See the ["datamodel" topic](/topics/datamodel#casting) for more details.
<div class="warning" markdown="1">
Since we're dealing with a loosely typed language (PHP)
as well as varying type support by the different database drivers,
type conversions between the two systems are not guaranteed to be lossless.
Please take particular care when casting booleans, null values, and on float precisions.
</div>
## Casting in templates
In templates, casting helpers are available without the need for an `obj()` call.
Example: Flagging an object of type `MyObject` (see above) if it's date is in the past.
:::ss
<% if MyObjectInstance.MyDate.InPast %>Outdated!<% end_if %>
## Casting HTML Text
The database field types `[api:HTMLVarchar]` and `[api:Varchar]` are exactly the same in the database. However, the
templating engine knows to escape the `[api:Varchar]` field and not the `[api:HTMLVarchar]` field. So, it's important you
@ -29,6 +90,7 @@ use the right field if you don't want to be putting $FieldType.XML everywhere.
If you're going to put HTML content into the field, please use the field type with the HTML prefix. Otherwise, you're
going to risk double-escaping your data, forgetting to escape your data, and generally creating a confusing situation.
## Usage
## Related
* See [datamodel](/topics/datamodel) for information about **database schemas** implementing these types
* ["datamodel" topic](/topics/datamodel)
* ["security" topic](/topics/security)

View File

@ -3,21 +3,21 @@
SilverStripe uses an [object-relational model](http://en.wikipedia.org/wiki/Object-relational_model) that assumes the
following connections:
* Each database-table maps to a php-class
* Each database-row maps to a php-object
* Each database-column maps to a property on a php-object
* Each database-table maps to a PHP class
* Each database-row maps to a PHP object
* Each database-column maps to a property on a PHP object
All data tables in SilverStripe are defined as subclasses of `[api:DataObject]`. Inheritance is supported in the data
model: seperate tables will be linked together, the data spread across these tables. The mapping and saving/loading
logic is handled by SilverStripe, you don't need to worry about writing SQL most of the time.
The advanced object-relational layer in SilverStripe is one of the main reasons for requiring PHP5. Most of its
customizations are possible through [PHP5 Object
Most of the ORM customizations are possible through [PHP5 Object
Overloading](http://www.onlamp.com/pub/a/php/2005/06/16/overloading.html) handled in the `[api:Object]`-class.
See [database-structure](/reference/database-structure) for in-depth information on the database-schema.
See [database-structure](/reference/database-structure) for in-depth information on the database-schema,
and the ["sql queries" topic](/reference/sqlquery) in case you need to drop down to the bare metal.
## Generating the database-schema
## Generating the Database Schema
The SilverStripe database-schema is generated automatically by visiting the URL.
`http://<mysite>/dev/build`
@ -28,21 +28,20 @@ Note: You need to be logged in as an administrator to perform this command.
## Querying Data
Every query to data starts with a `DataList::create($class)` or `$class::get()` call. For example, this query would return
all of the Member objects:
Every query to data starts with a `DataList::create(<class>)` or `<class>::get()` call. For example, this query would return all of the `Member` objects:
:::php
$members = Member::get();
The ORM uses a "fluent" syntax, where you specify a query by chaining together different methods. Two common methods
are filter() and sort():
are `filter()` and `sort()`:
:::php
$members = Member::get()->filter(array('FirstName' => 'Sam'))->sort('Surname');
Those of you who know a bit about SQL might be thinking "it looks like you're querying all members, and then filtering
to those with a first name of 'Sam'. Isn't this very slow?" Is isn't, because the ORM doesn't actually execute the
query until you iterate on the result with a `foreach()` or `<% control %>`.
query until you iterate on the result with a `foreach()` or `<% loop %>`.
:::php
// The SQL query isn't executed here...
@ -83,19 +82,18 @@ If you have constructed a query that you know should return a single record, you
Quiet often you would like to sort a list. Doing this on a list could be done in a few ways.
If would like to sort the list by FirstName in a ascending way (from A to Z).
If would like to sort the list by `FirstName` in a ascending way (from A to Z).
:::php
$member = Member::get()->sort('FirstName');
// Or the more expressive way
$member = Member::get()->sort('FirstName', 'ASC');
$member = Member::get()->sort('FirstName'); // Ascending is implied
To reverse the sort
:::php
$member = Member::get()->sort('FirstName', 'DESC');
However you might have several entries with the same FirstName and would like to sort them by FirstName and LastName
However you might have several entries with the same `FirstName` and would like to sort them by `FirstName` and `LastName`
:::php
$member = Member::get()->sort(array(
@ -206,7 +204,7 @@ will return all members whose first name or surname contain the string 'sam'.
:::php
$members = Member::get()->filter(array(
'FirstName,Surname:Contains' => 'sam'
'FirstName,Surname:PartialMatch' => 'sam'
));
If you wish to match against any of a number of values, you can pass an array as the value. This will return all
@ -395,7 +393,7 @@ Note: Alternatively you can set defaults directly in the database-schema (rather
Properties defined in *static $db* are automatically casted to their [data-types](data-types) when used in templates.
You can also cast the return-values of your custom functions (e.g. your "virtual properties").
Calling those functions directly will still return whatever type your php-code generates,
Calling those functions directly will still return whatever type your PHP code generates,
but using the *obj()*-method or accessing through a template will cast the value according to the $casting-definition.
:::php
@ -406,7 +404,7 @@ but using the *obj()*-method or accessing through a template will cast the value
// $myPlayer->MembershipFee() returns a float (e.g. 123.45)
// $myPlayer->obj('MembershipFee') returns a object of type Currency
// In a template: <% control MyPlayer %>MembershipFee.Nice<% end_control %> returns a casted string (e.g. "$123.45")
// In a template: <% loop MyPlayer %>MembershipFee.Nice<% end_loop %> returns a casted string (e.g. "$123.45")
public function getMembershipFee() {
return $this->Team()->BaseFee * $this->MembershipYears;
}
@ -526,8 +524,10 @@ accessors available on both ends.
### Adding relations
Inside SilverStripe it doesn't matter if you're editing a *has_many*- or a *many_many*-relationship. You need to get a
`[api:ComponentSet]`.
Adding new items to a relations works the same,
regardless if you're editing a *has_many*- or a *many_many*.
They are encapsulated by `[api:HasManyList]` and `[api:ManyManyList]`,
both of which provide very similar APIs, e.g. an `add()` and `remove()` method.
:::php
class Team extends DataObject {
@ -536,20 +536,8 @@ Inside SilverStripe it doesn't matter if you're editing a *has_many*- or a *many
"Categories" => "Category",
);
/**
* @param DataObjectSet
*/
public function addCategories($additionalCategories) {
$existingCategories = $this->Categories();
// method 1: Add many by iteration
foreach($additionalCategories as $category) {
$existingCategories->add($category);
}
// method 2: Add many by ID-List
$existingCategories->addMany(array(1,2,45,745));
public function addCategories(SS_List $cats) {
foreach($cats as $cat) $this->Categories()->add($cat);
}
}
@ -557,8 +545,8 @@ Inside SilverStripe it doesn't matter if you're editing a *has_many*- or a *many
### Custom Relations
You can use the flexible datamodel to get a filtered result-list without writing any SQL. For example, this snippet gets
you the "Players"-relation on a team, but only containing active players. (See `[api:DataObject::$has_many]` for more info on
the described relations).
you the "Players"-relation on a team, but only containing active players.
See `[api:DataObject::$has_many]` for more info on the described relations.
:::php
class Team extends DataObject {
@ -572,6 +560,48 @@ the described relations).
}
}
Note: Adding new records to a filtered `RelationList` like in the example above
doesn't automatically set the filtered criteria on the added record.
## Validation and Constraints
Traditionally, validation in SilverStripe has been mostly handled on the controller
through [form validation](/topics/form-validation).
While this is a useful approach, it can lead to data inconsistencies if the
record is modified outside of the controller and form context.
Most validation constraints are actually data constraints which belong on the model.
SilverStripe provides the `[api:DataObject->validate()]` method for this purpose.
By default, there is no validation - objects are always valid!
However, you can overload this method in your
DataObject sub-classes to specify custom validation,
or use the hook through `[api:DataExtension]`.
Invalid objects won't be able to be written - a [api:ValidationException]`
will be thrown and no write will occur.
It is expected that you call validate() in your own application to test that an object
is valid before attempting a write, and respond appropriately if it isn't.
The return value of `validate()` is a `[api:ValidationResult]` object.
You can append your own errors in there.
Example: Validate postcodes based on the selected country
:::php
class MyObject extends DataObject {
static $db = array(
'Country' => 'Varchar',
'Postcode' => 'Varchar'
);
public function validate() {
$result = parent::validate();
if($this->Country == 'DE' && $this->Postcode && strlen($this->Postcode) != 5) {
$result->error('Need five digits for German postcodes');
}
return $result;
}
}
## Maps
A map is an array where the array indexes contain data as well as the values. You can build a map
@ -594,6 +624,9 @@ This functionality is provided by the `SS_Map` class, which can be used to build
$members = Member::get();
$map = new SS_Map($members, 'ID', 'FirstName');
Note: You can also retrieve a single property from all contained records
through `[api:SS_List->column()]`.
## Data Handling
When saving data through the object model, you don't have to manually escape strings to create SQL-safe commands.
@ -703,19 +736,13 @@ It checks if a member is logged in who belongs to a group containing the permiss
}
}
### Saving data with forms
See [forms](/topics/forms).
### Saving data with custom SQL
See `[api:SQLQuery]` for custom *INSERT*, *UPDATE*, *DELETE* queries.
See the ["sql queries" topic](/reference/sqlquery) for custom *INSERT*, *UPDATE*, *DELETE* queries.
## Extending DataObjects
@ -724,18 +751,14 @@ code or subclassing.
Please see `[api:DataExtension]` for a general description, and `[api:Hierarchy]` for our most
popular examples.
## FAQ
### Whats the difference between DataObject::get() and a relation-getter?
You can work with both in pretty much the same way, but relationship-getters return a special type of collection:
A `[api:ComponentSet]` with relation-specific functionality.
You can work with both in pretty much the same way,
but relationship-getters return a special type of collection:
A `[api:HasManyList]` or a `[api:ManyManyList]` with relation-specific functionality.
:::php
$myTeam = DataObject::get_by_id('Team',$myPlayer->TeamID); // returns DataObject
$myTeam->add(new Player()); // fails
$myTeam = $myPlayer->Team(); // returns Componentset
$myTeam->add(new Player()); // works
$myTeams = $myPlayer->Team(); // returns HasManyList
$myTeam->add($myOtherPlayer);

View File

@ -140,6 +140,9 @@ the development effort itself as "test-driven development".
#### Profiling
Silverstripe includes a profiling suite called [Profiler](http://www.adepteo.net/profiler/manual.html) from Carl Taylor
at Adepteo. You can use this withing your installation during development to find bottlenecks and more. You can enable
the profiler by adding `?debug_profile=1` to your URL.
Profiling is the best way to identify bottle necks and other slow moving parts of your application prime for optimization. SilverStripe
does not include any profiling tools out of the box, but we recommend the use of existing tools such as [XHProf](https://github.com/facebook/xhprof/)
and [XDebug](http://xdebug.org/).
* [Profiling with XHProf](http://techportal.inviqa.com/2009/12/01/profiling-with-xhprof/)
* [Profiling PHP Applications With xdebug](http://devzone.zend.com/1139/profiling-php-applications-with-xdebug/)

View File

@ -34,7 +34,7 @@ The default HTML template is located in `framework/templates/email/GenericEmail.
### Templates
* Create a SS-template file called, in this example we will use 'MyEmail.ss' inside `mysite/templates/email`.
* Fill this out with the body text for your email. You can use any [SS-template syntax](/topics/templates) (e.g. `<% control %>`,
* Fill this out with the body text for your email. You can use any [SS-template syntax](/topics/templates) (e.g. `<% loop %>`,
`<% if %>`, $FirstName etc)
* Choose your template with **setTemplate()**
* Populate any custom data into the template before sending with **populateTemplate()**

View File

@ -10,7 +10,7 @@ TODO Screenshot of admin interface
## Upload
TODO Link to Upload and FileIframeField classes
TODO Link to UploadField and FileField classes
## Image Resizing

View File

@ -1,15 +1,16 @@
# Form Validation
Form validation is a combination of PHP and JavaScript
SilverStripe provides PHP form validation out of the box,
but doesn't come with any built-in JavaScript validation
(the previously used `Validator.js` approach has been deprecated).
## PHP
### Introduction
Validators are implemented as an argument to the `[api:Form]` constructor. You create a required fields validator like
so. In this case, we're creating a `[api:RequiredFields]` validator - the `[api:Validator]` class itself is an abstract
class.
## Required Fields
Validators are implemented as an argument to the `[api:Form]` constructor,
and are subclasses of the abstract `[api:Validator]` base class.
The only implementation which comes with SilverStripe is
the `[api:RequiredFields]` class, which ensures fields are filled out
when the form is submitted.
:::php
public function Form() {
@ -19,7 +20,7 @@ class.
new TextField('MyOptionalField')
),
new FieldList(
new FormAction('submit', 'Submit')
new FormAction('submit', 'Submit form')
),
new RequiredFields(array('MyRequiredField'))
);
@ -28,7 +29,108 @@ class.
return $form;
}
### Subclassing Validator
## Form Field Validation
Form fields are responsible for validating the data they process,
through the `[api:FormField->validate()] method. There are many fields
for different purposes (see ["form field types"](/reference/form-field-types) for a full list).
## Adding your own validation messages
In many cases, you want to add PHP validation which is more complex than
validating the format or existence of a single form field input.
For example, you might want to have dependent validation on
a postcode which depends on the country you've selected in a different field.
There's two ways to go about this: Either you can attach a custom error message
to a specific field, or a generic message for the whole form.
Example: Validate postcodes based on the selected country (on the controller).
:::php
class MyController extends Controller {
public function Form() {
return Form::create($this, 'Form',
new FieldList(
new NumericField('Postcode'),
new CountryDropdownField('Country')
),
new FieldList(
new FormAction('submit', 'Submit form')
),
new RequiredFields(array('Country'))
);
}
public function submit($data, $form) {
// At this point, RequiredFields->validate() will have been called already,
// so we can assume that the values exist.
// German postcodes need to be five digits
if($data['Country'] == 'de' && isset($data['Postcode']) && strlen($data['Postcode']) != 5) {
$form->addErrorMessage('Postcode', 'Need five digits for German postcodes', 'bad');
return $this->redirectBack();
}
// Global validation error (not specific to form field)
if($data['Country'] == 'IR' && isset($data['Postcode']) && $data['Postcode']) {
$form->sessionMessage("Ireland doesn't have postcodes!", 'bad');
return $this->redirectBack();
}
// continue normal processing...
}
}
## JavaScript Validation
While there are no built-in JavaScript validation handlers in SilverStripe,
the `FormField` API is flexible enough to provide the information required
in order to plug in custom libraries.
### HTML5 attributes
HTML5 specifies some built-in form validations ([source](http://www.w3.org/wiki/HTML5_form_additions)),
which are evaluated by modern browsers without any need for JavaScript.
SilverStripe supports this by allowing to set custom attributes on fields.
:::php
// Markup contains <input type="text" required />
TextField::create('MyText')->setAttribute('required', true);
// Markup contains <input type="url" pattern="https?://.+" />
TextField::create('MyText')
->setAttribute('type', 'url')
->setAttribute('pattern', 'https?://.+')
### HTML5 metadata
In addition, HTML5 elements can contain custom data attributes with the `data-` prefix.
These are general purpose attributes, but can be used to hook in your own validation.
:::php
// Validate a specific date format (in PHP)
// Markup contains <input type="text" data-dateformat="dd.MM.yyyy" />
DateField::create('MyDate')->setConfig('dateformat', 'dd.MM.yyyy');
// Limit extensions on upload (in PHP)
// Markup contains <input type="file" data-allowed-extensions="jpg,jpeg,gif" />
$exts = array('jpg', 'jpeg', 'gif');
$validator = new Upload_Validator();
$validator->setAllowedExtensions($exts);
$upload = Upload::create()->setValidator($validator);
$fileField = FileField::create('MyFile')->setUpload(new);
$fileField->setAttribute('data-allowed-extensions', implode(',', $exts));
Note that these examples don't have any effect on the client as such,
but are just a starting point for custom validation with JavaScript.
## Model Validation
An alternative (or additional) approach to validation is to place it directly
on the model. SilverStripe provides a `[api:DataObject->validate()]` method for this purpose.
Refer to the ["datamodel" topic](/topics/datamodel#validation-and-constraints) for more information.
## Subclassing Validator
To create your own validator, you need to subclass validator and define two methods:
@ -36,42 +138,6 @@ To create your own validator, you need to subclass validator and define two meth
* **php($data)** Should return true if the given data is valid, and call $this->validationError() if there were any
errors.
## JavaScript
### Default validator.js implementation
TODO Describe behaviour.js solution easily, how to disable it
Setting fieldEl.requiredErrorMsg or formEl.requiredErrorMsg will override the default error message. Both can include
the string '$FieldLabel', which will be replaced with the field's label. Otherwise, the message is "Please fill out
"$FieldLabel", it is required".
You can use Behaviour to load in the appropriate value:
:::js
Behaviour.register({
'#Form_Form' : {
requiredErrorMsg: "Please complete this question before moving on.",
}
});
### Other validation libraries
By default, SilverStripe forms with an attached Validator instance use the custom Validator.js clientside logic. It is
quite hard to customize, and might not be appropriate for all use-cases. You can disable integrated clientside
validation, and use your own (e.g. [jquery.validate](http://docs.jquery.com/Plugins/Validation)).
Disable for all forms (in `mysite/_config.php`):
:::php
Validator::set_javascript_validation_handler('none');
Disable for a specific form:
:::php
$myForm->getValidator()->setJavascriptValidationHandler('none');
## Related
* Model Validation with [api:DataObject->validate()]

View File

@ -60,78 +60,42 @@ The real difference, however, is that you can then define your controller method
## Form Field Types
There are many classes extending `[api:FormField]`. Some examples:
* `[api:TextField]`
* `[api:EmailField]`
* `[api:NumericField]`
* `[api:DateField]`
* `[api:CheckboxField]`
* `[api:DropdownField]`
* `[api:OptionsetField]`
* `[api:CheckboxSetField]`
Full overview at [form-field-types](/reference/form-field-types)
There are many classes extending `[api:FormField]`,
there's a full overview at [form-field-types](/reference/form-field-types)
### Using Form Fields
To get these fields automatically rendered into a form element, all you need to do is create a new instance of the
To get these fields automatically rendered into a form element,
all you need to do is create a new instance of the
class, and add it to the fieldlist of the form.
:::php
$form = new Form(
$controller = $this,
$name = "SignupForm",
$fields = new FieldList(
new TextField(
$name = "FirstName",
$title = "First name"
),
new TextField("Surname"),
new EmailField("Email", "Email address"),
),
$actions = new FieldList(
// List the action buttons here
new FormAction("signup", "Sign up")
),
$requiredFields = new RequiredFields(
// List the required fields here: "Email", "FirstName"
)
);
You'll note some of the fields are optional.
Implementing the more complex fields requires extra arguments.
:::php
$form = new Form(
$controller = $this,
$name = "SignupForm",
$fields = new FieldList(
// List the your fields here
new TextField(
$name = "FirstName",
$title = "First name"
),
new TextField("Surname"),
new EmailField("Email", "Email address")
new DropdownField(
$name = "Country",
$title = "Country (if outside nz)",
$source = Geoip::getCountryDropDown(),
$value = Geoip::visitor_country()
)
), new FieldList(
// List the action buttons here
new FormAction("signup", "Sign up")
), new RequiredFields(
// List the required fields here: "Email", "FirstName"
)
$this, // controller
"SignupForm", // form name
new FieldList( // fields
TextField::create("FirstName")
->setTitle('First name')
TextField::create("Surname")
->setTitle('Last name')
->setMaxLength(50),
EmailField::create("Email")
->setTitle("Email address")
->setAttribute('type', 'email')
),
new FieldList( // actions
FormAction::create("signup")->setTitle("Sign up")
),
new RequiredFields( // validation
"Email", "FirstName"
)
);
You'll notice that we've used a new notation for creating form fields,
using `create()` instead of the `new` operator. These are functionally equivalent,
but allows PHP to chain operations like `setTitle()` without assigning
the field instance to a temporary variable.
## Readonly
@ -144,7 +108,7 @@ Readonly on a Form
Readonly on a FieldList
:::php
$myFieldSet->makeReadonly();
$myFieldList->makeReadonly();
Readonly on a FormField
@ -167,29 +131,29 @@ First of all, you need to create your form on it's own class, that way you can d
:::php
class MyForm extends Form {
public function __construct($controller, $name) {
$fields = new FieldList(
new TextField('FirstName', 'First name'),
new EmailField('Email', 'Email address')
);
public function __construct($controller, $name) {
$fields = new FieldList(
new TextField('FirstName', 'First name'),
new EmailField('Email', 'Email address')
);
$actions = new FieldList(
new FormAction('submit', 'Submit')
);
$actions = new FieldList(
new FormAction('submit', 'Submit')
);
parent::__construct($controller, $name, $fields, $actions);
}
parent::__construct($controller, $name, $fields, $actions);
}
public function forTemplate() {
return $this->renderWith(array(
$this->class,
'Form'
));
}
public function forTemplate() {
return $this->renderWith(array(
$this->class,
'Form'
));
}
public function submit($data, $form) {
// do stuff here
}
public function submit($data, $form) {
// do stuff here
}
}
@ -201,31 +165,31 @@ basic customisation:
:::ss
<form $FormAttributes>
<% if Message %>
<p id="{$FormName}_error" class="message $MessageType">$Message</p>
<% else %>
<p id="{$FormName}_error" class="message $MessageType" style="display: none"></p>
<% end_if %>
<% if Message %>
<p id="{$FormName}_error" class="message $MessageType">$Message</p>
<% else %>
<p id="{$FormName}_error" class="message $MessageType" style="display: none"></p>
<% end_if %>
<fieldset>
<div id="FirstName" class="field text">
<label class="left" for="{$FormName}_FirstName">First name</label>
$dataFieldByName(FirstName)
</div>
<fieldset>
<div id="FirstName" class="field text">
<label class="left" for="{$FormName}_FirstName">First name</label>
$dataFieldByName(FirstName)
</div>
<div id="Email" class="field email">
<label class="left" for="{$FormName}_Email">Email</label>
$dataFieldByName(Email)
</div>
<div id="Email" class="field email">
<label class="left" for="{$FormName}_Email">Email</label>
$dataFieldByName(Email)
</div>
$dataFieldByName(SecurityID)
</fieldset>
$dataFieldByName(SecurityID)
</fieldset>
<% if Actions %>
<div class="Actions">
<% control Actions %>$Field<% end_control %>
</div>
<% end_if %>
<% if Actions %>
<div class="Actions">
<% loop Actions %>$Field<% end_loop %>
</div>
<% end_if %>
</form>
`$dataFieldByName(FirstName)` will return the form control contents of `Field()` for the particular field object, in
@ -234,7 +198,7 @@ for the type of field. Pass in the name of the field as the first parameter, as
template.
To find more methods, have a look at the `[api:Form]` class, as there is a lot of different methods of customising the form
templates, for example, you could use `<% control Fields %>` instead of specifying each field manually, as we've done
templates, for example, you could use `<% loop Fields %>` instead of specifying each field manually, as we've done
above.
### Custom form field templates

View File

@ -14,7 +14,7 @@ This example might come from a Controller designed to manage the members of a gr
*/
public function MemberForm() {
$field = new GridField("Members", "Members of this group", $this->group->Members());
return new Form("MemberForm", $this, new FieldSet($field), new FieldSet());
return new Form("MemberForm", $this, new FieldList($field), new FieldList());
}
Note that the only way to specify the data that is listed in a grid field is with `SS_List` argument. If you want to customise the data displayed, you can do so by customising this object.
@ -166,7 +166,7 @@ Objects can be searched through an input field (partially matching one or more f
Selecting from the results will add the object to the relation.
:::php
$group = DataObject::get_one('Group');
$group = Group::get()->First();
$config = GridFieldConfig::create()->addComponent(new GridFieldAddExistingAutocompleter(array('FirstName', 'Surname', 'Email'));
$gridField = new GridField('Members', 'Members', $group->Members(), $config);

View File

@ -70,9 +70,9 @@ to write your own logic for any frontend output.
i18n::set_date_format('dd.MM.YYYY');
i18n::set_time_format('HH:mm');
Most localization routines in SilverStripe use the [http://framework.zend.com/manual/en/zend.date.html](Zend_Date API).
Most localization routines in SilverStripe use the [Zend_Date API](http://framework.zend.com/manual/en/zend.date.html).
This means all formats are defined in
[http://framework.zend.com/manual/en/zend.date.constants.html#zend.date.constants.selfdefinedformats](ISO date format),
[ISO date format](http://framework.zend.com/manual/en/zend.date.constants.html#zend.date.constants.selfdefinedformats),
not PHP's built-in [date()](http://nz.php.net/manual/en/function.date.php).
### i18n in URLs
@ -227,22 +227,20 @@ which supports different translation adapters, dealing with different storage fo
By default, SilverStripe 3.x uses a YAML format (through the [Zend_Translate_RailsYAML adapter](https://github.com/chillu/zend_translate_railsyaml)).
Example: sapphire/lang/en.yml (extract)
Example: framework/lang/en.yml (extract)
:::yml
en:
ImageUploader:
Attach: 'Attach %s'
FileIFrameField:
UploadField:
NOTEADDFILES: 'You can add files once you have saved for the first time.'
Translation table: sapphire/lang/de.yml (extract)
Translation table: framework/lang/de.yml (extract)
:::yml
de:
ImageUploader:
ATTACH: '%s anhängen'
FileIframeField:
UploadField:
NOTEADDFILES: 'Sie können Dateien hinzufügen sobald Sie das erste mal gespeichert haben'
Note that translations are cached across requests.
@ -262,7 +260,7 @@ Example: framework/lang/en_US.php (extract)
'Attach %s',
'Attach image/file'
);
$lang['en_US']['FileIFrameField']['NOTEADDFILES'] = 'You can add files once you have saved for the first time.';
$lang['en_US']['UploadField']['NOTEADDFILES'] = 'You can add files once you have saved for the first time.';
// ...
@ -270,7 +268,7 @@ Translation table: framework/lang/de_DE.php (extract)
:::php
$lang['de_DE']['ImageUploader']['ATTACH'] = '%s anhängen';
$lang['de_DE']['FileIframeField']['NOTEADDFILES'] = 'Sie können Dateien hinzufügen sobald Sie das erste mal gespeichert haben';
$lang['de_DE']['UploadField']['NOTEADDFILES'] = 'Sie können Dateien hinzufügen sobald Sie das erste mal gespeichert haben';
In order to enable usage of PHP language definitions in 3.x, you need to register a legacy adapter
in your `mysite/_config.php`:

View File

@ -30,7 +30,4 @@ It is where most documentation should live, and is the natural "second step" aft
* [Testing](testing): Functional and Unit Testing with PHPUnit and SilverStripe's testing framework
* [Developing Themes](theme-development): Package templates, images and CSS to a reusable theme
* [Widgets](widgets): Small feature blocks which can be placed on a page by the CMS editor, also outlines how to create and add widgets
## Feedback
If you have a topic you would like covered in these section please ask for it on our [Bug Tracker](http://open.silverstripe.org)
* [Versioning](versioning): Extension for SiteTree and other classes to store old versions and provide "staging"

View File

@ -375,9 +375,9 @@ Template:
:::ss
<ul>
<% control Results %>
<% loop Results %>
<li id="Result-$ID">$Title</li>
<% end_control %>
<% end_loop %>
</ul>
@ -386,8 +386,7 @@ PHP:
:::php
class MyController {
function autocomplete($request) {
$SQL_title = Convert::raw2sql($request->getVar('title'));
$results = DataObject::get("Page", "Title = '$SQL_title'");
$results = Page::get()->filter("Title", $request->getVar('title'));
if(!$results) return new HTTPResponse("Not found", 404);
// Use HTTPResponse to pass custom status messages

View File

@ -1,68 +1,227 @@
# Building templates for page types
Much of your work building a SilverStripe site will involve the creation of templates for your [page types](/topics/page-types). SilverStripe has its own template language, which is described in full [here](/reference/templates).
Much of your work building a SilverStripe site will involve the creation of
templates for your own page types. SilverStripe has its own template language.
Its basic features like variables, blocks and loops are described in our ["templates" reference guide](/reference/templates).
In this guide, we'll show you specific uses for creating page layouts.
This assumes you are familiar with the concept of ["page types"](/topics/page-types).
SilverStripe templates consist of HTML code augmented with special control codes, described below. Because of this, you can have as much control of your site's HTML code as you like.
To get a feel for what those templates look like, let's have a look at an abbreviated example. In your webroot, these templates are usually located in `themes/<your-theme>/templates`.
Replace the `<your-theme>` placeholder accordingly, most likely you're using a theme called "simple")
Most of the magic happens in `Page.ss` and `Layout/Page.ss`.
Take a look at mysite/templates/Page.ss. It contains standard HTML markup, with some extra tags. You can see that this file only generates some of the content it sets up the `<html>` tags, deals with the `<head>` section, creates the first-level navigation, and then closes it all off again. See `$Layout`? Thats what is doing most of the work when you visit a page.
Now take a look at `mysite/templates/Layout/Page.ss`. This as you can see has a lot more markup in it its what is included into `$Layout` when the Page page type is rendered. Similarly, `mysite/templates/Layout/HomePage.ss` would be rendered into `$Layout` when the HomePage page type is selected for the current page youre viewing.
Here is a very simple pair of templates. We shall explain their contents item by item.
`templates/Page.ss`
`themes/<your-theme>/templates/Page.ss`
:::ss
<html>
<%-- This is my first template --%>
<head>
<% base_tag %>
<title>$Title</title>
$MetaTags
<title>$SiteConfig.Title | $Title</title>
$MetaTags(false)
</head>
<body>
<div id="Container">
<div id="Header">
<header>
<h1>Bob's Chicken Shack</h1>
<% with $CurrentMember %>
<p>You are logged in as $FirstName $Surname.</p>
<% end_with %>
</div>
<div id="Navigation">
</header>
<navigation>
<% if $Menu(1) %>
<ul>
<% loop $Menu(1) %>
<li><a href="$Link" title="Go to the $Title page" class="$LinkingMode">$MenuTitle</a></li>
<li><a href="$Link" class="$LinkingMode">$MenuTitle</a></li>
<% end_loop %>
</ul>
<% end_if %>
</div>
</navigation>
<div class="typography">
$Layout
</div>
<div id="Footer">
<p>Copyright $Now.Year</p>
</div>
</div>
</body>
</html>
`templates/Layout/Page.ss`
`themes/<your-theme>/templates/Layout/Page.ss`
<h1>$Title</h1>
<h2>$Title</h2>
$Content
$Form
## <%-- This is my first template --%>
### Template inheritance through $Layout
This is a comment. Like HTML comments, these tags let you include explanatory information in your comments. Unlike HTML comments, these tags won't be included in the HTML file downloaded by your visitors.
Our example shows two templates, both called `Page.ss`.
One is located in the `templates/` "root" folder, the other one in a `templates/Layout/` subfolder.
This "inner template" is used by the `$Layout` placeholder in the "root template",
and is inherited based on the underlying PHP classes (read more about template inheritance
on the ["page types" topic](/topics/page-types)).
## <% base_tag %>
"Layout" is a fixed naming convention,
you can't use the same pattern for other folder names.
This tag must always appear in the `<head>` of your templates. SilverStripe uses a combination of a site-wide base tag and relative links to ensure that a site can function when loaded into a subdirectory on your webserver, as this is handy when developing a site. For more information see the [templates reference](/reference/templates#base-tag)
### Page Content
### $MetaTags
:::ss
$Content
This variable in the `Layout` template contains the main content of the current page,
edited through the WYSIWIG editor in the CMS.
It returns the database content of the `SiteTree.Content` property.
Please note that this database content can be "staged",
meaning that draft content edited in the CMS can be different from published content
shown to your website visitors. In templates, you don't need to worry about this distinction.
The `$Content` variable contain the published content by default,
and only preview draft content if explicitly requested (e.g. by the "preview" feature in the CMS)
(see the ["versioning" topic](topics/versioning) for more details).
### Menu Loops
:::ss
<% loop $Menu(1) %>...<% end_loop %>
`$Menu(1)` is a built-in page control that defines the top-level menu.
You can also create a sub-menu using `$Menu(2)`, and so forth.
The `<% loop $Menu(1) %>...<% end_loop %>` block defines a repeating element.
It will change the "scope" of your template, which means that all of the template variables you use inside it will refer to a menu item. The template code will be repeated once per menu item, with the scope set to that menu item's page. In this case, a menu item refers to an instance
of the `Page` class, so you can access all properties defined on there, for example `$Title`.
Note that pages with the `ShowInMenus` property set to FALSE will be filtered out
(its a checkbox in the "Settings" panel of the CMS).
### Children Loops
:::ss
<% loop Children %>...<% end_loop %>
Will loop over all children of the current page context.
Helpful to create page-specific subnavigations.
Most likely, you'll want to use `<% loop Menu %>` for your main menus,
since its independent of the page context.
:::ss
<% loop ChildrenOf(<my-page-url>) %>...<% end_loop %>
Will create a list of the children of the given page,
as identified by its `URLSegment` value. This can come in handy because its not dependent
on the context of the current page. For example, it would allow you to list all staff member pages
underneath a "staff" holder on any page, regardless if its on the top level or elsewhere.
:::ss
<% loop allChildren %>...<% end_loop %>
This will show all children of a page even if the `ShowInMenus` property is set to FALSE.
### Access to Parent and Level Pages
:::ss
<% with $Level(1) %>
$Title
<% end_with %>
Will return a page in the current path, at the level specified by the numbers.
It is based on the current page context, looking back through its parent pages.
For example, imagine you're on the "bob marley" page,
which is three levels in: "about us > staff > bob marley".
* `$Level(1).Title` would return "about us"
* `$Level(2).Title` would return "staff"
* `$Level(3).Title` would return "bob marley"
To simply retrieve the parent page of the current context (if existing), use the `$Parent` variable.
### Access to a specific Page
:::ss
<% loop Page(my-page) %>...<% end_loop %>`
"Page" will return a single page from the site tree, looking it up by URL. You can use it in the `<% loop %>` format.
Can't be called using `$Page(my-page).Title`.
### Title and Menu Title
The CMS provides two fields to label a page: "Title" and "Menu Title".
"Title" is the title in its full length, while "Menu Title" can be
a shorter version suitable for size-constrained menus.
If "Menu Title" is left blank by the CMS author, it'll just default to "Title".
### Links and Linking Modes
:::ss
$LinkingMode
Each menu item we loop over knows its location on the website, so can generate a link to it.
This happens through the `[api:SiteTree->Link()]` method behind the scenes.
We're not using the direct database property `SiteTree.URLSegment` here
because pages can be nested, so the link needs to be generated on the fly.
In the template syntax, there's no distinction between a method and a property though.
The link is relative by default (see `<% base_tag %>`),
you can get an absolute one including the domain through [$AbsoluteLink](api:SiteTree->AbsoluteLink())`.
In addition, each menu item gets some context information relative
to the page you're currently viewing, contained in the `$LinkingMode` placeholder.
By setting a HTML class to this value, you can distinguish the styling of
the currently selected menu item. It can have the following values:
* `link`: You are neither on this page nor in this section.
* `current`: You are currently on this page.
* `section`: The current page is a child of this menu item, so the current "section"
More common uses:
* `$LinkOrCurrent`: Determines if the item is the current page. Returns "link" or "current" strings.
* `$LinkOrSection`: Determines if the item is in the current section, so in the path towards the current page. Useful for menus which you only want to show a second level menu when you are on that page or a child of it. Returns "link" or "section" strings.
* `InSection(page-url)`: This if block will pass if we're currently on the page-url page or one of its children.
Example: Only show the menu item linked if its the current one:
:::ss
<% if LinkOrCurrent = current %>
$Title
<% else %>
<a href="$Link">$Title</a>
<% end_if %>
### Breadcrumbs
Breadcrumbs are the path of parent pages which needs to be taken
to reach the current page, and can be a great navigation aid for website users.
While you can achieve breadcrumbs through the `<% Level(<level>) %>` control already,
there's a nicer shortcut: The `$Breadcrumbs` control.
It uses its own template defined in `BreadcrumbsTemplate.ss`.
Simply place a file with the same name in your `themes/<your-theme>/templates`
folder to customize its output. Here's the default template:
:::ss
<% if Pages %>
<% loop Pages %>
<% if Last %>$Title.XML<% else %><a href="$Link">$MenuTitle.XML</a> &raquo;<% end_if %>
<% end_loop %>
<% end_if %>
For more customization options like limiting the amount of breadcrumbs,
take a look at `[api:SiteTree->Breadcrumbs()]`.
### SiteConfig: Global settings
:::ss
$SiteConfig.Title
The ["SiteConfig"](/reference/siteconfig) object allows content authors
to modify global data in the CMS, rather than PHP code.
By default, this includes a website title and tagline
(as opposed to the title of a specific page).
It can be extended to hold other data, for example a logo image
which can be uploaded through the CMS.
The object is available to all page templates through the `$SiteConfig` placeholder.
### Meta Tags
The `$MetaTags` placeholder in a template returns a segment of HTML appropriate for putting into the `<head>` tag. It
will set up title, keywords and description meta-tags, based on the CMS content and is editable in the 'Meta-data' tab
@ -73,63 +232,54 @@ By default `$MetaTags` renders:
:::ss
<title>Title of the Page</title>
<meta name="generator" http-equiv="generator" content="SilverStripe 2.0" >
<meta http-equiv="Content-type" content="text/html; charset=utf-8" >
<meta name="generator" http-equiv="generator" content="SilverStripe 3.0" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
### <% with $CurrentMember %>...<% end_with %>
#### URLSegment
This is a "with" block. A with block will change the "scope" of the template, so that all template variables inside that block will contain values from the $CurrentMember object, rather than from the page being rendered.
This returns the part of the URL of the page you're currently on.
Shouldn't be used for linking to a page, since the link
is a composite value based on all parent pages as well (through the `$Link` variable).
`$CurrentMember` is an object with information about the currently logged in member. If no-one is logged in, then it's blank. In that case, the entire `<% with $CurrentMember %>` block will be omitted.
#### ClassName
### $FirstName, $Surname
Returns the class of the underlying `Page` record.
This can be handy to add to your `<body>` tag to influence
CSS styles and JavaScript behaviour based on the page type used:
These two variables come from the `$CurrentMember` object, because they are inside the `<% with $CurrentMember %>` block. In particular, they will contain the first name and surname of the currently logged in member.
:::ss
<body class="$ClassName">
### <% if $Menu(1) %>...<% end_if %>
In case you want to include parent PHP classes in this list as well,
use the `$CSSClasses` placeholder instead.
This template code defines a conditional block. Its contents will only be shown if `$Menu(1)` contains anything.
#### BaseHref
`$Menu(1)` is a built-in page control that defines the top-level menu. You can also create a sub-menu using `$Menu(2)`, and a third-level menu using using `$Menu(3)`, etc.
Returns the base URL for the current site.
This is used to populate the `<base>` tag by default.
Can be handy to prefix custom links (not generated through `SiteTree->Link()`),
to ensure they work correctly when the webroot is hosted in a subfolder
rather than its own domain (a common development setup).
### <% loop $Menu(1) %>...<% end_loop %> %>
### Forms
This template code defines a repeating element. `$Menu(1)`. Like `<% with %>`, the loop block will change the "scope" of your template, which means that all of the template variables you use inside it will refer to a menu item. The template code will be repeated once per menu item, with the scope set to that menu item's page.
:::ss
$Form
### $Link, $Title, $MenuTitle
Very often, a page will contain some content and a form of some kind. For example, the log-in page has a log-in form. If you are on such a page, the `$Form` variable will contain the HTML content of the form. Placing it just below `$Content` is a good default. Behind the scenes,
it maps to the `Page_Controller->Form()` method. You can add more forms by implementing
new methods there (see ["forms" topic](/topics/forms) for details).
Because these 3 variables are within the repeating element, then their values will come from that repeating element. In this case, they will be the values of each menu item.
### More Advanced Controls
* `$Link`: A link to that menu item.
* `$Title`: The page title of that menu item.
* `$MenuTitle`: The navigation label of that menu item.
Template variables and controls are just PHP properties and methods
on the underlying controllers and model classes.
We've just shown you the most common once, in practice
you can use any public API on those classes, and [extend](/reference/dataextension) them
with your own. To get an overview on what's available to you,
we recommend that you dive into the API docs for the following classes:
### $LinkingMode
Once again, this is a variable that will be source from the menu item. This variable differs for each menu item, and will be set to one of these 3 values:
* `link`: You are neither on this page nor in this section.
* `current`: You are currently on this page.
* `section`: The current page is a child of this menu item; so this is menu item identifies the section that you're currently in.
By setting the HTML class to this value, you can distinguish the styling of the currently selected menu item.
### $Layout
This variable will be replaced with the the rendered version of `templates/Layout/Page.ss`. If you create a page type that is a subclass of Page, then it is possible to only define `templates/Layout/MySubclassPage.ss`. In that case, then the rendered version of `templates/Layout/MySubclassPage.ss` wil be inserted into the `$Layout` variable in `templates/Page.ss`. This is a good way of defining a single main template and page specific sub-templates.
### $Now.Year
This will return the current year. `$Now` returns an `SS_Datetime` object, which has a number of methods, such as `Year`. See [the API documentation](api:SS_Datetime) for a list of all the methods.
### $Title
This is the same template code as used in the title attribute of your navgation. However, because we are using it outside of the `<% loop Menu(1) >` block, it will return the title of the current page, rather than the title of the menu item. We use this to make our main page title.
### $Content
This variable contains the content of the current page.
### $Form
Very often, a page will contain some content and a form of some kind. For example, the log-in page has a log-in form. If you are on such a page, this variable will contain the HTML content of the form. Putting it just below $Content is a good default.
* `[api:ContentController]`: The main controller responsible for handling pages
* `[api:Controller]`: Generic controller (not specific to pages)
* `[api:DataObject]`: Underlying model class for page objects
* `[api:ViewableData]`: Underlying object class for pretty much anything displayable

View File

@ -4,23 +4,18 @@
Page Types are the basic building blocks of any SilverStripe website. A page type can define:
* The template or templates that are used to display content
* What fields are available to edit in the CMS
* Behaviour specific to a page type for example a contact form on the Contact Us page that sends an email
when the form is submitted
* Templates being used to display content
* Form fields available to edit content in the CMS
* Behaviour specific to a page type. For example a contact form on a Contact Us page type, sending an email when the form is submitted
All the pages on the base installation are of the page type "Page". See
All the pages on the base installation are of the page type called "Page". See
[tutorial:2-extending-a-basic-site](/tutorials/2-extending-a-basic-site) for a good introduction to page-types.
## Page type templates
## Class and Template Inheritance
Each page type on your website is a sub-class of the SiteTree class. Usually, youll define a class called Page
Each page type on your website is a sub-class of the `SiteTree` class. Usually, youll define a class called `Page`
and use this template to lay out the basic design elements that dont change.
Why do we sub-class Page for everything? The easiest way to explain this is to use the example of a search form. If we
create a search form on the Page class, then any other sub-class can also use it in their templates. This saves us
re-defining commonly used forms or controls in every class we use.
![](_images/pagetype-inheritance.png)
Each page type is represented by two classes: a data object and a controller. In the diagrams above and below, the data
@ -31,55 +26,47 @@ we want to do to the CMS for this page type in here.
![](_images/controllers-and-dataobjects.png)
Page types are created using PHP classes. If youre not sure about how these work, [click here for a gentler
introduction to PHP classes](http://www-128.ibm.com/developerworks/opensource/library/os-phpobj/).
We put the `Page` class into a file called `Page.php` inside `mysite/code`.
As a convention, we also put the `Page_Controller` class in the same file.
We put the Page class into a file called Page.php inside `mysite/code`. We also put Page_Controller in here. Any other
classes that are based on Page for example, the class Page_AnythingElse will also go into Page.php. Likewise, the
StaffPage_Image class will go into StaffPage.php.
Why do we sub-class `Page` for everything? The easiest way to explain this is to use the example of a search form. If we
create a search form on the `Page` class, then any other sub-class can also use it in their templates. This saves us
re-defining commonly used forms or controls in every class we use.
## Templates
Take a look at mysite/templates/Page.ss. It contains standard HTML markup, with some differences. Well go over
these later, but for now, you can see that this file only generates some of the content it sets up the
`<html>` tags, deals with the `<head>` section, creates the first-level navigation, and then closes it all off again.
See $Layout? Thats what is doing most of the work when you visit a page. Now take a look at `mysite/templates/Layout/Page.ss`.
This as you can see has a lot more markup in it its what is included into $Layout when the Page page type is rendered.
Similarly, `mysite/templates/Layout/HomePage.ss` would be rendered into $Layout when the HomePage page type is selected for the
current page youre viewing.
Page type templates work much the same as other [templates](/reference/templates) in SilverStripe
(see ). There's some specialized controls and placeholders, as well as built-in inheritance.
This is explained in a more in-depth topic at [Page Type Templates](/topics/page-type-templates).
See the [Page Type Templates](/topics/page-type-templates) page for more information.
## Adding database-fields
## Adding Database Fields
Adding database fields is a simple process. You define them in an array of the static variable `$db`, this array is
added on the object class. For example, Page or StaffPage. Every time you run db/build to recompile the manifest, it
checks if any new entries are added to the `$db` array and adds any fields to the database that are missing.
For example, you may want an additional field on a StaffPage class which extends Page, called Author. Author is a
standard text field, and can be [casted](/topics/datamodel) as a variable character object in php (VARCHAR in SQL). In the
following example, our Author field is casted as a variable character object with maximum characters of 50. This is
For example, you may want an additional field on a `StaffPage` class which extends `Page`, called `Author`. `Author` is a
standard text field, and can be [casted](/topics/datamodel) as a variable character object in php (`VARCHAR` in SQL). In the
following example, our `Author` field is casted as a variable character object with maximum characters of 50. This is
especially useful if you know how long your source data needs to be.
:::php
class StaffPage extends Page {
static $db = array(
'Author' => 'Varchar(50)'
);
}
class StaffPage_Controller extends Page_Controller {
}
See [datamodel](/topics/datamodel) for a more detailed explanation on adding database fields, and how the SilverStripe data
model works.
## Adding formfields and tabs
## Adding Form Fields and Tabs
See [form](/topics/forms) and [tutorial:2-extending-a-basic-site](/tutorials/2-extending-a-basic-site)
See [form](/topics/forms) and [tutorial:2-extending-a-basic-site](/tutorials/2-extending-a-basic-site).
Note: To modify fields in the "Settings" tab, you need to use `updateSettingsFields()` instead.
## Removing inherited form fields and tabs
@ -134,35 +121,3 @@ Metadata tab.
For more information on forms, see [form](/topics/forms), [tutorial:2-extending-a-basic-site](/tutorials/2-extending-a-basic-site)
and [tutorial:3-forms](/tutorials/3-forms).
## Creating a new page:
:::php
$page = new Page();
$page->ParentID = 18; //if you want it to be a child of a certain other page...
$page->Title = "Crazy page";
$page->MetaTitle = "madness";
$page->PageTitle = "Funny";
$page->writeToStage('Stage');
$page->publish('Stage', 'Live');
## Updating a page:
:::php
$page = DataObject::get_one("Page", "ParentID = 18");
$page->Title = "More Serious";
$page->writeToStage('Stage');
$page->Publish('Stage', 'Live');
$page->Status = "Published";
## Deleting pages:
:::php
$pageID = $page->ID;
$stageRecord = Versioned::get_one_by_stage('SiteTree', 'Stage', "SiteTree.ID = $pageID");
if ($stageRecord) $stageRecord->delete();
$liveRecord = Versioned::get_one_by_stage('SiteTree', 'Live', "SiteTree_Live.ID = $pageID");
if ($liveRecord) $liveRecord->delete();

View File

@ -17,7 +17,7 @@ It is usually added through the `[api:DataObject->getCMSFields()]` method:
static $db = array('Content' => 'HTMLText');
public function getCMSFields() {
return new FieldSet(new HTMLEditorField('Content'));
return new FieldList(new HTMLEditorField('Content'));
}
}
@ -121,10 +121,10 @@ of the CMS you have to take care of instanciation yourself:
:::ss
// File: mysite/templates/MyController.ss
$Form
<% control EditorToolbar %>
<% with EditorToolbar %>
$MediaForm
$LinkForm
<% end_control %>
<% end_with %>
Note: The dialogs rely on CMS-access, e.g. for uploading and browsing files,
so this is considered advanced usage of the field.

View File

@ -8,7 +8,7 @@ See [Tutorial: Site Search](/tutorials/4-site-search) for details.
## Searching for DataObjects
The `[api:SearchContext]` class provides a good base implementation that you can hook into your own controllers.
A working implementation of searchable DataObjects can be seen in the `[api:ModelAdmin]` class.
A working implementation of searchable DataObjects can be seen in the `[ModelAdmin](/reference/modeladmin)` class.
[SearchContext](/reference/searchcontext) goes into more detail about setting up a default search form for `[api:DataObject]`s.
@ -33,7 +33,7 @@ dedicated search service like the [sphinx module](http://silverstripe.org/sphinx
## Related
* `[api:ModelAdmin]`
* [ModelAdmin](/reference/modeladmin)
* [RestfulServer module](https://github.com/silverstripe/silverstripe-restfulserver)
* [Tutorial: Site Search](/tutorials/4-site-search)
* [SearchContext](/reference/searchcontext)

View File

@ -24,6 +24,7 @@ For `[api:MySQLDatabase]`, this will be `[mysql_real_escape_string()](http://de3
* DataObject::castedUpdate()
* DataObject->Property = 'val', DataObject->setField('Property','val')
* DataObject::write()
* DataList->byID()
* Form->saveInto()
* FormField->saveInto()
* DBField->saveInto()
@ -65,7 +66,7 @@ Example:
class MyForm extends Form {
public function save($RAW_data, $form) {
$SQL_data = Convert::raw2sql($RAW_data); // works recursively on an array
$objs = DataObject::get('Player', "Name = '{$SQL_data[name]}'");
$objs = Player::get()->where("Name = '{$SQL_data[name]}'");
// ...
}
}
@ -80,7 +81,7 @@ Example:
class MyController extends Controller {
public function myurlaction($RAW_urlParams) {
$SQL_urlParams = Convert::raw2sql($RAW_urlParams); // works recursively on an array
$objs = DataObject::get('Player', "Name = '{$SQL_data[OtherID]}'");
$objs = Player::get()->where("Name = '{$SQL_data[OtherID]}'");
// ...
}
}
@ -314,8 +315,8 @@ Below is an example with different ways you would use this casting technique:
// cast the 'category' GET variable as an integer
$categoryID = (int)$_GET['category'];
// perform a get_by_id, ensure the ID is an integer before querying
return DataObject::get_by_id('CaseStudy', $categoryID);
// perform a byID(), which ensures the ID is an integer before querying
return CaseStudy::get()->byID($categoryID);
}

View File

@ -30,7 +30,7 @@ but rather just the returned collection,
:::php
$myPage = $this->objFromFixture('Page', 'mypage');
$myOtherPage = $this->objFromFixture('Page', 'myotherpage');
$pages = DataObject::get('Page');
$pages = Page::get();
// Bad: Assumptions about IDs and their order
$this->assertEquals(array(1,2), $pages->column('ID'));
// Good: Uses actually created IDs, independent of their order

View File

@ -74,9 +74,9 @@ our theme in action. The code for mine is below.
<div id="Navigation">
<% if Menu(1) %>
<ul>
<% control Menu(1) %>
<% loop Menu(1) %>
<li><a href="$Link" title="Go to the $Title page" class="$LinkingMode">$MenuTitle</a></li>
<% end_control %>
<% end_loop %>
</ul>
<% end_if %>
</div>
@ -99,7 +99,6 @@ our theme in action. The code for mine is below.
<h1>$Title</h1>
$Content
$Form
$PageComments
All you have to do now is tell your site to use your new theme - This is defined in the **mysite/_config.php** file
@ -183,9 +182,9 @@ Next is a division for the main navigation. This may contain something like:
<div id="Navigation">
<% if Menu(1) %>
<ul>
<% control Menu(1) %>
<% loop Menu(1) %>
<li><a href="$Link" title="Go to the $Title page" class="$LinkingMode">$MenuTitle</a></li>
<% end_control %>
<% end_loop %>
</ul>
<% end_if %>
</div>
@ -193,14 +192,14 @@ Next is a division for the main navigation. This may contain something like:
This is the standard for creating the main Navigation. As you can see it outputs the Menu 1 in a unordered list.
Before stepping into a control (a foreach loop) it's good practise to check if it exists first. This is not only
Before stepping into a loop it's good practise to check if it exists first. This is not only
important in manipulating SilverStripe templates, but in any programming language!
:::ss
<% if MyFunction %>
<% control MyFunction %>
<% loop MyFunction %>
$Title
<% end_control %>
<% end_loop %>
<% end_if %>

View File

@ -0,0 +1,173 @@
# Versioning of Database Content
## Overview
Database content in SilverStripe can be "staged" before its publication,
as well as track all changes through the lifetime of a database record.
It is most commonly applied to pages in the CMS (the `SiteTree` class).
This means that draft content edited in the CMS can be different from published content
shown to your website visitors.
The versioning happens automatically on read and write.
If you are using the SilverStripe ORM to perform these operations,
you don't need to alter your existing calls.
Versioning in SilverStripe is handled through the `[api:Versioned]` class.
It's a `[api:DataExtension]`, which allow it to be applied to any `[api:DataObject]` subclass.
## Configuration
Adding versioned to your `DataObject` subclass works the same as any other extension.
It accepts two or more arguments denoting the different "stages",
which map to different database tables.
:::php
// mysite/_config.php
Object::add_extension('MyRecord', 'Versioned("Stage","Live")');
Note: The extension is automatically applied to `SiteTree` class.
## Database Structure
Depending on how many stages you configured, two or more new tables will be created for your records.
Note that the "Stage" naming has a special meaning here, it will leave the original
table name unchanged, rather than adding a suffix.
* `MyRecord` table: Contains staged data
* `MyRecord_Live` table: Contains live data
* `MyRecord_versions` table: Contains a version history (new record created on each save)
Similarly, any subclass you create on top of a versioned base
will trigger the creation of additional tables, which are automatically joined as required:
* `MyRecordSubclass` table: Contains only staged data for subclass columns
* `MyRecordSubclass_Live` table: Contains only live data for subclass columns
* `MyRecordSubclass_versions` table: Contains only version history for subclass columns
## Usage
### Reading Versions
By default, all records are retrieved from the "Draft" stage (so the `MyRecord` table in our example).
You can explicitly request a certain stage through various getters on the `Versioned` class.
:::php
// Fetching multiple records
$stageRecords = Versioned::get_by_stage('MyRecord', 'Stage');
$liveRecords = Versioned::get_by_stage('MyRecord', 'Live');
// Fetching a single record
$stageRecord = Versioned::get_one_by_stage('MyRecord', 'Stage')->byID(99);
$liveRecord = Versioned::get_one_by_stage('MyRecord', 'Live')->byID(99);
### Historical Versions
The above commands will just retrieve the latest version of its respective stage for you,
but not older versions stored in the `<class>_versions` tables.
:::php
$historicalRecord = Versioned::get_version('MyRecord', <record-id>, <version-id>);
Caution: The record is retrieved as a `DataObject`, but saving back modifications
via `write()` will create a new version, rather than modifying the existing one.
In order to get a list of all versions for a specific record,
we need to generate specialized `[api:Versioned_Version]` objects,
which expose the same database information as a `DataObject`,
but also include information about when and how a record was published.
:::php
$record = MyRecord::get()->byID(99); // stage doesn't matter here
$versions = $record->allVersions();
echo $versions->First()->Version; // instance of Versioned_Versoin
### Writing Versions and Changing Stages
The usual call to `DataObject->write()` will write to whatever stage is currently
active, as defined by the `Versioned::current_stage()` global setting.
Each call will automatically create a new version in the `<class>_versions` table.
To avoid this, use `[writeWithoutVersion()](api:Versioned->writeWithoutVersion())` instead.
To move a saved version from one stage to another,
call `[writeToStage(<stage>)](api:Versioned->writeToStage())` on the object.
The process of moving a version to a different stage is also called "publishing",
so we've created a shortcut for this: `publish(<from-stage>, <to-stage>)`.
:::php
$record = Versioned::get_by_stage('MyRecord', 'Stage')->byID(99);
$record->MyField = 'changed';
// will update `MyRecord` table (assuming Versioned::current_stage() == 'Stage'),
// and write a row to `MyRecord_versions`.
$record->write();
// will copy the saved record information to the `MyRecord_Live` table
$record->publish('Stage', 'Live');
Similarly, an "unpublish" operation does the reverse, and removes a record
from a specific stage.
:::php
$record = MyRecord::get()->byID(99); // stage doesn't matter here
// will remove the row from the `MyRecord_Live` table
$record->deleteFromStage('Live');
### Forcing the Current Stage
The current stage is stored as global state on the object.
It is usually modified by controllers, e.g. when a preview is initialized.
But it can also be set and reset temporarily to force a specific operation
to run on a certain stage.
:::php
$origMode = Versioned::get_reading_mode(); // save current mode
$obj = MyRecord::getComplexObjectRetrieval(); // returns 'Live' records
Versioned::set_reading_mode('Stage'); // temporarily overwrite mode
$obj = MyRecord::getComplexObjectRetrieval(); // returns 'Stage' records
Versioned::set_reading_mode($origMode); // reset current mode
### Custom SQL
We generally discourage writing `Versioned` queries from scratch,
due to the complexities involved through joining multiple tables
across an inherited table scheme (see `[api:Versioned->augmentSQL()]`).
If possible, try to stick to smaller modifications of the generated `DataList` objects.
Example: Get the first 10 live records, filtered by creation date:
:::php
$records = Versioned::get_by_stage('MyRecord', 'Live')->limit(10)->sort('Created', 'ASC');
### Permissions
The `Versioned` extension doesn't provide any permissions on its own,
but you can have a look at the `SiteTree` class for implementation samples,
specifically `canPublish()` and `canDeleteFromStage()`.
### Page Specific Operations
Since the `Versioned` extension is primarily used for page objects,
the underlying `SiteTree` class has some additional helpers.
See the ["sitetree" reference](/reference/sitetree) for details.
### Templates Variables
In templates, you don't need to worry about this distinction.
The `$Content` variable contain the published content by default,
and only preview draft content if explicitly requested (e.g. by the "preview" feature in the CMS).
If you want to force a specific stage, we recommend the `Controller->init()` method for this purpose.
## Recipes
### Trapping the publication event
Sometimes, you'll want to do something whenever a particular kind of page is published. This example sends an email
whenever a blog entry has been published.
:::php
class Page extends SiteTree {
// ...
public function onAfterPublish() {
mail("sam@silverstripe.com", "Blog published", "The blog has been published");
parent::onAfterPublish();
}
}

View File

@ -4,8 +4,7 @@
Welcome to the first in this series of tutorials on the SilverStripe Content Management System (CMS).
These tutorials are designed to take you from an absolute beginner to being able to build large, complex websites with
SilverStripe. We assume to begin with, that you have some XHTML, CSS and PHP knowledge. This first tutorial provides an absolute
These tutorials are designed to take you from an absolute beginner to being able to build large, complex websites with SilverStripe. We assume to begin with, that you have some XHTML, CSS and PHP knowledge. This first tutorial provides an absolute
introduction to building a simple website using SilverStripe. It will also teach you how to use the content management system at a basic level.
## What are we working towards?
@ -16,8 +15,7 @@ templates - one for the home page, and one for the rest of the site.
## Installation
You need to [download the SilverStripe software](http://www.silverstripe.org/stable-download) and install it to your local
machine or to a webserver.
You need to [download the SilverStripe software](http://www.silverstripe.org/stable-download) and install it to your local machine or to a webserver.
For more infomation about installing and configuring a webserver read the [Installation instructions and videos](../installation).
@ -27,7 +25,7 @@ This tutorial uses the SilverStripe CMS default theme 'Simple' which you will fi
After installation, open up the folder where you installed SilverStripe.
If you installed on windows with WAMP, it will likely be at *c:\wamp\wwww*. On Mac OS X with MAMP, it will likely be at */Applications/MAMP/htdocs/*
If you installed on windows with WAMP, it will likely be at *c:\wamp\wwww*. On Mac OS X, using the built in webserver, it will be in your sites directory */Sites/* (with MAMP, it will likely be at */Applications/MAMP/htdocs/*)
Let's have a look at the folder structure.
@ -36,8 +34,8 @@ Let's have a look at the folder structure.
| assets/ | | Contains images and other files uploaded via the SilverStripe CMS. You can also place your own content inside it, and link to it from within the content area of the CMS. |
| cms/ | | Contains all the files that form the CMS area of your site. Its structure is similiar to the mysite/ directory, so if you find something interesting, it should be easy enough to look inside and see how it was built. |
| framework/ | | The framework that builds both your own site and as the CMS that powers it. Youll be utilizing files in this directory often, both directly and indirectly. |
| mysite/ | | Contains all your sites code (mainly PHP and JavaScript) |
| themes/ | | Combines all images, stylesheets and templates powering your website into a reusable "theme" |
| mysite/ | | Contains all your sites code (mainly PHP) |
| themes/ | | Combines all images, stylesheets, javascript and templates powering your website into a reusable "theme" |
When designing your site you should only need to modify the *mysite*, *themes* and *assets* folders. The rest of the folders contain files and data that are not specific to any site.
@ -47,43 +45,34 @@ When designing your site you should only need to modify the *mysite*, *themes* a
![](_images/tutorial1_cms-basic.jpg)
The CMS is the area in which you can manage your site content. You can access the cms at http://localhost/admin (or http://yourdomain.com/admin if you are using you own domain name). You
will be presented with a login screen. You can login with the details you provided at installation. After logging in you
should be greeted with the CMS and a list of the pages currently in the CMS. Here you can add, delete and reorganize the pages using the buttons at the top. Clicking on a page will open it in the page editing interface pictured below (we've entered some test content).
The CMS is the area in which you can manage your site content. You can access the cms at http://localhost/your_site_name/admin (or http://yourdomain.com/admin if you are using you own domain name). You
will be presented with a login screen. Login using the details you provided at installation. After logging in you
should see the CMS interface with a list of the pages currently on your website (the site tree). Here you can add, delete and reorganize pages. If you need to delete, publish, or unpublish a page, first check "multi-selection" at the top. You will then be able to perform actions on any checked files using the "Actions" dropdown. Clicking on a page will open it in the page editing interface pictured below (we've entered some test content).
![](_images/tutorial1_cms-numbered.jpg)
1. These buttons allow you to move between the different sections in the CMS. There are four core sections in the CMS - "Pages", "Files", "Users" and "Settings". Modules may have their own sections here as well, if any are installed. In this tutorial we will be focusing on the "Pages" section.
2. While in "Pages" you can quickly move between the pages in the CMS by clicking the vertical bar between the CMS menu and the editor. This will slide out a sidebar. To hide this, click the arrow at the bottom of the sidebar.
![](_images/tutorial1_cms-numbered-2b.jpg)
3. This section allows you to edit the content for the currently selected page, as well as changing other properties of the page such as the page name and URL. The content editor has full [WYSIWYG](http://en.wikipedia.org/wiki/WYSIWYG) abilities, allow you to change formatting and insert links, images and tables.
4. These buttons allow you to save your changes to the draft copy, publish your draft copy, unpublish from the live website or remove a page from the draft website.
The SilverStripe CMS workflow stores two copies of a page, a draft and a published one. By having separate draft & published copies, we can preview draft changes in the site before publishing them to the live website. You can quickly preview your draft pages without leaving the CMS by clicking the "Preview" button.
### Page Editor
Once you have selected a page to modify from the Pages section your page will open in the Page Editior.
The Edit Page section has 3 main areas in which you can edit the content of the page, change the settings and track your revision history (These will be covered in more detail further on in the tutorials).
![](_images/tutorial1_editpage-numbered.jpg)
1. *Content* - Allows you to set the title, wysiwyg content, URL and Meta data for your page.
2. *Settings* - Here you set the type of page behavior, parent page, show in search, show in menu, and who can view or edit the page.
3. *History* - This allow you to view previous version of your page, compare change and revert to previous version if need be.
1. This menu allows you to move between different sections of the CMS. There are four core sections - "Pages", "Files", "Users" and "Settings". If you have modules installed, they may have their own sections here. In this tutorial we will be focusing on the "Pages" section.
2. The breadcrumbs on the left will show you a direct path to the page you are currently looking at. You can use this path to navigate up through a page's heirarchy. On the left there are tabs you may use to flick between different aspects of a page. By default, you should be shown three tabs: "Content", "Settings", and "History".
* Content - Allows you to set the title, wysiwyg content, URL and Meta data for your page.
* Settings - Here you set the type of page behavior, parent page, show in search, show in menu, and who can view or edit the page.
* History - This allows you to view previous version of your page, compare change and revert to previous version if need be.
3. Within the "Pages" section (provided you are in the "Content", or "Settings" tab) you can quickly move between pages in the CMS using the site tree. To collapse and expand this sidebar, click the arrow at the bottom. If you are in the history tab, you will notice the site tree has been replaced by a list of the alterations to the current page.
![](_images/tutorial1_cms-numbered-3.jpg)
4. This section allows you to edit the content for the currently selected page, as well as changing other properties of the page such as the page name and URL. The content editor has full [WYSIWYG](http://en.wikipedia.org/wiki/WYSIWYG) abilities, allow you to change formatting and insert links, images and tables.
5. These buttons allow you to save your changes to the draft copy, publish your draft copy, unpublish from the live website or remove a page from the draft website. The SilverStripe CMS workflow stores two copies of a page, a draft and a published one. By having separate draft & published copies, we can preview draft changes in the site before publishing them to the live website. You can quickly preview your draft pages without leaving the CMS by clicking the "Preview" button.
![](_images/tutorial1_cms-numbered-5.jpg)
### Try it
There are three pages already created for you - "Home", "About Us" and "Contact Us", as well as a 404 page. Experiment
with the editor - try different formatting, tables and images. When you are done, click "Save" to save the page or "Save
with the editor - try different formatting, tables and images. When you are done, click "Save Draft" or "Save
& Publish" to post the content to the live site.
When you create a new page, you are given a drop down that allows you to set the structure of the page (Top level or Under another page) and the page type.
The page type specifies the templates used to render the page, the fields that are able to be edited in the CMS, and page specific
behavior. We will explain page types in more depth as we progress; for now, make all pages of the type "Page".
### New pages
To create a new page, click the "Add New" button above the site tree.
When you create a new page, you are given the option of setting the structure of the page ("Top level" or "Under another page") and the page type.
The page type specifies the templates used to render the page, the fields that are able to be edited in the CMS, and page specific behavior. We will explain page types in more depth as we progress; for now, make all pages of the type "Page".
![](_images/tutorial1_addpage.jpg)
@ -102,12 +91,13 @@ become *about-us*. You are able to change it yourself so that you can make long
example, *Employment Opportunities* could be shortened to *jobs*. The ability to generate easy to type, descriptive URLs
for SilverStripe pages improves accessibility for humans and search engines.
You should ensure the URL for the home page is *home*. By default, SilverStripe loads the page with the URL *home*.
You should ensure the URL for the home page is *home*, as that's the page SilverStripe loads by default.
## Templates
All pages on a SilverStripe site are rendered using a template. A template is an HTML file augmented with special
All pages on a SilverStripe site are rendered using a template. A template is an file
with a special `*.ss` file extension, containing HTML augmented with some
control codes. Because of this, you can have as much control of your sites HTML code as you like.
Every page in your site has a **page type**. We will briefly talk about page types later, and go into much more detail
@ -116,31 +106,48 @@ for a template file in the *simple/templates* folder, with the name `<PageType>`
Open *themes/simple/templates/Page.ss*. It uses standard HTML apart from these exceptions:
`<% base_tag %>` is replaced with the HTML [base element](http://www.w3.org/TR/html401/struct/links.html#h-12.4). This
:::ss
<% base_tag %>
The base_tag variable is replaced with the HTML [base element](http://www.w3.org/TR/html401/struct/links.html#h-12.4). This
ensures the browser knows where to locate your site's images and css files.
*$MetaTitle, $Title, and $SiteConfig.Title* in the html <title> tag are replaced by the title set in the Meta tags, Page Name, or Settings -> Site Title.
:::ss
$MetaTitle
$Title
$SiteConfig.Title
*$Title* is simply replaced with the name of the page ('Page name' on the 'Main' tab in the editor).
These three variables are found within the html `<title>` tag, and are replaced by the text set in the "Meta Title", "Page Name", or "Settings -> Site Title" in the CMS.
*$MetaTags* adds meta tags for search engines, as well as the page title ('Title' on the 'Meta-data' tab in the
editor). You can define your metatags in the meta-data tab off the content editor in the CMS.
:::ss
$MetaTags
*$Layout* is replaced with the contents of a template file with the same name as the page type we are using.
The MetaTags variable will add meta tags, which are used by search engines. You can define your meta tags in the tab fields at the bottom of the content editor in the CMS.
:::ss
$Layout
The Layout variable is replaced with the contents of a template file with the same name as the page type we are using.
Open *themes/simple/templates/Layout/Page.ss*. You will see more HTML and more SilverStripe template replacement tags and variables.
*$Content* is replaced with the content of the page currently being viewed. This allows you to make all changes to
:::ss
$Content
The Content variable is replaced with the content of the page currently being viewed. This allows you to make all changes to
your site's content in the CMS.
These template markers are processed by SilverStripe into HTML before being sent to your
browser and are formatted either with a *$* at the beginning or are between the SilverStripe template tags *`<% %>`*.
browser and are either prefixed with a dollar sign ($)
or placed between SilverStripe template tags:
:::ss
<% %>
**Flushing the cache**
Whenever we edit a template file, we need to append *?flush=1* onto the end of the URL, e.g.
http://localhost/home/?flush=1. SilverStripe stores template files in a cache for quicker load times. Whenever there are
Whenever we edit a template file, we need to append *?flush=all* onto the end of the URL, e.g.
http://localhost/your_site_name/?flush=all. SilverStripe stores template files in a cache for quicker load times. Whenever there are
changes to the template, we must flush the cache in order for the changes to take effect.
## The Navigation System
@ -149,16 +156,23 @@ We are now going to look at how the navigation system is implemented in the temp
Open up *themes/simple/templates/Includes/Navigation.ss*
Menu for our site are created using a **loop**. Loops allow us to iterate over a data set, and render each item using a sub-template. The
**loop** *Menu(1)* returns the set of the first level menu items. We can then use the template variable
*$MenuTitle* to show the title of the page we are linking to, $Link for the URL of the page and $LinkingMode to help style our menu with CSS (explained in more detail shortly).
The Menu for our site is created using a **loop**. Loops allow us to iterate over a data set, and render each item using a sub-template.
:::ss
<% loop Menu(1) %>
returns a set of first level menu items. We can then use the template variable
*$MenuTitle* to show the title of the page we are linking to, *$Link* for the URL of the page and *$LinkingMode* to help style our menu with CSS (explained in more detail shortly).
> *$Title* refers to **Page Name** in the CMS, whereas *$MenuTitle* refers to (the often shorter) **Navigation label**
> $Title refers to *Page Name* in the CMS, whereas $MenuTitle refers to (the often shorter) *Navigation label*
:::ss
<ul>
<% loop Menu(1) %>
<li class="$LinkingMode"><a href="$Link" title="$Title.XML">$MenuTitle.XML</a></li>
<li class="$LinkingMode">
<a href="$Link" title="$Title.XML">$MenuTitle.XML</a>
</li>
<% end_loop %>
</ul>
@ -171,20 +185,25 @@ This creates the navigation at the top of the page:
## Highlighting the current page
### Highlighting the current page
A useful feature is highlighting the current page the user is looking at. We can do this with the template variable
*$LinkingMode* which we mentioned before. *$LinkingMode* returns one of three values:
A useful feature is highlighting the current page the user is looking at. We can do this with the template variable: `$LinkingMode`. It returns one of three values:
* *current* - This page is being visited, and should be highlighted
* *link* - The page is not currently being visited, so shouldn't be highlighted
* *section* - A page under this page is being visited so you probably want to highlight it.
* *current* - This page is being visited
* *link* - This page is not currently being visited
* *section* - A page under this page is being visited
> For example: if you were visiting a staff member such as "Home > Company > Staff > Bob Smith", you would want to highlight 'Company' to say you are in that section.
For example, if you were here: "Home > Company > Staff > Bob Smith", you may want to highlight 'Company' to say you are in that section. If you add $LinkingMode to your navigation elements as a class, ie:
Highlighting the current page is easy, simply assign a css class based on the value of *$LinkingMode*. Then provide a different style for current/section in css, as has been provided for you in *simple/css/layout.css*.
:::ss
<li class="$LinkingMode">
<a href="$Link" title="$Title.XML">$MenuTitle.XML</a>
</li>
![](_images/tutorial1_menu-highlighted.jpg)
you will then be able to target a section in css (*simple/css/layout.css*), e.g.:
:::css
.section { background:#ccc; }
## A second level of navigation
@ -201,16 +220,19 @@ Either way, your site tree should now look something like this:
![](_images/tutorial1_2nd_level-cut.jpg)
Great, we now have a hierarchical site structure, let's now look at how this is created and displayed in our template.
Great, we now have a hierarchical site structure! Let's look at how this is created and displayed in our template.
Adding a second level menu is very similar to adding the first level menu.
Open up */themes/simple/templates/Includes/Sidebar.ss* template and look at the following code:
Adding a second level menu is very similar to adding the first level menu. Open up */themes/simple/templates/Includes/Sidebar.ss* template and look at the following code:
:::ss
<ul>
<% loop Menu(2) %>
<li class="$LinkingMode"><a href="$Link" title="Go to the $Title.XML page"><span class="arrow">&rarr;</span><span class="text">$MenuTitle.XML</span></a></li>
<li class="$LinkingMode">
<a href="$Link" title="Go to the $Title.XML page">
<span class="arrow">&rarr;</span>
<span class="text">$MenuTitle.XML</span>
</a>
</li>
<% end_loop %>
</ul>
@ -228,7 +250,12 @@ like this:
...
<ul>
<% loop Menu(2) %>
<li class="$LinkingMode"><a href="$Link" title="Go to the $Title.XML page"><span class="arrow">&rarr;</span><span class="text">$MenuTitle.XML</span></a></li>
<li class="$LinkingMode">
<a href="$Link" title="Go to the $Title.XML page">
<span class="arrow">&rarr;</span>
<span class="text">$MenuTitle.XML</span>
</a>
</li>
<% end_loop %>
</ul>
...
@ -244,37 +271,45 @@ Open up */themes/simple/templates/Includes/BreadCrumbs.ss* template and look at
:::ss
<% if Level(2) %>
<div id="Breadcrumbs">
$Breadcrumbs
</div>
<% end_if %>
<div id="Breadcrumbs">
$Breadcrumbs
</div>
<% end_if %>
Breadcrumbs are only useful on pages that aren't in the top level. We can ensure that we only show them if we aren't in
the top level with another if statement.
The *Level* page control allows you to get data from the page's parents, e.g. if you used *Level(1)*, you could use
*$Level(1).Title* to get the top level page title. In this case, we merely use it to check the existence of a second
level page; if one exists then we include the breadcrumbs.
The *Level* page control allows you to get data from the page's parents, e.g. if you used *Level(1)*, you could use:
This shows how the two level navigation system functions. Both menus should be updating and highlighting as you move
from page to page. They will also mirror changes done in the SilverStripe CMS, such as renaming pages or moving them
around.
:::ss
$Level(1).Title
to get the top level page title. In this case, we merely use it to check the existence of a second level page: if one exists then we include breadcrumbs.
Both the top menu, and the sidebar menu should be updating and highlighting as you move from page to page. They will also mirror changes done in the SilverStripe CMS, such as renaming pages or moving them around.
![](_images/tutorial1_menu-two-level.jpg)
Feel free to experiment with the if and loop blocks, for example you could create a drop down style menu from the top navigation using a combination of the if blocks, loop blocks and some CSS to style it. This uses a *Children* if and loop block which checks to see if there is any sub-pages available within each top level navigation item, you will need to come up with your own CSS to correctly style this approach.
Feel free to experiment with the if and loop statements, for example you could create a drop down style menu from the top navigation using a combination of if statements, loops and some CSS to style it.
::ss
The following example runs an if statement, and a loop on *Children*, checking to see if any sub-pages exist within each top level navigation item, you will need to come up with your own CSS to correctly style this approach.
:::ss
<ul>
<% loop Menu(1) %>
<li class="$LinkingMode">
<a href="$Link" title="$Title.XML">$MenuTitle.XML</a>
<% if Children %>
<ul>
<% loop Children %>
<li class="$LinkingMode"><a href="$Link" title="Go to the $Title.XML page"><span class="arrow">&rarr;</span><span class="text">$MenuTitle.XML</span></a></li>
<% end_loop %>
<ul>
<ul>
<% loop Children %>
<li class="$LinkingMode">
<a href="$Link" title="Go to the $Title.XML page">
<span class="arrow">&rarr;</span>
<span class="text">$MenuTitle.XML</span>
</a>
</li>
<% end_loop %>
<ul>
<% end_if %>
</li>
<% end_loop %>
@ -282,7 +317,6 @@ Feel free to experiment with the if and loop blocks, for example you could creat
## Using a different template for the home page
So far, a single template layout *Layouts/Page.ss* is being used for the entire site. This is useful for the purpose of this
@ -294,34 +328,23 @@ banner to welcome visitors.
### Creating a new page type
Earlier we stated that every page in a SilverStripe site has a **page type**, and that SilverStripe will look for a
template or template layout corresponding to the page type. Therefore, the first step to get the homepage using a different template is to
create a new page type.
template, or template layout, corresponding to the page type. Therefore, the first step when switching the homepage template is to create a new page type.
Each page type is represented by two php classes: a *data object* and a *controller*. Don't worry about the details of page
types right now, we will go into much more detail in tutorial two.
Each page type is represented by two PHP classes: a *data object* and a *controller*. Don't worry about the details of page
types right now, we will go into much more detail in the [next tutorial](2-extending-a-basic-site).
Create a new file *HomePage.php* in *mysite/code*. Copy the following code into it:
:::php
<?php
/**
* Defines the HomePage page type
*/
class HomePage extends Page {
static $db = array(
);
static $has_one = array(
);
}
class HomePage_Controller extends Page_Controller {
}
Every page type also has a database table corresponding to it. Every time we modify the database, we need to rebuild it.
We can do this by going to [http://localhost/dev/build?flush=1](http://localhost/dev/build?flush=1) or replace *localhost* with your own domain name.
We can do this by going to [http://localhost/your_site_name/dev/build?flush=all](http://localhost/your_site_name/dev/build?flush=1) (replace *localhost/your_site_name* with your own domain name if applicable).
It may take a moment, so be patient. This add tables and fields needed by your site, and modifies any structures that have changed. It
does this non-destructively - it will never delete your data.
@ -332,24 +355,30 @@ As we have just created a new page type, SilverStripe will add this to the list
After building the database, we can change the page type of the homepage in the CMS.
Navigate in the CMS to the "Home" page and under the "Behaviour" tab in the "Edit Page > Settings" section. Change it to *Home Page*, and click "Save & Publish".
In the CMS, navigate to the "Home" page and switch to the "Settings" tab. Change "Page type" to *Home Page*, and click "Save & Publish".
![](_images/tutorial1_homepage-type.jpg)
Our homepage is now of the page type *HomePage*. However, even though it is of the *HomePage* page type, it is still
rendered with the *Page* template. SilverStripe still renders the homepage using the *Page* template because when we
created the *HomePage* page type, we inherited from *Page*. So when SilverStripe cannot find a *HomePage* template, it
will use the *Page* template. SilverStripe always attempts to use the most specific template first, and then falls back
to the template of the page type's parents.
Our homepage is now of the page type *HomePage*. Regardless, it is still
rendered with the *Page* template. SilverStripe does this the type inherits from *Page*,
which acts as a fallback if no *HomePage* template can be found.
It always tries to use the most specific template in an inheritance chain.
### Creating a new template
To create a new template layout, create a copy of *Page.ss* (found in *themes/simple/templates/Layouts*) and call it *HomePage.ss*. If we flush the cache (*?flush=1*), SilverStripe should now be using *HomePage.ss* for the homepage, and *Page.ss* for the rest of the site. Now let's customize the *HomePage* template.
To create a new template layout, create a copy of *Page.ss* (found in *themes/simple/templates/Layout*) and call it *HomePage.ss*. If we flush the cache (*?flush=all*), SilverStripe should now be using *HomePage.ss* for the homepage, and *Page.ss* for the rest of the site. Now let's customize the *HomePage* template.
First, remove the breadcrumbs and the secondary menu by removing the `<% include SideBar %>` line of code; we don't need them for the homepage. Let's replace the title with an image. Add this line above the *$Content* variable.
First, we don't need the breadcrumbs and the secondary menu for the homepage. Let's remove them:
:::ss
<% include SideBar %>
Now add the following to replace the `<h1>$Title</h1>` code in your template:
We'll also replace the title text with an image. Find this line:
:::ss
<h1>$Title</h1>
and replace it with:
:::ss
<div id="Banner">
@ -365,23 +394,18 @@ Your Home page should now look like this:
SilverStripe first searches for a template in the *themes/simple/templates* folder. Since there is no *HomePage.ss*,
it will use the *Page.ss* for both *Page* and *HomePage* page types. When it comes across the *$Layout* tag, it will
then descend into the *themes/simple/templates/Layout* folder, and will use *Page.ss* for the *Page* page type, and
*HomePage.ss* for the *HomePage* page type. So while you could create a HomePage.ss in the *themes/simple/templates/* it is better to reuse the navigation and footer common to both our Home page and the rest of the pages on our website and lets you write less code to achieve the end result.
*HomePage.ss* for the *HomePage* page type. So while you could create a HomePage.ss in the *themes/simple/templates/* it is better to reuse the navigation and footer common to both our Home page and the rest of the pages on our website.
![](_images/tutorial1_subtemplates-diagram.jpg)
## Summary
We have introduced template variables, controls and if blocks, and we have used these
to build a basic but fully functional site. You have also been briefly introduced to page types, and seen how they
correspond to templates and sub-templates. By using these templates, you have seen how to customize the site content
according to the page type of the page you are displaying.
So far we have taken a look at the different areas and functionality within the pages area of the CMS. We have learnt about template variables, controls and if statements and used these to build a basic, but fully functional, website. We have also briefly covered page types, and looked at how they correspond to templates and sub-templates. Using this knowledge, we have customized our website's homepage design.
In the next tutorial, [Extending a Basic Site](2-extending-a-basic-site), we will explore page types on a
deeper level, and see how you can customize your own page types to extend SilverStripe to do much more interesting
things.
In the next tutorial, [Extending a Basic Site](2-extending-a-basic-site), we will explore page types on a deeper level, and look at customising our own page types to extend the functionality of SilverStripe.
[Next Tutorial >>](2-extending-a-basic-site)
[Next tutorial >>](2-extending-a-basic-site)
## Books on SilverStripe

View File

@ -3,58 +3,49 @@
## Overview
In the [first tutorial](1-building-a-basic-site) we learned how to create a basic site using SilverStripe. This
tutorial builds on what you have learned in [the first tutorial](1-building-a-basic-site), so it is recommended
that you complete it first.
In this tutorial you will explore extending SilverStripe by creating your own page types. In doing this you will get a
good overview of how SilverStripe works.
In the [first tutorial](1-building-a-basic-site) we learnt how to create a basic site using SilverStripe. This tutorial will build on that, and explore extending SilverStripe by creating our own page types. After doing this we should have a better understanding of how SilverStripe works.
## What are we working towards?
Throughout this tutorial we are going to work on adding two new sections to the site we built in the first tutorial. The
first is a news section, with a recent news listing on the homepage and an RSS feed. The second is a staff section,
which demonstrates more complex database structures by associating an image with each staff member.
We are going to work on adding two new sections to the site we built in the first tutorial.
The first of these new sections will be *News*, with a recent news listing on the homepage and an RSS feed:
![](_images/tutorial2_newslist.jpg)
The second will be a *Staff* section, to demonstrate more complex database structures (such as associating an image with each staff member):
![](_images/tutorial2_einstein.jpg)
## The SilverStripe data model
A large part of designing complex SilverStripe sites is the creation of your own page types. Before we progress any
further, it is important to understand what a page type is, and how the SilverStripe data model works.
A large part of designing complex SilverStripe sites is the creation of our own page types. Before we progress any further, it is important to understand what a page type is and how the SilverStripe data model works.
SilverStripe is based on the **"Model-View-Controller"** design pattern. This means that SilverStripe attempts to separate
data, logic and presentation as much as possible. Every page has three separate parts which are combined to give you the
SilverStripe is based on the **"Model-View-Controller"** design pattern. This means that SilverStripe attempts to separate data, logic and presentation as much as possible. Every page has three separate parts which are combined to give you the
final page. Lets look at each one individually:
### Model
All content on your site is stored in a database. There is a table in the database corresponding for every class that is
a child of the `[api:DataObject]` class. Every object of that class corresponds to a row in that table -
this is your "data object", the **"model"** of Model-View-Controller. A page type has a data object that represents all the data for your page - rather than inheriting
directly from data object it inherits from `[api:SiteTree]`. We generally create a "Page" data object, and subclass this for
the rest of the page types. This allows us to define behavior that is consistent across all pages in our site.
All content on our site is stored in a database. Each class that is a child of the `[api:DataObject]` class will have its own table in our database.
Every object of such a class will correspond to a row in that table -
this is our "data object", the **"model"** of Model-View-Controller. A page type has a data object that represents all the data for our page. Rather than inheriting
directly from `[api:DataObject]`, it inherits from `[api:SiteTree]`. We generally create a "Page" data object, and subclass this for all other page types. This allows us to define behavior that is consistent across all pages in our site.
### View
The **"view"** is the presentation of your site. As we have already seen, the templates SilverStripe uses to render a page
is dependent on the page type. Using both your templates and css, you are able to have full control over the
presentation of your site.
The **"view"** is the presentation of our site. As we have already seen, the templates SilverStripe uses to render a page are dependent on the page type. Using templates and css, we are able to have full control over the
presentation of our website.
### Controller
A page type also has a **"controller"**. A controller contains all the code used to manipulate your data before it is
rendered. For example, suppose you were making an auction site, and you only wanted to display the auctions closing in
the next ten minutes. You would implement this in the controller. The controller for a page should inherit from
`[api:ContentController]`. Just as we create a "Page" data object and subclass it for the rest of the
site, we also create a "Page_Controller" that is subclassed.
Each page type also has a **"controller"**. The controller contains all the code used to manipulate our data before it is rendered. For example, suppose we were making an auction site, and we only wanted to display the auctions closing in the next ten minutes. We would implement this logic in the controller. The controller for a page should inherit from `[api:ContentController]`. Just as we create a "Page" data object and subclass it for the rest of the site, we also create a "Page_Controller" that is subclassed.
Creating a new page type simply requires creating these three things. You can then have full control over presentation,
the database, which fields can be edited in the CMS, and can use code to make our pages do much more clever things.
Creating a new page type requires creating each of these three elements. We will then have full control over presentation, the database, and editable CMS fields.
A more in-depth introduction of Model-View-Controller can be found
[here](http://www.slash7.com/articles/2005/02/22/mvc-the-most-vexing-conundrum).
@ -63,37 +54,22 @@ A more in-depth introduction of Model-View-Controller can be found
## Creating the news section page types
Let's make our news section. We'll need two new page types for this. The first one is obvious: we need an *ArticlePage*
page type. The second is a little less obvious: we need an *ArticleHolder* page type that contains our articles.
To create a news section we'll need two new page types. The first one is obvious: we need an *ArticlePage* page type. The second is a little less obvious: we need an *ArticleHolder* page type to contain our article pages.
We'll start with the *ArticlePage* page type. First we create the model, a class called "ArticlePage". We put the
*ArticlePage* class into a file called "ArticlePage.php" inside *mysite/code*. We also put the controller,
*ArticlePage_Controller*, in here. Any other classes that are related to *ArticlePage* for example, the class
*ArticlePage_AnythingElse* - will also go into "ArticlePage.php".
We'll start with the *ArticlePage* page type. First we create the model, a class called "ArticlePage". We put the *ArticlePage* class into a file called "ArticlePage.php" inside *mysite/code*. All other classes relating to *ArticlePage* should be placed within "ArticlePage.php", this includes our controller (*ArticlePage_Controller*).
**mysite/code/ArticlePage.php**
:::php
<?php
/**
* Defines the ArticlePage page type
*/
class ArticlePage extends Page {
static $db = array(
);
static $has_one = array(
);
}
class ArticlePage_Controller extends Page_Controller {
}
Here we've created our data object/controller pair, but we haven't actually extended them at all. Don't worry about the
*$db* and *$has_one* arrays just yet, we'll explain them soon, as well as other ways in which you can extend your page
types. SilverStripe will use the template for the *Page* page type as explained in the first tutorial, so we don't need
Here we've created our data object/controller pair, but we haven't extended them at all. Don't worry about the *$db* and *$has_one* arrays just yet, we'll explain them shortly. SilverStripe will use the template for the *Page* page type as explained in the first tutorial, so we don't need
to specifically create the view for this page type.
Let's create the *ArticleHolder* page type.
@ -102,44 +78,31 @@ Let's create the *ArticleHolder* page type.
:::php
<?php
/**
* Defines the ArticleHolder page type
*/
class ArticleHolder extends Page {
static $db = array(
);
static $has_one = array(
);
static $allowed_children = array('ArticlePage');
}
class ArticleHolder_Controller extends Page_Controller {
}
Here we have done something interesting: the *$allowed_children* field. This is one of a number of static fields we can
define to change the properties of a page type. The *$allowed_children* field is an array of page types that are allowed
to be children of the page in the site tree. As we only want news articles in the news section, we only want
*ArticlePage* pages for children. We can enforce this in the CMS by setting the *$allowed_children* field.
Here we have done something interesting: the *$allowed_children* field. This is one of a number of static fields we can define to change the properties of a page type. The *$allowed_children* field is an array of page types that are allowed
to be children of the page in the site tree. As we only want **news articles** in the news section, we only want pages of the type *ArticlePage* as children. We can enforce this in the CMS by setting the *$allowed_children* field within this class.
We will be introducing other fields like this as we progress; there is a full list in the documentation for
`[api:SiteTree]`.
We will be introduced to other fields like this as we progress; there is a full list in the documentation for `[api:SiteTree]`.
Now that we have created our page types, we need to let SilverStripe rebuild the database. If we rebuild the database by
going to [http://localhost/dev/build?flush=1](http://localhost/dev/build?flush=1), SilverStripe will detect that there are two
new page types and add them to the list of page types in the database.
Now that we have created our page types, we need to let SilverStripe rebuild the database: [http://localhost/your_site_name/dev/build?flush=all](http://localhost/your_site_name/dev/build?flush=all). SilverStripe should detect that there are two new page types, and add them to the list of page types in the database.
> It is SilverStripe convention to suffix general page types with "Page", and page types that hold other page types with
> "Holder". This is to ensure that we don't have URLs with the same name as a page type; if we named our *ArticleHolder*
> page type "News", it would conflict with the page name also called "News".
<div class="hint" markdown="1">
It is SilverStripe convention to suffix general page types with "Page", and page types that hold other page types with
"Holder". This is to ensure that we don't have URLs with the same name as a page type; if we named our *ArticleHolder*
page type "News", it would conflict with the page name also called "News".
</div>
## Adding date and author fields
Now that we have an *ArticlePage* page type, let's make it a little more useful. Remember the *$db* array? We can use
this array to add extra fields to the database. It would be nice to know when each article was posted, and who posted
it. Change the *$db* array in the *ArticlePage* class so it looks like this:
it. Add a *$db* property definition in the *ArticlePage* class:
:::php
<?php
@ -153,17 +116,16 @@ it. Change the *$db* array in the *ArticlePage* class so it looks like this:
}
Every entry in the array is a key-value pair. The key is the name of the field, and the value is the type. We have a
`[api:Date]` for a complete list of different data types.
Every entry in the array is a *key => value* pair. The **key** is the name of the field, and the **value** is the type. See ["data types"](/topics/data-types) for a complete list of types.
> Note: The names chosen for the fields you add must not already be used. Be careful using field names such as Title,
> Content etc. as these may already be defined in the page types your new page is extending from.
<div class="hint" markdown="1">
The names chosen for the fields you add must not already be used. Be careful using field names such as Title,
Content etc. as these may already be defined in the page types your new page is extending from.
</div>
If we rebuild the database, we will see that now the *ArticlePage* table is created. Even though we had an *ArticlePage*
page type before, the table was not created because we had no fields that were unique to the article page type. We now
have the extra fields in the database, but still no way of changing them. To add these fields to the CMS we have to
override the *getCMSFields()* method, which is called by the CMS when it creates the form to edit a page. Add the
method to the *ArticlePage* class.
When we rebuild the database, we will see that the *ArticlePage* table has been created. Even though we had an *ArticlePage* page type before, a table was not created because there were no fields unique to the article page type. There are now extra fields in the database, but still no way of changing them.
To add our new fields to the CMS we have to override the *getCMSFields()* method, which is called by the CMS when it creates the form to edit a page. Add the method to the *ArticlePage* class.
:::php
<?php
@ -201,19 +163,23 @@ returned is a `[api:FieldList]` object.
We can then add our new fields with *addFieldToTab*. The first argument is the tab on which we want to add the field to:
"Root.Main" is the tab which the content editor is on (another is "Root.Metadata". The second argument is the field to add; this is not a database field, but a `[api:FormField]` - see the documentation for more details.
"Root.Main" is the tab which the content editor is on. The second argument is the field to add; this is not a database field, but a `[api:FormField]` - see the documentation for more details.
We add two fields: A simple `[api:TextField}` and a `[api:DateField]`. There are many more FormFields available in the default installation, please refer to [Form Field Types](form-field-types) for the list.
<div class="hint" markdown="1">
Note: By default, the CMS only has one tab. Creating new tabs is much like adding to existing tabs. For instance: `$fields->addFieldToTab('Root.NewTab', new TextField('Author'));`
would create a new tab called "New Tab", and a single "Author" textfield inside.
</div>
We have added two fields: A simple `[api:TextField]` and a `[api:DateField]`.
There are many more fields available in the default installation, listed in ["form field types"](/reference/form-field-types).
:::php
return $fields;
Finally, we return the fields to the CMS. If we flush the cache (by adding ?flush=1 at the end of the URL), we will be able
to edit the fields in the CMS.
Finally, we return the fields to the CMS. If we flush the cache (by adding ?flush=all at the end of the URL), we will be able to edit the fields in the CMS.
Now that we have created our page types, let's add some content. Go into the CMS and create an *ArticleHolder* page
named "News", and create some *ArticlePage*s inside it.
Now that we have created our page types, let's add some content. Go into the CMS and create an *ArticleHolder* page named "News", then create a few *ArticlePage*'s within it.
![](_images/tutorial2_news-cms.jpg)
@ -254,29 +220,27 @@ Let's walk through these changes.
:::php
$dateField->setConfig('showcalendar', true);
Set *showCalendar* to true to have a calendar appear underneath the Date field when you click on the field.
By enabling *showCalendar* you show a calendar overlay when clicking on the field.
:::php
$dateField->setConfig('dateformat', 'dd/MM/YYYY');
*dateFormat* allows you to specify how you wish the date to be entered and displayed in the CMS field.
*dateFormat* allows you to specify how you wish the date to be entered and displayed in the CMS field. See the `[api:DateField]` documentation for more configuration options.
:::php
$fields->addFieldToTab('Root.Content', new TextField('Author','Author Name'), 'Content');
By default the field name *'Date'* or *'Author'* is shown as the title, however this might not be that helpful so to change the title,
add the new title as the second argument. See the `[api:DateField]` documentation for more details of the DateField configuration.
By default the field name *'Date'* or *'Author'* is shown as the title, however this might not be that helpful so to change the title, add the new title as the second argument.
## Creating the templates
We can already look at the content of news pages on our site, because the article holder page and the article pages
inherit their templates from Page. But we're not getting the author and date fields displayed in either case.
Because our new pages inherit their templates from *Page*, we can view anything entered in the content area when navigating to these pages on our site. However, as there is no reference to the date or author fields in the *Page* template this data is not being displayed.
So let's create a template for each of our new page types. We'll put these in *themes/tutorial/templates/Layout* so we
only have to define the page specific parts: SilverStripe will use *themes/tutorial/templates/Page.ss* for the basic
To fix this we will create a template for each of our new page types. We'll put these in *themes/tutorial/templates/Layout* so we only have to define the page specific parts: SilverStripe will use *themes/tutorial/templates/Page.ss* for the basic
page layout.
### ArticlePage Template
First, the template for displaying a single article:
**themes/simple/templates/Layout/ArticlePage.ss**
@ -292,20 +256,15 @@ First, the template for displaying a single article:
<div class="content">$Content</div>
</article>
$Form
$PageComments
</div>
<% include SideBar %>
Most of the code is just like the regular Page.ss, we include an informational div with the date and the author of the Article.
We use *$Date* and *$Author* to access the new fields. In fact, all template variables and page controls come from
either the data object or the controller for the page being displayed. The *$Title* variable comes from the
*Title* field of the `[api:SiteTree]` class. *$Date* and *$Author* come from the *ArticlePage* table through
your custom Page. *$Content* comes from the *SiteTree* table through the same data object. The data for your page is
To access the new fields, we use *$Date* and *$Author*. In fact, all template variables and page controls come from either the data object or the controller for the page being displayed. The *$Title* variable comes from the *Title* field of the `[api:SiteTree]` class. *$Date* and *$Author* come from the *ArticlePage* table through your custom Page. *$Content* comes from the *SiteTree* table through the same data object. The data for your page is
spread across several tables in the database matched by id - e.g. *Content* is in the *SiteTree* table, and *Date* and
*Author* are in the *ArticlePage* table. SilverStripe matches these records by their ids and collates them into the single
data object.
*Author* are in the *ArticlePage* table. SilverStripe matches this data, and collates it into a single data object.
![](_images/tutorial2_data-collation.jpg)
@ -315,8 +274,8 @@ database.
![](_images/tutorial2_news.jpg)
Now we'll create a template for the article holder: we want our news section to show a list of news items, each with a
summary.
###ArticleHolder Template
We'll now create a template for the article holder. We want our news section to show a list of news items, each with a summary and a link to the main article (our Article Page).
**themes/simple/templates/Layout/ArticleHolder.ss**
@ -339,22 +298,18 @@ summary.
<% include SideBar %>
Here we use the page control *Children*. As the name suggests, this control allows you to iterate over the children of a
page, which in this case is our news articles. The *$Link* variable will give the address of the article which we can
use to create a link, and the *FirstParagraph* function of the `[api:HTMLText]` field gives us a nice summary of the
article. The function strips all tags from the paragraph extracted.
Here we use the page control *Children*. As the name suggests, this control allows you to iterate over the children of a page. In this case, the children are our news articles. The *$Link* variable will give the address of the article which we can use to create a link, and the *FirstParagraph* function of the `[api:HTMLText]` field gives us a nice summary of the article. The function strips all tags from the paragraph extracted.
![](_images/tutorial2_articleholder.jpg)
### Using include files in templates
You can make your templates more modular and easier to maintain by separating commonly-used pieces into include files.
You are already familiar with the `<% include Sidebar %>`-Line for the menu.
We can make our templates more modular and easier to maintain by separating commonly-used components in to *include files*. We are already familiar with the `<% include Sidebar %>` line from looking at the menu in the [first tutorial](1-building-a-basic-site).
We'll separate the display of linked articles as we want to reuse this code later on.
Replace the code in *ArticleHolder.ss** with an include statement:
Cut the code in *ArticleHolder.ss** and replace it with an include statement:
**themes/simple/templates/Layout/ArticleHolder.ss**
@ -365,7 +320,7 @@ Replace the code in *ArticleHolder.ss** with an include statement:
<% end_loop %>
...
and paste the code in a new include snippet:
Paste the code that was in ArticleHolder into a new include file called ArticleTeaser.ss:
**themes/simple/templates/Includes/ArticleTeaser.ss**
@ -397,24 +352,20 @@ This will change the icons for the pages in the CMS.
## Showing the latest news on the homepage
It would be nice to greet page visitors with a summary of the latest news when they visit the homepage. This requires a
little more code though - the news articles are not direct children of the homepage, so we can't use the *Children*
control. We can get the data for the news articles by implementing our own function in *HomePage_Controller*.
It would be nice to greet page visitors with a summary of the latest news when they visit the homepage. This requires a little more code though - the news articles are not direct children of the homepage, so we can't use the *Children* control. We can get the data for news articles by implementing our own function in *HomePage_Controller*.
**mysite/code/HomePage.php**
:::php
...
public function LatestNews($num=5) {
$holder = DataObject::get_one("ArticleHolder");
return ($holder) ? DataList::create('ArticlePage')->where('"ParentID" = '.$holder->ID)->sort('Date DESC')->limit($num) : false;
$holder = ArticleHolder::get()->First();
return ($holder) ? ArticlePage::get()->filter('ParentID', $holder->ID)->sort('Date DESC')->limit($num) : false;
}
...
This function simply runs a database query that gets the latest news articles from the database. By default, this is
five, but you can change it by passing a number to the function. See the [Data Model](../topics/datamodel) documentation for
details. We can reference this function as a page control in our *HomePage* template:
This function simply runs a database query that gets the latest news articles from the database. By default, this is five, but you can change it by passing a number to the function. See the [Data Model](../topics/datamodel) documentation for details. We can reference this function as a page control in our *HomePage* template:
**themes/tutorial/templates/Layout/Homepage.ss**
@ -428,14 +379,9 @@ details. We can reference this function as a page control in our *HomePage* temp
...
When SilverStripe comes across a variable or page control it doesn't recognize, it first passes control to the
controller. If the controller doesn't have a function for the variable or page control, it then passes control to the
data object. If it has no matching functions, it then searches its database fields. Failing that it will return nothing.
When SilverStripe comes across a variable or page control it doesn't recognize, it first passes control to the controller. If the controller doesn't have a function for the variable or page control, it then passes control to the data object. If it has no matching functions, it then searches its database fields. Failing that it will return nothing.
The controller for a page is only created when page is actually visited, while the data object is available when the
page is referenced in other pages, e.g. by page controls. A good rule of thumb is to put all functions specific to the
page currently being viewed in the controller; only if a function needs to be used in another page should you put it in
the data object.
The controller for a page is only created when page is actually visited, while the data object is available when the page is referenced in other pages, e.g. by page controls. A good rule of thumb is to put all functions specific to the page currently being viewed in the controller; only if a function needs to be used in another page should you put it in the data object.
![](_images/tutorial2_homepage-news.jpg)
@ -443,8 +389,7 @@ the data object.
## Creating a RSS feed
An RSS feed is something that no news section should be without. SilverStripe makes it easy to create RSS feeds by
providing an `[api:RSSFeed]` class to do all the hard work for you. Create the following function in the
An RSS feed is something that no news section should be without. SilverStripe makes it easy to create RSS feeds by providing an `[api:RSSFeed]` class to do all the hard work for us. Create the following function in the
*ArticleHolder_Controller*:
:::php
@ -454,18 +399,13 @@ providing an `[api:RSSFeed]` class to do all the hard work for you. Create the f
}
This function simply creates an RSS feed of all the news articles, and outputs it to the browser. If you go to
[http://localhost/news/rss](http://localhost/news/rss) you will see our RSS feed. What happens here is that
when there is more to a URL after the page's base URL - "rss" in this case - SilverStripe will call the function with
that name on the controller if it exists.
This function creates an RSS feed of all the news articles, and outputs it to the browser. If we go to [http://localhost/your_site_name/news/rss](http://localhost/your_site_name/news/rss) we should see our RSS feed. When there is more to a URL after a page's base URL, "rss" in this case, SilverStripe will call the function with that name on the controller if it exists.
Depending on your browser, you should see something like the picture below. If your browser doesn't support RSS, you
will most likely see the XML output instead.
Depending on your browser, you should see something like the picture below. If your browser doesn't support RSS, you will most likely see the XML output instead. For more on RSS, see `[api:RSSFeed]`
![](_images/tutorial2_rss-feed.jpg)
Now all we need is to let the user know that our RSS feed exists. The `[api:RSSFeed]` in your controller, it will be
called when the page is requested. Add this function to *ArticleHolder_Controller*:
Now all we need is to let the user know that our RSS feed exists. Add this function to *ArticleHolder_Controller*:
:::php
public function init() {
@ -474,17 +414,11 @@ called when the page is requested. Add this function to *ArticleHolder_Controlle
}
This automatically generates a link-tag in the header of our template. The *init* function is then called on the parent
class to ensure any initialization the parent would have done if we hadn't overridden the *init* function is still
called. Depending on your browser, you can see the RSS feed link in the address bar:
![](_images/tutorial2_rss.jpg)
This automatically generates a link-tag in the header of our template. The *init* function is then called on the parent class to ensure any initialization the parent would have done if we hadn't overridden the *init* function is still called. Depending on your browser, you can see the RSS feed link in the address bar.
## Adding a staff section
Now that we have a complete news section, let's move on to the staff section. We need to create *StaffHolder* and
*StaffPage* page types, for an overview on all staff members and a detail-view for a single member. First let's start
with the *StaffHolder* page type.
Now that we have a complete news section, let's take a look at the staff section. We need to create *StaffHolder* and *StaffPage* page types, for an overview on all staff members and a detail-view for a single member. First let's start with the *StaffHolder* page type.
**mysite/code/StaffHolder.php**
@ -505,9 +439,7 @@ with the *StaffHolder* page type.
}
Nothing here should be new. The *StaffPage* page type is more interesting though. Each staff member has a portrait
image. We want to make a permanent connection between this image and the specific *StaffPage* (otherwise we could simply
insert an image in the *$Content* field).
Nothing here should be new. The *StaffPage* page type is more interesting though. Each staff member has a portrait image. We want to make a permanent connection between this image and the specific *StaffPage* (otherwise we could simply insert an image in the *$Content* field).
**mysite/code/StaffPage.php**
@ -535,10 +467,7 @@ insert an image in the *$Content* field).
}
Instead of adding our *Image* as a field in *$db*, we have used the *$has_one* array. This is because an *Image* is not
a simple database field like all the fields we have seen so far, but has its own database table. By using the *$has_one*
array, we create a relationship between the *StaffPage* table and the *Image* table by storing the id of the respective
*Image* in the *StaffPage* table.
Instead of adding our *Image* as a field in *$db*, we have used the *$has_one* array. This is because an *Image* is not a simple database field like all the fields we have seen so far, but has its own database table. By using the *$has_one* array, we create a relationship between the *StaffPage* table and the *Image* table by storing the id of the respective *Image* in the *StaffPage* table.
We then add an `[api:UploadField]` in the *getCMSFields* function to the tab "Root.Images". Since this tab doesn't exist,
the *addFieldToTab* function will create it for us. The *UploadField* allows us to select an image or upload a new one in
@ -546,7 +475,7 @@ the CMS.
![](_images/tutorial2_photo.jpg)
Rebuild the database ([http://localhost/dev/build?flush=1](http://localhost/dev/build?flush=1)) and open the CMS. Create
Rebuild the database ([http://localhost/your_site_name/dev/build?flush=1](http://localhost/your_site_name/dev/build?flush=1)) and open the CMS. Create
a new *StaffHolder* called "Staff", and create some *StaffPage*s in it.
![](_images/tutorial2_create-staff.jpg)
@ -597,18 +526,16 @@ The *StaffPage* template is also very straight forward.
$Content</div>
</article>
$Form
$PageComments
</div>
<% include SideBar %>
Here we also use the *SetWidth* function to get a different sized image from the same source image. You should now have
Here we use the *SetWidth* method to get a different sized image from the same source image. You should now have
a complete staff section.
![](_images/tutorial2_einstein.jpg)
## Summary
In this tutorial we have explored the concept of page types. In the process of creating and extending page types you
have been introduced to many of the concepts required to build a site with SilverStripe.
In this tutorial we have explored the concept of page types. In the process of creating and extending page types we have covered many of the concepts required to build a site with SilverStripe.
[Next Tutorial >>](3-forms)

View File

@ -207,7 +207,7 @@ that the *BrowserPollSubmission* table is created. Now we just need to define 'd
$submission = new BrowserPollSubmission();
$form->saveInto($submission);
$submission->write();
Director::redirectBack();
return $this->redirectBack();
}
}
@ -218,7 +218,7 @@ A function that processes a form submission takes two arguments - the first is t
In our function we create a new *BrowserPollSubmission* object. Since the name of our form fields and the name of the
database fields are the same we can save the form directly into the data object.
We call the 'write' method to write our data to the database, and 'Director::redirectBack()' will redirect the user back
We call the 'write' method to write our data to the database, and 'redirectBack()' will redirect the user back
to the home page.
@ -237,11 +237,8 @@ Change the end of the 'BrowserPollForm' function so it looks like this:
:::php
public function BrowserPollForm() {
...
// Create validator
// ...
$validator = new RequiredFields('Name', 'Browser');
return new Form($this, 'BrowserPollForm', $fields, $actions, $validator);
}
@ -266,22 +263,16 @@ First modify the 'doBrowserPoll' to set the session variable 'BrowserPollVoted'
*mysite/code/HomePage.php*
:::php
...
HomePage_Controller extends Page_Controller {
...
// ...
class HomePage_Controller extends Page_Controller {
// ...
public function doBrowserPoll($data, $form) {
$submission = new BrowserPollSubmission();
$form->saveInto($submission);
$submission->write();
Session::set('BrowserPollVoted', true);
Director::redirectBack();
return $this->redirectBack();
}
...
}
@ -293,59 +284,55 @@ it is.
if(Session::get('BrowserPollVoted')) {
return false;
}
...
// ...
}
If you visit the home page now you will see you can only vote once per session; after that the form won't be shown. You
can start a new session by closing and reopening your browser (or if you're using Firefox and have installed the [Web
Developer](http://chrispederick.com/work/web-developer/) extension, you can use its Clear Session Cookies command).
If you visit the home page now you will see you can only vote once per session;
after that the form won't be shown.
You can start a new session by closing and reopening your browser.
Although the form is not shown, you'll still see the 'Browser Poll' heading. We'll leave this for now: after we've built
the bar graph of the results, we'll modify the template to show the graph instead of the form if the user has already
voted.
Now that we're collecting data, it would be nice to show the results
on the website as well. We could simply output every vote, but that's boring.
Let's group the results by browser, through the SilverStripe data model.
We now need some way of getting the data from the database into the template.
In the [second tutorial](/tutorials/2-extending-a-basic-site),
we got a collection of news articles for the home page by
using the 'ArticleHolder::get()' function, which returns a `[api:DataList]`.
We can get all submissions in the same fashion, through `BrowserPollSubmission::get()`.
This list will be the starting point for our result aggregation.
In the second tutorial we got the latest news articles for the home page by using the 'DataObject::get' function. We
can't use the 'DataObject::get' function here directly as we wish to count the total number of votes for each browser.
By looking at the documentation for 'DataObject::get', we can see that it returns a `[api:DataObjectSet]`
object. In fact, all data that can be iterated over in a template with a page control is contained in a DataObjectSet.
A `[api:DataObjectSet]` is a set of not just DataObjects, but of ViewableData, which the majority of
SilverStripe's classes (including DataObject) inherit from. We can create a DataObjectSet, fill it with our data, and
then create our graph using a page control in the template. Create the function 'BrowserPollResults' on the
*HomePage_Controller* class.
Create the function 'BrowserPollResults' on the *HomePage_Controller* class.
** mysite/code/HomePage.php **
:::php
public function BrowserPollResults() {
$submissions = DataObject::get('BrowserPollSubmission');
$submissions = new GroupedList(BrowserPollSubmission::get());
$total = $submissions->Count();
$doSet = new DataObjectSet();
foreach($submissions->groupBy('Browser') as $browser => $data) {
$list = new ArrayList();
foreach($submissions->groupBy('Browser') as $browserName => $browserSubmissions) {
$percentage = (int) ($data->Count() / $total * 100);
$record = array(
'Browser' => $browser,
$list->push(new ArrayData(array(
'Browser' => $browserName,
'Percentage' => $percentage
);
$doSet->push(new ArrayData($record));
)));
}
return $doSet;
return $list;
}
This introduces a few new concepts, so let's step through it.
This code introduces a few new concepts, so let's step through it.
:::php
$submissions = DataObject::get('BrowserPollSubmission');
$submissions = new GroupedList(BrowserPollSubmission::get());
First we get all of the *BrowserPollSubmission*s from the database. This returns the submissions as a
DataObjectSet, which contains the submissions as *BrowserPollSubmission* objects.
First we get all of the `BrowserPollSubmission` records from the database.
This returns the submissions as a `[api:DataList]`.
Then we wrap it inside a `[api:GroupedList]`, which adds the ability
to group those records. The resulting object will behave just like
the original `DataList`, though (with the addition of a `groupBy()` method).
:::php
$total = $submissions->Count();
@ -354,29 +341,24 @@ DataObjectSet, which contains the submissions as *BrowserPollSubmission* objects
We get the total number of submissions, which is needed to calculate the percentages.
:::php
$doSet = new DataObjectSet();
foreach($submissions->groupBy('Browser') as $browser => $data) {
$percentage = (int) ($data->Count() / $total * 100);
$record = array(
'Browser' => $browser,
$list = new ArrayList();
foreach($submissions->groupBy('Browser') as $browserName => $browserSubmissions) {
$percentage = (int) ($browserSubmissions->Count() / $total * 100);
$list->push(new ArrayData(array(
'Browser' => $browserName,
'Percentage' => $percentage
);
$doSet->push(new ArrayData($record));
)));
}
Now we create an empty DataObjectSet to hold our data and then iterate over the 'Browser' submissions field. The 'groupBy'
method of DataObjectSet splits our DataObjectSet by the 'Browser' field passed to it. The percentage of submissions for each
browser is calculated using the size of the DataObjectSet. It puts these new DataObjectSets into an array indexed
by the value of the field. The `[api:ArrayData]` class wraps an array into a ViewableData object, so we finally create a new
ArrayData object, which we can add to our *$doSet* DataObjectSet of results.
:::php
return $doSet;
After we have iterated through all the browsers, the DataObjectSet contains all the results, which is then
returned.
Now we create an empty `[api:ArrayList]` to hold the data we'll pass to the template.
Its similar to `[api:DataList]`, but can hold arbitrary objects rather than just `DataObject` instances.
Then iterate over the 'Browser' submissions field.
The `groupBy()` method splits our list by the 'Browser' field passed to it,
creating new lists with submissions just for a specific browser.
Each of those lists is keyed by the browser name.
The aggregated result is then contained in an `[api:ArrayData]` object,
which behaves much like a standard PHP array, but allows us to use it in SilverStripe templates.
The final step is to create the template to display our data. Change the 'BrowserPoll' div in
*themes/tutorial/templates/Layout/HomePage.ss* to the below.
@ -388,12 +370,12 @@ The final step is to create the template to display our data. Change the 'Browse
$BrowserPollForm
<% else %>
<ul>
<% control BrowserPollResults %>
<% loop BrowserPollResults %>
<li>
<div class="browser">$Browser: $Percentage%</div>
<div class="bar" style="width:$Percentage%">&nbsp;</div>
</li>
<% end_control %>
<% end_loop %>
</ul>
<% end_if %>
</div>
@ -408,6 +390,9 @@ a complete poll.
![](_images/pollresults.jpg)
<div class="hint" markdown="1">
While the ORM is
</div>
## Summary

View File

@ -98,7 +98,7 @@ function, and then attempt to render it with *Page_results.ss*, falling back to
## Creating the template
Lastly we need the template for the search page. This template uses all the same techniques used in previous
tutorials. It also uses a number of pagination variables, which are provided by the `[api:DataObjectSet]`
tutorials. It also uses a number of pagination variables, which are provided by the `[api:PaginatedList]`
class.
*themes/simple/templates/Layout/Page_results.ss*
@ -113,7 +113,7 @@ class.
<% if Results %>
<ul id="SearchResults">
<% control Results %>
<% loop Results %>
<li>
<a class="searchResultHeader" href="$Link">
<% if MenuTitle %>
@ -127,7 +127,7 @@ class.
title="Read more about &quot;{$Title}&quot;"
>Read more about &quot;{$Title}&quot;...</a>
</li>
<% end_control %>
<% end_loop %>
</ul>
<% else %>
<p>Sorry, your search query did not return any results.</p>
@ -142,13 +142,13 @@ class.
<a class="prev" href="$Results.PrevLink" title="View the previous page">Prev</a>
<% end_if %>
<span>
<% control Results.Pages %>
<% loop Results.Pages %>
<% if CurrentBool %>
$PageNum
<% else %>
<a href="$Link" title="View page number $PageNum">$PageNum</a>
<% end_if %>
<% end_control %>
<% end_loop %>
</span>
<p>Page $Results.CurrentPage of $Results.TotalPages</p>
</div>

View File

@ -214,7 +214,7 @@ To use your *HasOneComplexTableField* table for a **1-to-1** relation, make this
$tablefield->setOneToOne();
$fields->addFieldToTab( 'Root.Content.Student', $tablefield );
$fields->addFieldToTab( 'Root.Student', $tablefield );
return $fields;
}
@ -505,44 +505,44 @@ Let's start with the *ProjectsHolder* page created before. For this template, we
</tr>
</thead>
<tbody>
<% control Children %>
<% loop Children %>
<tr>
<td>$Title</td>
<td>
<% if MyStudent %>
<% control MyStudent %>
<% loop MyStudent %>
$FirstName $Lastname
<% end_control %>
<% end_loop %>
<% else %>
No Student
<% end_if %>
</td>
<td>
<% if MyStudent %>
<% control MyStudent %>
<% loop MyStudent %>
<% if MyMentor %>
<% control MyMentor %>
<% loop MyMentor %>
$FirstName $Lastname
<% end_control %>
<% end_loop %>
<% else %>
No Mentor
<% end_if %>
<% end_control %>
<% end_loop %>
<% else %>
No Mentor
<% end_if %>
</td>
<td>
<% if Modules %>
<% control Modules %>
<% loop Modules %>
$Name &nbsp;
<% end_control %>
<% end_loop %>
<% else %>
No Modules
<% end_if %>
</td>
</tr>
<% end_control %>
<% end_loop %>
</tbody>
</table>
@ -579,7 +579,7 @@ We can now do the same for every *Project* page by creating its own template.
$Content
<% if MyStudent %>
<% control MyStudent %>
<% loop MyStudent %>
<p>First Name: <strong>$FirstName</strong></p>
<p>Lastname: <strong>$Lastname</strong></p>
<p>Nationality: <strong>$Nationality</strong></p>
@ -587,15 +587,15 @@ We can now do the same for every *Project* page by creating its own template.
<h3>Mentor</h3>
<% if MyMentor %>
<% control MyMentor %>
<% loop MyMentor %>
<p>First Name: <strong>$FirstName</strong></p>
<p>Lastname: <strong>$Lastname</strong></p>
<p>Nationality: <strong>$Nationality</strong></p>
<% end_control %>
<% end_loop %>
<% else %>
<p>This student doesn't have any mentor.</p>
<% end_if %>
<% end_control %>
<% end_loop %>
<% else %>
<p>There is no any student working on this project.</p>
<% end_if %>
@ -604,9 +604,9 @@ We can now do the same for every *Project* page by creating its own template.
<% if Modules %>
<ul>
<% control Modules %>
<% loop Modules %>
<li>$Name</li>
<% end_control %>
<% end_loop %>
</ul>
<% else %>
<p>This project has not used any modules.</p>
@ -655,13 +655,13 @@ We can now modify the *Project.ss* template.
<h3>Mentor</h3>
<% control MyStudent %>
<% loop MyStudent %>
<% if MyMentor %>
$MyMentor.PersonalInfo
<% else %>
<p>This student doesn't have any mentor.</p>
<% end_if %>
<% end_control %>
<% end_loop %>
<% else %>
<p>There is no any student working on this project.</p>
<% end_if %>
@ -703,7 +703,7 @@ it *MyProject* for instance.
...
public function MyProject() {
return DataObject::get( 'Project', "`MyStudentID` = '{$this->ID}'" );
return Project::get()->filter("MyStudentID", $this->ID);
}
}
@ -738,20 +738,20 @@ That's how we can use this function in the *Mentor* template.
</tr>
</thead>
<tbody>
<% control Students %>
<% loop Students %>
<tr>
<td>$FirstName $Lastname</td>
<td>
<% if MyProject %>
<% control MyProject %>
<% loop MyProject %>
$Title
<% end_control %>
<% end_loop %>
<% else %>
No Project
<% end_if %>
</td>
</tr>
<% end_control %>
<% end_loop %>
</tbody>
</table>
<% else %>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -393,7 +393,6 @@ class File extends DataObject {
return $fields;
}
/**
* Returns a category based on the file extension.
* This can be useful when grouping files by type,
@ -402,14 +401,23 @@ class File extends DataObject {
*
* @return String
*/
public function appCategory() {
$ext = strtolower($this->Extension);
public static function get_app_category($ext) {
$ext = strtolower($ext);
foreach(self::$app_categories as $category => $exts) {
if(in_array($ext, $exts)) return $category;
}
return false;
}
/**
* Returns a category based on the file extension.
*
* @return String
*/
public function appCategory() {
return self::get_app_category($this->Extension);
}
function CMSThumbnail() {
return '<img src="' . $this->Icon() . '" />';
}

View File

@ -41,7 +41,7 @@ class Folder extends File {
* @deprecated in favor of the correct name find_or_make
*/
public static function findOrMake($folderPath) {
Deprecation::notice('3.0', "Folder::findOrMake() is deprecated in favor of Folder::find_or_make()");
Deprecation::notice('3.0', "Use Folder::find_or_make() instead.");
return self::find_or_make($folderPath);
}

View File

@ -1,6 +1,6 @@
<?php
/**
* Shows two password-fields, and checks for matching passwords.
* Two masked input fields, checks for matching passwords.
* Optionally hides the fields by default and shows
* a link to toggle their visibility.
*

View File

@ -19,6 +19,8 @@ class CountryDropdownField extends DropdownField {
*/
static $default_country = 'NZ';
protected $extraClasses = array('dropdown');
/**
* Get the locale of the Member, or if we're not logged in or don't have a locale, use the default one
* @return string

View File

@ -1,6 +1,10 @@
<?php
/**
* CreditCard field, contains validation and formspec for creditcard fields.
* Allows input of credit card numbers via four separate form fields,
* including generic validation of its numeric values.
*
* @todo Validate
*
* @package forms
* @subpackage fields-formattedinput
*/

View File

@ -1,6 +1,9 @@
<?php
/**
* Currency field.
* Renders a text field, validating its input as a currency.
* Limited to US-centric formats, including a hardcoded currency
* symbol and decimal separators.
* See {@link MoneyField} for a more flexible implementation.
*
* @todo Add localization support, see http://open.silverstripe.com/ticket/2931
*

View File

@ -16,6 +16,7 @@ require_once 'Zend/Date.php';
* CAUTION: Might not be useable in combination with 'showcalendar', depending on the used javascript library
* - 'dmyseparator' (string): HTML markup to separate day, month and year fields.
* Only applicable with 'dmyfields'=TRUE. Use 'dateformat' to influence date representation with 'dmyfields'=FALSE.
* - 'dmyplaceholders': Show HTML5 placehoder text to allow identification of the three separate input fields
* - 'dateformat' (string): Date format compatible with Zend_Date.
* Usually set to default format for {@link locale} through {@link Zend_Locale_Format::getDateFormat()}.
* - 'datavalueformat' (string): Internal ISO format string used by {@link dataValue()} to save the
@ -64,6 +65,7 @@ class DateField extends TextField {
'jslocale' => null,
'dmyfields' => false,
'dmyseparator' => '&nbsp;<span class="separator">/</span>&nbsp;',
'dmyplaceholders' => true,
'dateformat' => null,
'datavalueformat' => 'yyyy-MM-dd',
'min' => null,
@ -120,6 +122,14 @@ class DateField extends TextField {
return $html;
}
function SmallFieldHolder($properties = array()){
$d = DateField_View_JQuery::create($this);
$d->onBeforeRender();
$html = parent::SmallFieldHolder($properties);
$html = $d->onAfterRender($html);
return $html;
}
function Field($properties = array()) {
$config = array(
'showcalendar' => $this->getConfig('showcalendar'),
@ -144,15 +154,21 @@ class DateField extends TextField {
$valArr = ($this->valueObj) ? $this->valueObj->toArray() : null;
// fields
$fieldDay = new NumericField($this->name . '[day]', false, ($valArr) ? $valArr['day'] : null);
$fieldDay->addExtraClass('day');
$fieldDay->setMaxLength(2);
$fieldMonth = new NumericField($this->name . '[month]', false, ($valArr) ? $valArr['month'] : null);
$fieldMonth->addExtraClass('month');
$fieldMonth->setMaxLength(2);
$fieldYear = new NumericField($this->name . '[year]', false, ($valArr) ? $valArr['year'] : null);
$fieldYear->addExtraClass('year');
$fieldYear->setMaxLength(4);
$fieldNames = Zend_Locale::getTranslationList('Field', $this->locale);
$fieldDay = NumericField::create($this->name . '[day]', false, ($valArr) ? $valArr['day'] : null)
->addExtraClass('day')
->setAttribute('placeholder', $this->getConfig('dmyplaceholders') ? $fieldNames['day'] : null)
->setMaxLength(2);
$fieldMonth = NumericField::create($this->name . '[month]', false, ($valArr) ? $valArr['month'] : null)
->addExtraClass('month')
->setAttribute('placeholder', $this->getConfig('dmyplaceholders') ? $fieldNames['month'] : null)
->setMaxLength(2);
$fieldYear = NumericField::create($this->name . '[year]', false, ($valArr) ? $valArr['year'] : null)
->addExtraClass('year')
->setAttribute('placeholder', $this->getConfig('dmyplaceholders') ? $fieldNames['year'] : null)
->setMaxLength(4);
// order fields depending on format
$sep = $this->getConfig('dmyseparator');

View File

@ -118,10 +118,10 @@ class DropdownField extends FormField {
$this->setSource($source);
if($emptyString === true) {
Deprecation::notice('3.1', 'Please use setHasEmptyDefault(true) instead of passing a boolean true $emptyString argument');
Deprecation::notice('3.1', 'Please use setHasEmptyDefault(true) instead of passing a boolean true $emptyString argument', Deprecation::SCOPE_GLOBAL);
}
if(is_string($emptyString)) {
Deprecation::notice('3.1', 'Please use setEmptyString() instead of passing a string $emptyString argument.');
Deprecation::notice('3.1', 'Please use setEmptyString() instead of passing a string $emptyString argument.', Deprecation::SCOPE_GLOBAL);
}
if($emptyString) $this->setHasEmptyDefault(true);

View File

@ -1,6 +1,8 @@
<?php
/**
* Text field with Email Validation.
* Text input field with validation for correct email format
* according to RFC 2822.
*
* @package forms
* @subpackage fields-formattedinput
*/

View File

@ -605,11 +605,14 @@ class FieldList extends ArrayList {
foreach($this->getTabPathRewrites() as $regex => $replace) {
if(preg_match($regex, $name)) {
$newName = preg_replace($regex, $replace, $name);
Deprecation::notice('3.0.0', sprintf(
'Using outdated tab path "%s", please use the new location "%s" instead',
$name,
$newName
));
Deprecation::notice('3.0.0',
sprintf(
'Using outdated tab path "%s", please use the new location "%s" instead',
$name,
$newName
),
Deprecation::SCOPE_GLOBAL
);
return $newName;
}
}

View File

@ -102,7 +102,13 @@ class FileField extends FormField {
* @param int $value The value of the field.
*/
function __construct($name, $title = null, $value = null) {
if(count(func_get_args()) > 3) Deprecation::notice('3.0', 'Use setRightTitle() and setFolderName() instead of constructor arguments');
if(count(func_get_args()) > 3) {
Deprecation::notice(
'3.0',
'Use setRightTitle() and setFolderName() instead of constructor arguments',
Deprecation::SCOPE_GLOBAL
);
}
$this->upload = new Upload();
@ -205,4 +211,16 @@ class FileField extends FormField {
return true;
}
/**
* @return Upload
*/
public function getUpload() {
return $this->upload;
}
public function setUpload(Upload $upload) {
$this->upload = $upload;
}
}

View File

@ -1,9 +1,6 @@
<?php
/**
* Single action button.
* The action buttons are <input type="submit"> tags.
*
* <b>Usage</b>
* The action buttons are <input type="submit"> as well as <button> tags.
*
* Upon clicking the button below will redirect the user to doAction under the current controller.
*

View File

@ -51,7 +51,7 @@ class HasManyComplexTableField extends ComplexTableField {
function __construct($controller, $name, $sourceClass, $fieldList = null, $detailFormFields = null, $sourceFilter = "", $sourceSort = "", $sourceJoin = "") {
parent::__construct($controller, $name, $sourceClass, $fieldList, $detailFormFields, $sourceFilter, $sourceSort, $sourceJoin);
Deprecation::notice('3.0', 'Use GridField with GridFieldConfig_RelationEditor');
Deprecation::notice('3.0', 'Use GridField with GridFieldConfig_RelationEditor', Deprecation::SCOPE_CLASS);
$this->Markable = true;

View File

@ -46,7 +46,7 @@ class HtmlEditorField extends TextareaField {
* @see TextareaField::__construct()
*/
public function __construct($name, $title = null, $value = '') {
if(count(func_get_args()) > 3) Deprecation::notice('3.0', 'Use setRows() and setCols() instead of constructor arguments');
if(count(func_get_args()) > 3) Deprecation::notice('3.0', 'Use setRows() and setCols() instead of constructor arguments', Deprecation::SCOPE_GLOBAL);
parent::__construct($name, $title, $value);
@ -63,14 +63,14 @@ class HtmlEditorField extends TextareaField {
if($links = $value->getElementsByTagName('a')) foreach($links as $link) {
$matches = array();
if(preg_match('/\[sitetree_link id=([0-9]+)\]/i', $link->getAttribute('href'), $matches)) {
if(preg_match('/\[sitetree_link(?:\s*|%20|,)?id=([0-9]+)\]/i', $link->getAttribute('href'), $matches)) {
if(!DataObject::get_by_id('SiteTree', $matches[1])) {
$class = $link->getAttribute('class');
$link->setAttribute('class', ($class ? "$class ss-broken" : 'ss-broken'));
}
}
if(preg_match('/\[file_link id=([0-9]+)\]/i', $link->getAttribute('href'), $matches)) {
if(preg_match('/\[file_link(?:\s*|%20|,)?id=([0-9]+)\]/i', $link->getAttribute('href'), $matches)) {
if(!DataObject::get_by_id('File', $matches[1])) {
$class = $link->getAttribute('class');
$link->setAttribute('class', ($class ? "$class ss-broken" : 'ss-broken'));
@ -114,7 +114,7 @@ class HtmlEditorField extends TextareaField {
$href = Director::makeRelative($link->getAttribute('href'));
if($href) {
if(preg_match('/\[sitetree_link id=([0-9]+)\]/i', $href, $matches)) {
if(preg_match('/\[sitetree_link,id=([0-9]+)\]/i', $href, $matches)) {
$ID = $matches[1];
// clear out any broken link classes
@ -299,9 +299,11 @@ class HtmlEditorField_Toolbar extends RequestHandler {
$this->controller,
"{$this->name}/LinkForm",
new FieldList(
new LiteralField(
'Heading',
sprintf('<h3>%s</h3>', _t('HtmlEditorField.LINK', 'Insert Link'))
$headerWrap = new CompositeField(
new LiteralField(
'Heading',
sprintf('<h3 class="htmleditorfield-mediaform-heading insert">%s</h3>', _t('HtmlEditorField.LINK', 'Insert Link'))
)
),
$contentComposite = new CompositeField(
new OptionsetField(
@ -341,7 +343,8 @@ class HtmlEditorField_Toolbar extends RequestHandler {
)
);
$contentComposite->addExtraClass('content');
$headerWrap->addExtraClass('CompositeField composite cms-content-header nolabel ');
$contentComposite->addExtraClass('ss-insert-link content');
$form->unsetValidator();
$form->loadDataFrom($this);
@ -362,11 +365,14 @@ class HtmlEditorField_Toolbar extends RequestHandler {
// TODO Handle through GridState within field - currently this state set too late to be useful here (during request handling)
$parentID = $this->controller->getRequest()->requestVar('ParentID');
$fileFieldConfig = GridFieldConfig::create();
$fileFieldConfig->addComponent(new GridFieldSortableHeader());
$fileFieldConfig->addComponent(new GridFieldFilterHeader());
$fileFieldConfig->addComponent(new GridFieldDataColumns());
$fileFieldConfig->addComponent(new GridFieldPaginator(5));
$fileFieldConfig = GridFieldConfig::create()->addComponents(
new GridFieldFilterHeader(),
new GridFieldSortableHeader(),
new GridFieldDataColumns(),
new GridFieldPaginator(5),
new GridFieldDeleteAction(),
new GridFieldDetailForm()
);
$fileField = new GridField('Files', false, null, $fileFieldConfig);
$fileField->setList($this->getFiles($parentID));
$fileField->setAttribute('data-selectable', true);
@ -512,7 +518,8 @@ class HtmlEditorField_Toolbar extends RequestHandler {
}
// Instanciate file wrapper and get fields based on its type
if($file && $file->appCategory() == 'image') {
// Check if appCategory is an image and exists on the local system, otherwise use oEmbed to refference a remote image
if($file && $file->appCategory() == 'image' && Director::is_site_url($url)) {
$fileWrapper = new HtmlEditorField_Image($url, $file);
} elseif(!Director::is_site_url($url)) {
$fileWrapper = new HtmlEditorField_Embed($url, $file);

View File

@ -45,7 +45,7 @@ class ImageField extends FileIFrameField {
* @return Form
*/
public function EditFileForm() {
Deprecation::notice('3.0', 'Use UploadField');
Deprecation::notice('3.0', 'Use UploadField', Deprecation::SCOPE_CLASS);
$filter = create_function('$item', 'return (in_array("Folder", ClassInfo::ancestry($item->ClassName)) || in_array("Image", ClassInfo::ancestry($item->ClassName)));');

View File

@ -18,7 +18,7 @@ class ImageFormAction extends FormAction {
* @param form The parent form, auto-set when the field is placed inside a form
*/
function __construct($action, $title = "", $image = "", $hoverImage = null, $className = null, $form = null) {
Deprecation::notice('3.0', "Use FormAction with setAttribute('src', 'myimage.png') and custom JavaScript to achieve hover effect");
Deprecation::notice('3.0', "Use FormAction with setAttribute('src', 'myimage.png') and custom JavaScript to achieve hover effect", Deprecation::SCOPE_CLASS);
$this->image = $image;
$this->hoverImage = $hoverImage;

View File

@ -43,7 +43,7 @@ class ManyManyComplexTableField extends HasManyComplexTableField {
function __construct($controller, $name, $sourceClass, $fieldList = null, $detailFormFields = null, $sourceFilter = "", $sourceSort = "", $sourceJoin = "") {
Deprecation::notice('3.0', 'Use GridField with GridFieldConfig_RelationEditor');
Deprecation::notice('3.0', 'Use GridField with GridFieldConfig_RelationEditor', Deprecation::SCOPE_CLASS);
parent::__construct($controller, $name, $sourceClass, $fieldList, $detailFormFields, $sourceFilter, $sourceSort, $sourceJoin);

View File

@ -1,7 +1,7 @@
<?php
/**
* A Single Numeric field extending a typical
* TextField but with validation.
* Text input field with validation for numeric values.
*
* @package forms
* @subpackage fields-formattedinput
*/

View File

@ -1,7 +1,6 @@
<?php
/**
* Set of radio buttons designed to emulate a dropdown.
* It even uses the same constructor as a dropdown field.
*
* This field allows you to ensure that a form element is submitted is not optional and is part of a fixed set of
* data. This field uses the input type of radio. It's a direct subclass of {@link DropdownField},

View File

@ -11,7 +11,7 @@ class PasswordField extends TextField {
* maxlength
*/
function __construct($name, $title = null, $value = "") {
if(count(func_get_args()) > 3) Deprecation::notice('3.0', 'Use setMaxLength() instead of constructor arguments');
if(count(func_get_args()) > 3) Deprecation::notice('3.0', 'Use setMaxLength() instead of constructor arguments', Deprecation::SCOPE_GLOBAL);
parent::__construct($name, $title, $value);
}

View File

@ -1,6 +1,6 @@
<?php
/**
* SelectionGroup represents a number of fields that are selectable by a radio
* SelectionGroup represents a number of fields which are selectable by a radio
* button that appears at the beginning of each item. Using CSS, you can
* configure the field to only display its contents if the corresponding radio
* button is selected.

View File

@ -69,9 +69,9 @@
class SimpleImageField extends FileField {
function __construct($name, $title = null, $value = null) {
Deprecation::notice('3.0', "Use UploadField with \$myField->allowedExtensions = array('jpg', 'gif', 'png')");
Deprecation::notice('3.0', "SimpleImageField is deprecated. Use UploadField with \$myField->allowedExtensions = array('jpg', 'gif', 'png')", Deprecation::SCOPE_CLASS);
if(count(func_get_args()) > 3) Deprecation::notice('3.0', 'Use setRightTitle() and setFolderName() instead of constructor arguments');
if(count(func_get_args()) > 3) Deprecation::notice('3.0', 'Use setRightTitle() and setFolderName() instead of constructor arguments', Deprecation::SCOPE_GLOBAL);
parent::__construct($name, $title, $value);

View File

@ -72,7 +72,7 @@ class TabSet extends CompositeField {
Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery-ui/jquery-ui.js');
Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery-cookie/jquery.cookie.js');
Requirements::css(FRAMEWORK_DIR . '/thirdparty/jquery-ui-themes/smoothness/jquery.ui.css');
Requirements::css(FRAMEWORK_DIR . '/thirdparty/jquery-ui-themes/smoothness/jquery-ui.css');
Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery-entwine/dist/jquery.entwine-dist.js');

View File

@ -66,6 +66,7 @@ class ToggleCompositeField extends CompositeField {
*/
public function setStartClosed($bool) {
$this->startClosed = (bool) $bool;
return $this;
}
/**
@ -80,6 +81,7 @@ class ToggleCompositeField extends CompositeField {
*/
public function setHeadingLevel($level) {
$this->headingLevel = $level;
return $this;
}
/**

15
forms/gridfield/GridField.php Executable file → Normal file
View File

@ -80,11 +80,7 @@ class GridField extends FormField {
$this->setList($dataList);
}
if(!$config) {
$this->config = GridFieldConfig_Base::create();
} else {
$this->config = $config;
}
$this->setConfig($config ?: GridFieldConfig_Base::create());
$this->config->addComponent(new GridState_Component());
$this->state = new GridState($this);
@ -134,6 +130,15 @@ class GridField extends FormField {
return $this->config;
}
/**
* @param GridFieldConfig $config
* @return GridField
*/
public function setConfig(GridFieldConfig $config) {
$this->config = $config;
return $this;
}
public function getComponents() {
return $this->config->getComponents();
}

28
forms/gridfield/GridFieldAddExistingAutocompleter.php Executable file → Normal file
View File

@ -41,6 +41,11 @@ class GridFieldAddExistingAutocompleter implements GridField_HTMLProvider, GridF
*/
protected $placeholderText;
/**
* @var int
*/
protected $resultsLimit = 20;
/**
*
* @param array $searchFields Which fields on the object in the list should be searched
@ -69,7 +74,7 @@ class GridFieldAddExistingAutocompleter implements GridField_HTMLProvider, GridF
// Apparently the data-* needs to be double qouted for the jQuery.meta data plugin
$searchField->setAttribute('data-search-url', '\''.Controller::join_links($gridField->Link('search').'\''));
$searchField->setAttribute('placeholder', $this->getPlaceholderText($dataClass));
$searchField->addExtraClass('relation-search');
$searchField->addExtraClass('relation-search no-change-track');
$findAction = new GridField_FormAction($gridField, 'gridfield_relationfind', _t('GridField.Find', "Find"), 'find', 'find');
$findAction->setAttribute('data-icon', 'relationfind');
@ -151,7 +156,7 @@ class GridFieldAddExistingAutocompleter implements GridField_HTMLProvider, GridF
*/
public function getURLHandlers($gridField) {
return array(
'search/$ID' => 'doSearch',
'search' => 'doSearch',
);
}
@ -176,10 +181,11 @@ class GridFieldAddExistingAutocompleter implements GridField_HTMLProvider, GridF
// TODO Replace with DataList->filterAny() once it correctly supports OR connectives
foreach($searchFields as $searchField) {
$stmts[] .= sprintf('"%s" LIKE \'%s%%\'', $searchField, $request->param('ID'));
$stmts[] .= sprintf('"%s" LIKE \'%s%%\'', $searchField, Convert::raw2sql($request->getVar('gridfield_relationsearch')));
}
$results = $allList->where(implode(' OR ', $stmts))->subtract($gridField->getList());
$results = $results->sort($searchFields[0], 'ASC');
$results = $results->limit($this->getResultsLimit());
$json = array();
foreach($results as $result) {
@ -272,6 +278,22 @@ class GridFieldAddExistingAutocompleter implements GridField_HTMLProvider, GridF
$this->placeholderText = $text;
}
/**
* Gets the maximum number of autocomplete results to display.
*
* @return int
*/
public function getResultsLimit() {
return $this->resultsLimit;
}
/**
* @param int $limit
*/
public function setResultsLimit($limit) {
$this->resultsLimit = $limit;
}
/**
* This will provide a StartsWith search that only returns a value if we are
* matching ONE object only. We wouldn't want to attach used any object to

View File

@ -83,11 +83,6 @@ class GridFieldDataColumns implements GridField_ColumnProvider {
}
/**
* Specify custom formatting for fields, e.g. to render a link instead of pure text.
* Caution: Make sure to escape special php-characters like in a normal php-statement.
* Example: "myFieldName" => '<a href=\"custom-admin/$ID\">$ID</a>'.
* Alternatively, pass a anonymous function, which takes one parameter: The list item.
*
* @return array
*/
public function getFieldCasting() {
@ -95,6 +90,12 @@ class GridFieldDataColumns implements GridField_ColumnProvider {
}
/**
* Specify custom formatting for fields, e.g. to render a link instead of pure text.
* Caution: Make sure to escape special php-characters like in a normal php-statement.
* Example: "myFieldName" => '<a href=\"custom-admin/$ID\">$ID</a>'.
* Alternatively, pass a anonymous function, which takes two parameters:
* The value returned by Convert::raw2xml and the original list item.
*
* @param array $formatting
*/
public function setFieldFormatting($formatting) {

View File

@ -328,11 +328,15 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
$form->addExtraClass('cms-content cms-edit-form center ss-tabset');
$form->setAttribute('data-pjax-fragment', 'CurrentForm Content');
if($form->Fields()->hasTabset()) $form->Fields()->findOrMakeTab('Root')->setTemplate('CMSTabSet');
// TODO Link back to controller action (and edited root record) rather than index,
// which requires more URL knowledge than the current link to this field gives us.
// The current root record is held in session only,
// e.g. page/edit/show/6/ vs. page/edit/EditForm/field/MyGridField/....
$form->Backlink = $toplevelController->hasMethod('Backlink') ? $toplevelController->Backlink() : $toplevelController->Link();
if($toplevelController->hasMethod('Backlink')) {
$form->Backlink = $toplevelController->Backlink();
} elseif($this->popupController->hasMethod('Breadcrumbs')) {
$parents = $this->popupController->Breadcrumbs(false)->items;
$form->Backlink = array_pop($parents)->Link;
} else {
$form->Backlink = $toplevelController->Link();
}
}
$cb = $this->component->getItemEditFormCallback();

View File

@ -91,12 +91,13 @@ class GridFieldFilterHeader implements GridField_HTMLProvider, GridField_DataMan
}
$filterArguments = $state->Columns->toArray();
$dataListClone = null;
foreach($filterArguments as $columnName => $value ) {
if($dataList->canFilterBy($columnName) && $value) {
$dataList->filter($columnName.':PartialMatch', $value);
$dataListClone = $dataList->filter($columnName.':PartialMatch', $value);
}
}
return $dataList;
return ($dataListClone) ? $dataListClone : $dataList;
}
public function getHTMLFragments($gridField) {
@ -120,6 +121,7 @@ class GridFieldFilterHeader implements GridField_HTMLProvider, GridField_DataMan
}
$field = new TextField('filter['.$columnField.']', '', $value);
$field->addExtraClass('ss-gridfield-sort');
$field->addExtraClass('no-change-track');
$field->setAttribute('placeholder', _t('GridField.FilterBy', "Filter by ") . _t('GridField.'.$metadata['title'], $metadata['title']));
@ -144,6 +146,7 @@ class GridFieldFilterHeader implements GridField_HTMLProvider, GridField_DataMan
->setAttribute('id', 'action_reset_' . $gridField->getModelClass() . '_' . $columnField)
);
$field->addExtraClass('filter-buttons');
$field->addExtraClass('no-change-track');
}else{
$field = new LiteralField('', '');
}

View File

@ -1,49 +1,84 @@
<?php
/**
* Adds a "level up" link to a GridField table, which is useful
* when viewing hierarchical data. Requires the managed record
* to have a "getParent()" method or has_one relationship called "Parent".
*/
class GridFieldLevelup extends Object implements GridField_HTMLProvider{
class GridFieldLevelup implements GridField_HTMLProvider{
/**
* @var integer - the record id of the level up to
*/
protected $levelID = null;
protected $currentID = null;
/**
* sprintf() spec for link to link to parent.
* Only supports one variable replacement - the parent ID.
* @var string
*/
protected $linkSpec = '';
/**
* @var array Extra attributes for the link
*/
protected $attributes = array();
/**
*
* @param integer $levelID - the record id of the level up to
* @param integer $currentID - The ID of the current item; this button will find that item's parent
*/
public function __construct($levelID = null) {
if($levelID && is_numeric($levelID)) {
$this->levelID = $levelID;
}
public function __construct($currentID) {
if($currentID && is_numeric($currentID)) $this->currentID = $currentID;
}
public function getHTMLFragments($gridField) {
$modelClass = $gridField->getModelClass();
if(isset($_GET['ParentID']) && $_GET['ParentID']){
$parentID = 0;
$modelObj = DataObject::get_by_id($modelClass, $_GET['ParentID']);
if($this->currentID) {
$modelObj = DataObject::get_by_id($modelClass, $this->currentID);
if(is_callable(array($modelObj, 'getParent'))){
$levelup = $modelObj->getParent();
if(!$levelup){
$parentID = 0;
}else{
$parentID = $levelup->ID;
}
if($modelObj->hasMethod('getParent')) {
$parent = $modelObj->getParent();
} elseif($modelObj->ParentID) {
$parent = $modelObj->Parent();
}
//$controller = $gridField->getForm()->Controller();
if($parent) $parentID = $parent->ID;
// Attributes
$attrs = array_merge($this->attributes, array(
'href' => sprintf($this->linkSpec, $parentID),
'class' => 'cms-panel-link list-parent-link'
));
$attrsStr = '';
foreach($attrs as $k => $v) $attrsStr .= " $k=\"" . Convert::raw2att($v) . "\"";
$forTemplate = new ArrayData(array(
'UpLink' => sprintf(
'<a class="cms-panel-link list-parent-link" href="?ParentID=%d&view=list" data-pjax-target="ListViewForm,Breadcrumbs">%s</a>',
$parentID,
_t('GridField.LEVELUP', 'Level up' )
),
'UpLink' => sprintf('<a%s>%s</a>', $attrsStr, _t('GridField.LEVELUP', 'Level up'))
));
return array(
'before' => $forTemplate->renderWith('GridFieldLevelup'),
//'header' => $forTemplate->renderWith('GridFieldLevelup_Row'),
);
}
}
public function setAttributes($attrs) {
$this->attributes = $attrs;
return $this;
}
public function getAttributes() {
return $this->attributes;
}
public function setLinkSpec($link) {
$this->linkSpec = $link;
return $this;
}
public function getLinkSpec() {
return $this->linkSpec;
}
}
?>

View File

@ -444,7 +444,7 @@ class i18n extends Object implements TemplateGlobalProvider {
'mfe_MU' => 'Morisyen (Mauritius)',
'mg_MG' => 'Malagasy (Madagascar)',
'mh_MH' => 'Marshallese (Marshall Islands)',
'mi_NZ' => 'Maori (New Zealand)',
'mi_NZ' => 'Māori (New Zealand)',
'min_ID' => 'Minangkabau (Indonesia)',
'mk_MK' => 'Macedonian (Macedonia)',
'ml_IN' => 'Malayalam (India)',
@ -685,7 +685,7 @@ class i18n extends Object implements TemplateGlobalProvider {
'lt' => array('Lithuanian', 'lietuvi&#353;kai'),
'lmo' => array('Lombard', 'Lombardo'),
'mk' => array('Macedonian', '&#1084;&#1072;&#1082;&#1077;&#1076;&#1086;&#1085;&#1089;&#1082;&#1080;'),
'mi' => array('Maori', 'Maori'),
'mi' => array('Maori', 'Māori'),
'ms' => array('Malay', 'Bahasa melayu'),
'mt' => array('Maltese', 'Malti'),
'mr' => array('Marathi', '&#2350;&#2352;&#2366;&#2336;&#2368;'),
@ -777,7 +777,7 @@ class i18n extends Object implements TemplateGlobalProvider {
'lv_LV' => array('Latvian', 'latvie&#353;u'),
'lt_LT' => array('Lithuanian', 'lietuvi&#353;kai'),
'mk_MK' => array('Macedonian', '&#1084;&#1072;&#1082;&#1077;&#1076;&#1086;&#1085;&#1089;&#1082;&#1080;'),
'mi_NZ' => array('Maori', 'Maori'),
'mi_NZ' => array('Maori', 'Māori'),
'ms_MY' => array('Malay', 'Bahasa melayu'),
'mt_MT' => array('Maltese', 'Malti'),
'mr_IN' => array('Marathi', '&#2350;&#2352;&#2366;&#2336;&#2368;'),
@ -1465,7 +1465,8 @@ class i18n extends Object implements TemplateGlobalProvider {
if(is_numeric($context) && in_array($context, array(PR_LOW, PR_MEDIUM, PR_HIGH))) {
Deprecation::notice(
'3.0',
'The $priority argument to _t() is deprecated, please use module inclusion priorities instead'
'The $priority argument to _t() is deprecated, please use module inclusion priorities instead',
Deprecation::SCOPE_GLOBAL
);
}

View File

@ -310,9 +310,9 @@ class i18nTextCollector extends Object {
// use the old method of getting _t() style translatable entities
// Collect in actual template
if(preg_match_all('/<%\s*(_t\(.*)%>/ms', $content, $matches)) {
foreach($matches as $match) {
$entities = array_merge($entities, $this->collectFromCode($match[0], $module));
if(preg_match_all('/(_t\([^\)]*?\))/ms', $content, $matches)) {
foreach($matches[1] as $match) {
$entities = array_merge($entities, $this->collectFromCode($match, $module));
}
}

View File

@ -26,7 +26,7 @@
}
});
$('.field.date input.text').live('click', function() {
$('.field.date input.text,.fieldholder-small input.text.date').live('click', function() {
$(this).ssDatepicker();
$(this).datepicker('show');
});

View File

@ -301,7 +301,7 @@
"X-Pjax" : 'Partial'
},
type: "GET",
url: suggestionUrl+'/'+request.term,
url: suggestionUrl,
data: form.serialize()+'&'+escape(searchField.attr('name'))+'='+escape(searchField.val()),
success: function(data) {
response( $.map(JSON.parse(data), function( name, id ) {

View File

@ -283,7 +283,7 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
onbeforestatechange: function(){
this.css('visibility', 'hidden');
var ed = this.getEditor(), container = ed.getInstance() ? ed.getContainer() : null;
var ed = this.getEditor(), container = (ed && ed.getInstance()) ? ed.getContainer() : null;
if(container && container.length) container.remove();
}
},
@ -468,7 +468,6 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
* which are toggled through a type dropdown. Variations share fields, so there's only one "title" field in the form.
*/
$('form.htmleditorfield-linkform').entwine({
// TODO Entwine doesn't respect submits triggered by ENTER key
onsubmit: function(e) {
this.insertLink();
@ -477,36 +476,27 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
},
resetFields: function() {
this._super();
this.find('fieldset :input:not(:radio)').val('').change();
// Reset the form using a native call. This will also correctly reset checkboxes and radio buttons.
this[0].reset();
},
redraw: function(setDefaults) {
redraw: function() {
this._super();
var linkType = this.find(':input[name=LinkType]:checked').val(), list = ['internal', 'external', 'file', 'email'];
// If we haven't selected an existing link, then just make sure we default to "internal" for the link type.
if(!linkType) {
this.find(':input[name=LinkType]').val(['internal']);
linkType = 'internal';
}
this.addAnchorSelector();
// Toggle field visibility and state based on type selection
// Toggle field visibility depending on the link type.
this.find('div.content .field').hide();
this.find('.field#LinkType').show();
this.find('.field#' + linkType).show();
if(linkType == 'internal' || linkType == 'anchor') this.find('.field#Anchor').show();
if(linkType !== 'email') this.find('.field#TargetBlank').show();
if(linkType == 'anchor') {
this.find('.field#AnchorSelector').show();
this.find('.field#AnchorRefresh').show();
}
this.find(':input[name=TargetBlank]').attr('disabled', (linkType == 'email'));
if(typeof setDefaults == 'undefined' || setDefaults) {
this.find(':input[name=TargetBlank]').attr('checked', (linkType == 'file'));
}
},
insertLink: function() {
var href, target = null, anchor = this.find(':input[name=Anchor]').val();
@ -614,6 +604,10 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
selector.append($('<option value="'+anchors[j]+'">'+anchors[j]+'</option>'));
}
},
/**
* Updates the state of the dialog inputs to match the editor selection.
* If selection does not contain a link, resets the fields.
*/
updateFromEditor: function() {
var htmlTagPattern = /<\S[^><]*>/g, fieldName, data = this.getCurrentLink();
@ -622,8 +616,12 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
var el = this.find(':input[name=' + fieldName + ']'), selected = data[fieldName];
// Remove html tags in the selected text that occurs on IE browsers
if(typeof(selected) == 'string') selected = selected.replace(htmlTagPattern, '');
if(el.is(':radio')) {
el.val([selected]).change(); // setting as an arry due to jQuery quirks
// Set values and invoke the triggers (e.g. for TreeDropdownField).
if(el.is(':checkbox')) {
el.prop('checked', selected).change();
} else if(el.is(':radio')) {
el.val([selected]).change();
} else {
el.val(selected).change();
}
@ -631,8 +629,9 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
}
},
/**
* Return information about the currently selected link, suitable for population of the link
* form.
* Return information about the currently selected link, suitable for population of the link form.
*
* Returns null if no link was currently selected.
*/
getCurrentLink: function() {
var selectedEl = this.getSelection(),
@ -682,7 +681,8 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
return {
LinkType: 'file',
file: RegExp.$1,
Description: title
Description: title,
TargetBlank: target ? true : false
};
} else if(href.match(/^#(.*)$/)) {
return {
@ -691,7 +691,7 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
Description: title,
TargetBlank: target ? true : false
};
} else if(href.match(/^\[sitetree_link\s*(?:%20)?id=([0-9]+)\]?(#.*)?$/)) {
} else if(href.match(/^\[sitetree_link(?:\s*|%20|,)?id=([0-9]+)\]?(#.*)?$/i)) {
return {
LinkType: 'internal',
internal: RegExp.$1,
@ -707,9 +707,8 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
TargetBlank: target ? true : false
};
} else {
return {
LinkType: 'internal'
};
// No link/invalid link selected.
return null;
}
}
});

View File

@ -2,10 +2,10 @@
$.entwine('ss', function($){
$('.ss-toggle').entwine({
onadd: function() {
opts = {collapsible: true};
if (this.hasClass("ss-toggle-start-closed")) opts.active = false;
this.accordion({ collapsible: true });
this.accordion({
collapsible: true,
active: !this.hasClass("ss-toggle-start-closed")
});
this._super();
},

View File

@ -29,7 +29,7 @@
* @todo Expand title height to fit all elements
*/
$('.TreeDropdownField').entwine({
onmatch: function() {
onadd: function() {
this.append(
'<span class="treedropdownfield-title"></span>' +
'<div class="treedropdownfield-toggle-panel-link"><a href="#" class="ui-icon ui-icon-triangle-1-s"></a></div>' +
@ -43,9 +43,6 @@
this.getPanel().hide();
this._super();
},
onunmatch: function() {
this._super();
},
getPanel: function() {
return this.find('.treedropdownfield-panel');
},
@ -260,7 +257,7 @@
});
$('.TreeDropdownField.searchable').entwine({
onmatch: function() {
onadd: function() {
this._super();
var title = this.data('title');
@ -270,9 +267,6 @@
this.setTitle(title ? title : strings.searchFieldTitle);
},
onunmatch: function() {
this._super();
},
setTitle: function(title) {
if(!title && title !== '') title = strings.fieldTitle;
@ -372,8 +366,13 @@
});
$('.TreeDropdownField input[type=hidden]').entwine({
onchange: function() {
this.getField().updateTitle();
onadd: function() {
this.bind('change.TreeDropdownField', function() {
$(this).getField().updateTitle();
});
},
onremove: function() {
this.unbind('.TreeDropdownField');
}
});
});

View File

@ -36,9 +36,6 @@ if (version_compare(phpversion(), '5.3.2', '<')) {
* After that, it calls {@link Director::direct()}, which is responsible for doing most of the
* real work.
*
* Finally, main.php will use {@link Profiler} to show a profile if the querystring variable
* "debug_profile" is set.
*
* CONFIGURING THE WEBSERVER
*
* To use SilverStripe, every request that doesn't point directly to a file should be rewritten to
@ -89,12 +86,6 @@ if (isset($_GET['url'])) {
// Remove base folders from the URL if webroot is hosted in a subfolder
if (substr(strtolower($url), 0, strlen(BASE_URL)) == strtolower(BASE_URL)) $url = substr($url, strlen(BASE_URL));
if (isset($_GET['debug_profile'])) {
Profiler::init();
Profiler::mark('all_execution');
Profiler::mark('main.php init');
}
// Connect to database
require_once('model/DB.php');
@ -114,20 +105,8 @@ if(!isset($databaseConfig) || !isset($databaseConfig['database']) || !$databaseC
die();
}
if (isset($_GET['debug_profile'])) Profiler::mark('DB::connect');
DB::connect($databaseConfig);
if (isset($_GET['debug_profile'])) Profiler::unmark('DB::connect');
if (isset($_GET['debug_profile'])) Profiler::unmark('main.php init');
// Direct away - this is the "main" function, that hands control to the appropriate controller
DataModel::set_inst(new DataModel());
Director::direct($url, DataModel::inst());
if (isset($_GET['debug_profile'])) {
Profiler::unmark('all_execution');
if(!Director::isLive()) {
Profiler::show(isset($_GET['profile_trace']));
}
}

View File

@ -4,6 +4,6 @@
*/
class ComponentSet extends DataObjectSet {
function setComponentInfo($type, $ownerObj, $ownerClass, $tableName, $childClass, $joinField = null) {
Deprecation::notice('3.0', 'Use ManyManyList or HasManyList instead.');
Deprecation::notice('3.0', 'ComponentSet is deprecated. Use ManyManyList or HasManyList instead.', Deprecation::SCOPE_CLASS);
}
}

View File

@ -41,7 +41,7 @@ abstract class DataExtension extends Extension {
$statics = Injector::inst()->get($extensionClass, true, $args)->$extraStaticsMethod($class, $extensionClass);
if ($statics) {
Deprecation::notice('3.1.0', "$extraStaticsMethod deprecated. Just define statics on your extension, or use add_to_class");
Deprecation::notice('3.1.0', "$extraStaticsMethod deprecated. Just define statics on your extension, or use add_to_class", Deprecation::SCOPE_GLOBAL);
// TODO: This currently makes extraStatics the MOST IMPORTANT config layer, not the least
foreach (self::$extendable_statics as $key => $merge) {

View File

@ -141,7 +141,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
return $this;
}
if($limit && !is_numeric($limit)) {
Deprecation::notice('3.0', 'Please pass limits as 2 arguments, rather than an array or SQL fragment.');
Deprecation::notice('3.0', 'Please pass limits as 2 arguments, rather than an array or SQL fragment.', Deprecation::SCOPE_GLOBAL);
}
$this->dataQuery->limit($limit, $offset);
return $this;
@ -253,7 +253,13 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
$comparisor = $this->applyFilterContext($field, $fieldArg, $value);
}
} else {
$SQL_Statements[] = '"'.Convert::raw2sql($field).'" '.$customQuery;
if($field == 'ID') {
$field = sprintf('"%s"."ID"', ClassInfo::baseDataClass($this->dataClass));
} else {
$field = '"' . Convert::raw2sql($field) . '"';
}
$SQL_Statements[] = $field . ' ' . $customQuery;
}
}
if(count($SQL_Statements)) {
@ -350,10 +356,16 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
$SQL_Statements = array();
foreach($whereArguments as $fieldName => $value) {
if(is_array($value)){
$SQL_Statements[] = ('"'.$fieldName.'" NOT IN (\''.implode('\',\'', Convert::raw2sql($value)).'\')');
if($fieldName == 'ID') {
$fieldName = sprintf('"%s"."ID"', ClassInfo::baseDataClass($this->dataClass));
} else {
$SQL_Statements[] = ('"'.$fieldName.'" != \''.Convert::raw2sql($value).'\'');
$fieldName = '"' . Convert::raw2sql($fieldName) . '"';
}
if(is_array($value)){
$SQL_Statements[] = ($fieldName . ' NOT IN (\''.implode('\',\'', Convert::raw2sql($value)).'\')');
} else {
$SQL_Statements[] = ($fieldName . ' != \''.Convert::raw2sql($value).'\'');
}
}
$this->dataQuery->whereAny($SQL_Statements);
@ -587,7 +599,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
* @return DataList
*/
public function getRange($offset, $length) {
Deprecation::notice("3.0", 'getRange($offset, $length) is deprecated. Use limit($length, $offset) instead. Note the new argument order.');
Deprecation::notice("3.0", 'Use limit($length, $offset) instead. Note the new argument order.');
return $this->limit($length, $offset);
}

View File

@ -295,6 +295,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* don't have their defaults set.
*/
function __construct($record = null, $isSingleton = false, $model = null) {
parent::__construct();
// Set the fields data.
if(!$record) {
$record = array(
@ -345,8 +348,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
HTTP::register_modification_date($record['LastEdited']);
}
parent::__construct();
// Must be called after parent constructor
if(!$isSingleton && (!isset($this->record['ID']) || !$this->record['ID'])) {
$this->populateDefaults();
@ -2595,7 +2596,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// Todo: Determine if we can deprecate for 3.0.0 and use DI or something instead
// Todo: Make the $containerClass method redundant
if($containerClass != 'DataList') {
Deprecation::notice('3.0', '$containerClass argument is deprecated.');
Deprecation::notice('3.0', 'DataObject::get() - $containerClass argument is deprecated.', Deprecation::SCOPE_GLOBAL);
}
$result = DataList::create($callerClass)->where($filter)->sort($sort);

View File

@ -7,7 +7,7 @@
abstract class DataObjectDecorator extends DataExtension {
public function __construct() {
Deprecation::notice('3.0', 'Use DataExtension instead.');
Deprecation::notice('3.0', 'DataObjectDecorator is deprecated. Use DataExtension instead.', Deprecation::SCOPE_CLASS);
parent::__construct();
}

View File

@ -10,7 +10,7 @@ class DataObjectSet extends ArrayList {
* @deprecated 3.0
*/
public function __construct($items = array()) {
Deprecation::notice('3.0', 'Use DataList or ArrayList instead');
Deprecation::notice('3.0', 'DataObjectSet is deprecated. Use DataList or ArrayList instead', Deprecation::SCOPE_CLASS);
if ($items) {
if (!is_array($items) || func_num_args() > 1) {

View File

@ -223,7 +223,7 @@ class DataQuery {
}
$query->selectField("\"$baseClass\".\"ID\"", "ID");
$query->selectField("CASE WHEN \"$baseClass\".\"ClassName\" IS NOT NULL THEN \"$baseClass\".\"ClassName\" ELSE '$baseClass' END", "RecordClassName");
$query->selectField("CASE WHEN \"$baseClass\".\"ClassName\" IS NOT NULL THEN \"$baseClass\".\"ClassName\" ELSE ".DB::getConn()->prepStringForDB($baseClass)." END", "RecordClassName");
// TODO: Versioned, Translatable, SiteTreeSubsites, etc, could probably be better implemented as subclasses of DataQuery
@ -601,7 +601,6 @@ function max($field) {
foreach($ancestry as $ancestor){
if($ancestor != $component){
$this->query->addInnerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\"");
$component=$ancestor;
}
}
}
@ -623,7 +622,6 @@ function max($field) {
foreach($ancestry as $ancestor){
if($ancestor != $component){
$this->query->addInnerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\"");
$component=$ancestor;
}
}
}

View File

@ -455,8 +455,6 @@ abstract class SS_Database {
$fieldValue = null;
$newTable = false;
Profiler::mark('requireField');
// backwards compatibility patch for pre 2.4 requireField() calls
$spec_orig=$spec;
@ -507,10 +505,7 @@ abstract class SS_Database {
}
if($newTable || $fieldValue=='') {
Profiler::mark('createField');
$this->transCreateField($table, $field, $spec_orig);
Profiler::unmark('createField');
$this->alterationMessage("Field $table.$field: created as $spec_orig","created");
} else if($fieldValue != $specValue) {
// If enums/sets are being modified, then we need to fix existing data in the table.
@ -545,12 +540,9 @@ abstract class SS_Database {
}
}
}
Profiler::mark('alterField');
$this->transAlterField($table, $field, $spec_orig);
Profiler::unmark('alterField');
$this->alterationMessage("Field $table.$field: changed to $specValue <i style=\"color: #AAA\">(from {$fieldValue})</i>","changed");
}
Profiler::unmark('requireField');
}
/**
@ -891,6 +883,14 @@ abstract class SS_Database {
*/
abstract function datetimeDifferenceClause($date1, $date2);
/**
* Can the database override timezone as a connection setting,
* or does it use the system timezone exclusively?
*
* @return Boolean
*/
abstract function supportsTimezoneOverride();
/*
* Does this database support transactions?
*

View File

@ -10,7 +10,7 @@ class GroupedList extends SS_ListDecorator {
/**
* @param string $index
* @return ArrayList
* @return array
*/
public function groupBy($index) {
$result = array();
@ -29,8 +29,11 @@ class GroupedList extends SS_ListDecorator {
}
/**
* Similar to {@link groupBy()}, but returns
* the data in a format which is suitable for usage in templates.
*
* @param string $index
* @param string $children
* @param string $children Name of the control under which children can be iterated on
* @return ArrayList
*/
public function GroupedBy($index, $children = 'Children') {

View File

@ -87,6 +87,10 @@ class MySQLDatabase extends SS_Database {
return true;
}
public function supportsTimezoneOverride() {
return true;
}
/**
* Get the version of MySQL.
* @return string
@ -880,6 +884,9 @@ class MySQLDatabase extends SS_Database {
$list->setPageLEngth($pageLength);
$list->setTotalItems($totalCount);
// The list has already been limited by the query above
$list->setLimitItems(false);
return $list;
}

View File

@ -19,7 +19,7 @@ class SQLMap extends Object implements IteratorAggregate {
* @param SQLQuery $query The query to generate this map. THis isn't executed until it's needed.
*/
public function __construct(SQLQuery $query, $keyField = "ID", $titleField = "Title") {
Deprecation::notice('3.0', 'Use SS_Map or DataList::map() instead.');
Deprecation::notice('3.0', 'Use SS_Map or DataList::map() instead.', Deprecation::SCOPE_CLASS);
if(!$query) {
user_error('SQLMap constructed with null query.', E_USER_ERROR);

View File

@ -423,17 +423,29 @@ class SQLQuery {
*
* @param int|string|array $limit If passed as a string or array, assumes SQL escaped data.
* @param int $offset
*
* @throws InvalidArgumentException
*
* @return SQLQuery This instance
*/
public function setLimit($limit, $offset = 0) {
if((is_numeric($limit) && $limit < 0) || (is_numeric($offset) && $offset < 0)) {
throw new InvalidArgumentException("SQLQuery::setLimit() only takes positive values");
}
if($limit && is_numeric($limit)) {
$this->limit = array(
'start' => $offset,
'limit' => $limit,
);
} else if($limit && is_string($limit)) {
if(strpos($limit, ',') !== false) list($start, $innerLimit) = explode(',', $limit, 2);
else list($innerLimit, $start) = explode(' OFFSET ', strtoupper($limit), 2);
if(strpos($limit, ',') !== false) {
list($start, $innerLimit) = explode(',', $limit, 2);
}
else {
list($innerLimit, $start) = explode(' OFFSET ', strtoupper($limit), 2);
}
$this->limit = array(
'start' => trim($start),
'limit' => trim($innerLimit),

View File

@ -991,7 +991,7 @@ class Versioned extends DataExtension {
*/
static function get_latest_version($class, $id) {
$baseClass = ClassInfo::baseDataClass($class);
$list = DataList::create($class)->where("\"$baseClass\".\"RecordID\" = $id");
$list = DataList::create($baseClass)->where("\"$baseClass\".\"RecordID\" = $id");
$list->dataQuery()->setQueryParam("Versioned.mode", "latest_versions");
return $list->First();
}
@ -1025,12 +1025,15 @@ class Versioned extends DataExtension {
}
/**
* Return the specific version of the given id
* Return the specific version of the given id.
* Caution: The record is retrieved as a DataObject, but saving back modifications
* via write() will create a new version, rather than modifying the existing one.
*
* @return DataObject
*/
static function get_version($class, $id, $version) {
$baseClass = ClassInfo::baseDataClass($class);
$list = DataList::create($class)->where("\"$baseClass\".\"RecordID\" = $id")->where("\"$baseClass\".\"Version\" = " . (int)$version);
$list = DataList::create($baseClass)->where("\"$baseClass\".\"RecordID\" = $id")->where("\"$baseClass\".\"Version\" = " . (int)$version);
$list->dataQuery()->setQueryParam('Versioned.mode', 'all_versions');
return $list->First();
}

View File

@ -1,12 +1,13 @@
<?php
/**
* Class Enum represents an enumeration of a set of strings.
*
* See {@link DropdownField} for a {@link FormField} to select enum values.
*
* @package framework
* @subpackage model
*/
class Enum extends DBField {
class Enum extends StringField {
protected $enum, $default;
@ -15,22 +16,28 @@ class Enum extends DBField {
/**
* Create a new Enum field.
*
* Example usage in {@link DataObject::$db} with comma-separated string notation ('Val1' is default)
* Example usage in {@link DataObject::$db} with comma-separated string
* notation ('Val1' is default)
*
* <code>
* "MyField" => "Enum('Val1, Val2, Val3', 'Val1')"
* </code>
*
* Example usage in in {@link DataObject::$db} with array notation ('Val1' is default)
* Example usage in in {@link DataObject::$db} with array notation
* ('Val1' is default)
*
* <code>
* "MyField" => "Enum(array('Val1', 'Val2', 'Val3'), 'Val1')"
* </code>
*
* @param enum: A string containing a comma separated list of options or an array of Vals.
* @param default The default option, which is either NULL or one of the items in the enumeration.
* @param enum: A string containing a comma separated list of options or an
* array of Vals.
* @param string The default option, which is either NULL or one of the
* items in the enumeration.
*/
function __construct($name = null, $enum = NULL, $default = NULL) {
public function __construct($name = null, $enum = NULL, $default = NULL) {
if($enum) {
if(!is_array($enum)){
if(!is_array($enum)) {
$enum = preg_split("/ *, */", trim($enum));
}
@ -49,19 +56,38 @@ class Enum extends DBField {
$this->default = reset($enum);
}
}
parent::__construct($name);
}
function requireField(){
$parts=Array('datatype'=>'enum', 'enums'=>Convert::raw2sql($this->enum), 'character set'=>'utf8', 'collate'=> 'utf8_general_ci', 'default'=>Convert::raw2sql($this->default), 'table'=>$this->tableName, 'arrayValue'=>$this->arrayValue);
$values=Array('type'=>'enum', 'parts'=>$parts);
/**
* @return void
*/
public function requireField() {
$parts = array(
'datatype' => 'enum',
'enums' => Convert::raw2sql($this->enum),
'character set' => 'utf8',
'collate' => 'utf8_general_ci',
'default' => Convert::raw2sql($this->default),
'table' => $this->tableName,
'arrayValue' => $this->arrayValue
);
$values = array(
'type' => 'enum',
'parts' => $parts
);
DB::requireField($this->tableName, $this->name, $values);
}
}
/**
* Return a dropdown field suitable for editing this field
* Return a dropdown field suitable for editing this field.
*
* @return DropdownField
*/
function formField($title = null, $name = null, $hasEmpty = false, $value = "", $form = null, $emptyString = null) {
public function formField($title = null, $name = null, $hasEmpty = false, $value = "", $form = null, $emptyString = null) {
if(!$title) $title = $this->name;
if(!$name) $name = $this->name;
@ -71,28 +97,32 @@ class Enum extends DBField {
return $field;
}
/**
* @return DropdownField
*/
public function scaffoldFormField($title = null, $params = null) {
return $this->formField($title);
}
function scaffoldSearchField($title = null) {
/**
* @param string
*
* @return DropdownField
*/
public function scaffoldSearchField($title = null) {
$anyText = _t('Enum.ANY', 'Any');
return $this->formField($title, null, false, '', null, "($anyText)");
}
/**
* Return the values of this enum, suitable for insertion into a dropdown field.
* Returns the values of this enum as an array, suitable for insertion into
* a {@link DropdownField}
*
* @param boolean
*
* @return array
*/
function enumValues($hasEmpty = false) {
public function enumValues($hasEmpty = false) {
return ($hasEmpty) ? array_merge(array('' => ''), ArrayLib::valuekey($this->enum)) : ArrayLib::valuekey($this->enum);
}
function Lower() {
return StringField::LowerCase();
}
function Upper() {
return StringField::UpperCase();
}
}

View File

@ -132,6 +132,13 @@ class Oembed {
$oembedUrl = Controller::join_links($endpoint, '?format=json&url=' . rawurlencode($url));
}
// If autodescovery failed the resource might be a direct link to a file
if(!$oembedUrl) {
if(File::get_app_category(File::get_file_extension($url)) == "image") {
return new Oembed_Result($url, $url, $type, $options);
}
}
if($oembedUrl) {
// Inject the options into the Oembed URL.
if($options) {
@ -233,7 +240,20 @@ class Oembed_Result extends ViewableData {
$body = $body->getBody();
$data = json_decode($body, true);
if(!$data) {
// if the response is no valid JSON we might have received a binary stream to an image
$data = array();
$image = @imagecreatefromstring($body);
if($image !== FALSE) {
preg_match("/^(http:\/\/)?([^\/]+)/i", $this->url, $matches);
$protocoll = $matches[1];
$host = $matches[2];
$data['type'] = "photo";
$data['title'] = basename($this->url) . " ($host)";
$data['url'] = $this->url;
$data['provider_url'] = $protocoll.$host;
$data['width'] = imagesx($image);
$data['height'] = imagesy($image);
}
}
// Convert all keys to lowercase

View File

@ -64,65 +64,67 @@ class BBCodeParser extends TextParser {
static function usable_tags() {
return new ArrayList(
new ArrayData(array(
"Title" => _t('BBCodeParser.BOLD', 'Bold Text'),
"Example" => '[b]<b>'._t('BBCodeParser.BOLDEXAMPLE', 'Bold').'</b>[/b]'
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.ITALIC', 'Italic Text'),
"Example" => '[i]<i>'._t('BBCodeParser.ITALICEXAMPLE', 'Italics').'</i>[/i]'
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.UNDERLINE', 'Underlined Text'),
"Example" => '[u]<u>'._t('BBCodeParser.UNDERLINEEXAMPLE', 'Underlined').'</u>[/u]'
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.STRUCK', 'Struck-out Text'),
"Example" => '[s]<s>'._t('BBCodeParser.STRUCKEXAMPLE', 'Struck-out').'</s>[/s]'
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.COLORED', 'Colored text'),
"Example" => '[color=blue]'._t('BBCodeParser.COLOREDEXAMPLE', 'blue text').'[/color]'
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.ALIGNEMENT', 'Alignment'),
"Example" => '[align=right]'._t('BBCodeParser.ALIGNEMENTEXAMPLE', 'right aligned').'[/align]'
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.CODE', 'Code Block'),
"Description" => _t('BBCodeParser.CODEDESCRIPTION', 'Unformatted code block'),
"Example" => '[code]'._t('BBCodeParser.CODEEXAMPLE', 'Code block').'[/code]'
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.EMAILLINK', 'Email link'),
"Description" => _t('BBCodeParser.EMAILLINKDESCRIPTION', 'Create link to an email address'),
"Example" => "[email]you@yoursite.com[/email]"
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.EMAILLINK', 'Email link'),
"Description" => _t('BBCodeParser.EMAILLINKDESCRIPTION', 'Create link to an email address'),
"Example" => "[email=you@yoursite.com]Email[/email]"
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.UNORDERED', 'Unordered list'),
"Description" => _t('BBCodeParser.UNORDEREDDESCRIPTION', 'Unordered list'),
"Example" => '[ulist][*]'._t('BBCodeParser.UNORDEREDEXAMPLE1', 'unordered item 1').'[/ulist]'
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.IMAGE', 'Image'),
"Description" => _t('BBCodeParser.IMAGEDESCRIPTION', 'Show an image in your post'),
"Example" => "[img]http://www.website.com/image.jpg[/img]"
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.LINK', 'Website link'),
"Description" => _t('BBCodeParser.LINKDESCRIPTION', 'Link to another website or URL'),
"Example" => '[url]http://www.website.com/[/url]'
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.LINK', 'Website link'),
"Description" => _t('BBCodeParser.LINKDESCRIPTION', 'Link to another website or URL'),
"Example" => "[url=http://www.website.com/]Website[/url]"
))
array(
new ArrayData(array(
"Title" => _t('BBCodeParser.BOLD', 'Bold Text'),
"Example" => '[b]<b>'._t('BBCodeParser.BOLDEXAMPLE', 'Bold').'</b>[/b]'
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.ITALIC', 'Italic Text'),
"Example" => '[i]<i>'._t('BBCodeParser.ITALICEXAMPLE', 'Italics').'</i>[/i]'
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.UNDERLINE', 'Underlined Text'),
"Example" => '[u]<u>'._t('BBCodeParser.UNDERLINEEXAMPLE', 'Underlined').'</u>[/u]'
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.STRUCK', 'Struck-out Text'),
"Example" => '[s]<s>'._t('BBCodeParser.STRUCKEXAMPLE', 'Struck-out').'</s>[/s]'
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.COLORED', 'Colored text'),
"Example" => '[color=blue]'._t('BBCodeParser.COLOREDEXAMPLE', 'blue text').'[/color]'
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.ALIGNEMENT', 'Alignment'),
"Example" => '[align=right]'._t('BBCodeParser.ALIGNEMENTEXAMPLE', 'right aligned').'[/align]'
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.CODE', 'Code Block'),
"Description" => _t('BBCodeParser.CODEDESCRIPTION', 'Unformatted code block'),
"Example" => '[code]'._t('BBCodeParser.CODEEXAMPLE', 'Code block').'[/code]'
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.EMAILLINK', 'Email link'),
"Description" => _t('BBCodeParser.EMAILLINKDESCRIPTION', 'Create link to an email address'),
"Example" => "[email]you@yoursite.com[/email]"
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.EMAILLINK', 'Email link'),
"Description" => _t('BBCodeParser.EMAILLINKDESCRIPTION', 'Create link to an email address'),
"Example" => "[email=you@yoursite.com]Email[/email]"
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.UNORDERED', 'Unordered list'),
"Description" => _t('BBCodeParser.UNORDEREDDESCRIPTION', 'Unordered list'),
"Example" => '[ulist][*]'._t('BBCodeParser.UNORDEREDEXAMPLE1', 'unordered item 1').'[/ulist]'
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.IMAGE', 'Image'),
"Description" => _t('BBCodeParser.IMAGEDESCRIPTION', 'Show an image in your post'),
"Example" => "[img]http://www.website.com/image.jpg[/img]"
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.LINK', 'Website link'),
"Description" => _t('BBCodeParser.LINKDESCRIPTION', 'Link to another website or URL'),
"Example" => '[url]http://www.website.com/[/url]'
)),
new ArrayData(array(
"Title" => _t('BBCodeParser.LINK', 'Website link'),
"Description" => _t('BBCodeParser.LINKDESCRIPTION', 'Link to another website or URL'),
"Example" => "[url=http://www.website.com/]Website[/url]"
))
)
);
}

View File

@ -29,7 +29,7 @@
*
* <b>Inbuilt Shortcodes</b>
*
* From 2.4 onwards links inserted via the CMS into a content field are in the form ''<a href="[sitetree_link id=n]">''. At runtime this is replaced by a plain link to the page with the ID in question.
* From 2.4 onwards links inserted via the CMS into a content field are in the form ''<a href="[sitetree_link id=n]">'', and from 3.0 the comma is used as a separator instead ''<a href="[sitetree_link,id=n]">''. At runtime this is replaced by a plain link to the page with the ID in question.
*
* <b>Limitations</b>
*

View File

@ -226,7 +226,7 @@ $gf_grid_x: 16px;
border: none;
background: none;
margin: 0 0 0 2px;
padding: 6px 0;
padding: 1px 0;
width: auto;
text-shadow: none;
&.ui-state-hover {

140
scss/debug.scss Normal file
View File

@ -0,0 +1,140 @@
@import "compass/css3";
body {
background-color: #eee;
margin:0;
overflow-x: hidden;
padding:0;
font-family: Helvetica,Arial,sans-serif;
}
.info {
margin:0 0 6px 0;
padding: 18px;
background-color: #003050;
position: relative;
line-height: 24px;
color: #fff;
@include background-image(
linear-gradient(darken(#003050, 5%), #003050 10%, #003050 90%, darken(#003050, 5%))
);
h1 {
margin: 0 0 6px 0;
padding: 0 32px 0 0;
color: #fff;
font-size: 24px;
text-shadow: 0 1px darken(#003050, 5%);
line-height: 30px;
background: url(../admin/images/logo_small.png) no-repeat right 3px;
}
h3 {
color: #7da4be;
font-size: 16px;
line-height: 18px;
font-weight: normal;
}
p {
margin: 0;
font-size: 14px;
color: #fff;
}
a {
color: #fff;
font-weight: bold;
text-decoration: none;
&:hover,
&:active {
color: #fff;
text-decoration: underline;
}
}
}
.header {
margin: 0;
border-bottom: 6px solid #ccdef3;
height: 23px;
background-color: #666673;
padding: 4px 0 2px 6px;
}
.trace,
.build,
.options {
padding:6px 12px;
li {
font-size:14px; margin:6px 0;
}
}
a {
color: #666;
&:hover {
color: #222;
}
&:active {
color: #111;
}
}
p {
margin-bottom: 6px;
}
pre {
margin-bottom: 20px;
background-color: #f5f5f5;
border: 1px solid #eee;
border: 1px solid rgba(0,0,0,.08);
color: #333;
padding: 11px;
overflow: auto;
@include border-radius(4px);
@include box-shadow(inset 0 1px 1px rgba(0,0,0,.05));
span {
color:#999;
}
.error {
color:#f00;
}
}
h2 {
margin: 0 0 12px 0;
}
h3 {
margin: 0 0 6px 0;
color: #333;
font-size: 18px;
line-height: 24px;
}
ul {
margin: 0 0 18px 0;
padding: 0 0 0 18px;
}
.pass {
margin-top:18px; padding:2px 20px 2px 40px; color:#006600; background:#E2F9E3; border: 1px solid #8DD38D;
border-radius:4px;
}
.fail {
margin-top:18px; padding:2px 20px 2px 40px; color:#C80700; background:#FFE9E9;
border:1px solid #C80700; border-radius:4px;
}

View File

@ -230,15 +230,16 @@ class Permission extends DataObject implements TemplateGlobalProvider {
/**
* Get all the 'any' permission codes available to the given member.
* @return array();
*
* @return array
*/
public static function permissions_for_member($memberID) {
$groupList = self::groupList($memberID);
if($groupList) {
$groupCSV = implode(", ", $groupList);
// Raw SQL for efficiency
return array_unique(DB::query("
$allowed = array_unique(DB::query("
SELECT \"Code\"
FROM \"Permission\"
WHERE \"Type\" = " . self::GRANT_PERMISSION . " AND \"GroupID\" IN ($groupCSV)
@ -252,9 +253,16 @@ class Permission extends DataObject implements TemplateGlobalProvider {
WHERE \"GroupID\" IN ($groupCSV)
")->column());
} else {
return array();
$denied = array_unique(DB::query("
SELECT \"Code\"
FROM \"Permission\"
WHERE \"Type\" = " . self::DENY_PERMISSION . " AND \"GroupID\" IN ($groupCSV)
")->column());
return array_diff($allowed, $denied);
}
return array();
}

View File

@ -47,8 +47,10 @@ class RandomGenerator {
if($isWin && class_exists('COM')) {
try {
$comObj = new COM('CAPICOM.Utilities.1');
$e = base64_decode($comObj->GetRandom(64, 0));
return $e;
if(is_callable(array($comObj,'GetRandom'))) {
return base64_decode($comObj->GetRandom(64, 0));
}
} catch (Exception $ex) {
}
}
@ -67,5 +69,4 @@ class RandomGenerator {
function generateHash($algorithm = 'whirlpool') {
return hash($algorithm, $this->generateEntropy());
}
}

View File

@ -147,6 +147,13 @@ class SecurityToken extends Object implements TemplateGlobalProvider {
Session::set($this->getName(), $val);
}
/**
* Reset the token to a new value.
*/
public function reset() {
$this->setValue($this->generate());
}
/**
* Checks for an existing CSRF token in the current users session.
* This check is automatically performed in {@link Form->httpSubmission()}
@ -292,5 +299,4 @@ class NullSecurityToken extends SecurityToken {
function generate() {
return null;
}
}

View File

@ -1,22 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<% base_tag %>
$MetaTags
<% require css('framework/css/debug.css') %>
</head>
<body>
<h1><% if $Title %>$Title<% else %>Welcome to SilverStripe<% end_if %></h1>
<% if $Content %>$Content<% else %>
<p>To get started with the SilverStripe framework:</p>
<ol>
<li>Create a <code>Controller</code> subclass (<a href="http://doc.silverstripe.org/sapphire/en/topics/controller">doc.silverstripe.org/sapphire/en/topics/controller</a>)</li>
<li>Setup the routes to your <code>Controller</code>.</li>
<li>Create a template for your <code>Controller</code> (<a href="http://doc.silverstripe.org/sapphire/en/trunk/reference/templates">doc.silverstripe.org/sapphire/en/trunk/reference/templates</a>)</li>
</ol>
<% end_if %>
<p><em>Generated with the default Controller.ss template.</em></p>
<div class="info">
<h1><% if $Title %>$Title<% else %>Welcome to SilverStripe<% end_if %></h1>
<h3>Generated with the default Controller.ss template</h3>
</div>
<div class="options">
<% if $Content %>$Content<% else %>
<h3>Getting Started</h3>
<p>To get started with the SilverStripe framework:</p>
<ol>
<li>Create a <code>Controller</code> subclass (<a href="http://doc.silverstripe.org/sapphire/en/topics/controller">doc.silverstripe.org/sapphire/en/topics/controller</a>)</li>
<li>Setup the routes.yml f to your <code>Controller</code> (<a href="http://doc.silverstripe.org/framework/en/reference/director#routing">doc.silverstripe.org/framework/en/reference/director#routing</a>).</li>
<li>Create a template for your <code>Controller</code> (<a href="http://doc.silverstripe.org/sapphire/en/reference/templates">doc.silverstripe.org/sapphire/en/reference/templates</a>)</li>
</ol>
<% end_if %>
<h3>Community resources</h3>
<ul>
<li>
<p><a href="http://silverstripe.org/forum">silverstripe.org/forum</a> Discussion forums for the development community.</p>
</li>
<li><p><a href="http://silverstripe.org/irc">silverstripe.org/irc</a> IRC channel for realtime support and discussions.</p></li>
<li><p><a href="http://doc.silverstripe.org">doc.silverstripe.org</a> Searchable developer documentation, how-tos, tutorials, and reference.</p></li>
<li><p><a href="http://api.silverstripe.org">api.silverstripe.org</a> API documentation for PHP classes, methods and properties.</p></li>
<ul>
</div>
</body>
</html>

View File

@ -43,6 +43,20 @@ class RSSFeedTest extends SapphireTest {
$this->assertContains('<description>ItemC AltContent</description>', $content);
}
public function testRenderWithTemplate() {
$rssFeed = new RSSFeed(new ArrayList(), "", "", "");
$rssFeed->setTemplate('RSSFeedTest');
$content = $rssFeed->feedContent();
$this->assertContains('<title>Test Custom Template</title>', $content);
$rssFeed->setTemplate('RSSFeed');
$content = $rssFeed->feedContent();
$this->assertNotContains('<title>Test Custom Template</title>', $content);
}
public function setUp() {
parent::setUp();
Director::setBaseURL('/');

View File

@ -1,7 +1,31 @@
<?php
/**
* @package framework
* @subpackage tests
*/
class RestfulServiceTest extends SapphireTest {
protected $member_unique_identifier_field = '';
function setUp() {
// backup the project unique identifier field
$this->member_unique_identifier_field = Member::get_unique_identifier_field();
Member::set_unique_identifier_field('Email');
parent::setUp();
}
function tearDown() {
parent::tearDown();
// set old member::get_unique_identifier_field value
if ($this->member_unique_identifier_field) {
Member::set_unique_identifier_field($this->member_unique_identifier_field);
}
}
function testSpecialCharacters() {
$service = new RestfulServiceTest_MockRestfulService(Director::absoluteBaseURL());
$url = 'RestfulServiceTest_Controller/';
@ -134,8 +158,16 @@ class RestfulServiceTest extends SapphireTest {
}
class RestfulServiceTest_Controller extends Controller implements TestOnly {
public static $allowed_actions = array(
'index',
'httpErrorWithoutCache',
'httpErrorWithCache'
);
public function init() {
$this->basicAuthEnabled = false;
parent::init();
}

View File

@ -28,7 +28,8 @@ if(!defined('BASE_PATH')) define('BASE_PATH', dirname($frameworkPath));
// Copied from cli-script.php, to enable same behaviour through phpunit runner.
if(isset($_SERVER['argv'][2])) {
$args = array_slice($_SERVER['argv'],2);
$_GET = array();
if(!isset($_GET)) $_GET = array();
if(!isset($_REQUEST)) $_REQUEST = array();
foreach($args as $arg) {
if(strpos($arg,'=') == false) {
$_GET['args'][] = $arg;
@ -38,12 +39,9 @@ if(isset($_SERVER['argv'][2])) {
$_GET = array_merge($_GET, $newItems);
}
}
$_REQUEST = $_GET;
$_REQUEST = array_merge($_REQUEST, $_GET);
}
// Always flush the manifest for phpunit test runs
$_GET['flush'] = 1;
// Connect to database
require_once $frameworkPath . '/core/Core.php';
require_once $frameworkPath . '/tests/FakeController.php';
@ -65,3 +63,11 @@ TestRunner::use_test_manifest();
// Remove the error handler so that PHPUnit can add its own
restore_error_handler();
if(!isset($_GET['flush']) || !$_GET['flush']) {
Debug::message(
"WARNING: Manifest not flushed. " .
"Add flush=1 as an argument to discover new classes or files.\n",
false
);
}

View File

@ -122,16 +122,22 @@ class DirectorTest extends SapphireTest {
public function testMakeRelative() {
$siteUrl = Director::absoluteBaseURL();
$siteUrlNoProtocol = preg_replace('/https?:\/\//', '', $siteUrl);
$this->assertEquals(Director::makeRelative("$siteUrl"), '');
//$this->assertEquals(Director::makeRelative("https://$siteUrlNoProtocol"), '');
$this->assertEquals(Director::makeRelative("https://$siteUrlNoProtocol"), '');
$this->assertEquals(Director::makeRelative("http://$siteUrlNoProtocol"), '');
$this->assertEquals(Director::makeRelative(" $siteUrl/testpage "), 'testpage');
//$this->assertEquals(Director::makeRelative("$siteUrlNoProtocol/testpage"), 'testpage');
$this->assertEquals(Director::makeRelative("$siteUrlNoProtocol/testpage"), 'testpage');
$this->assertEquals(Director::makeRelative('ftp://test.com'), 'ftp://test.com');
$this->assertEquals(Director::makeRelative('http://test.com'), 'http://test.com');
// the below is not a relative URL, test makes no sense
// $this->assertEquals(Director::makeRelative('/relative'), '/relative');
$this->assertEquals(Director::makeRelative('relative'), 'relative');
$this->assertEquals(Director::makeRelative("$siteUrl/?url=http://test.com"), '?url=http://test.com');
$this->assertEquals("test", Director::makeRelative("https://".$siteUrlNoProtocol."/test"));
$this->assertEquals("test", Director::makeRelative("http://".$siteUrlNoProtocol."/test"));
}
/**

View File

@ -242,4 +242,16 @@ class HTTPRequestTest extends SapphireTest {
$req->addHeader('X-Requested-With', 'XMLHttpRequest');
$this->assertTrue($req->isAjax());
}
public function testGetURL() {
$req = new SS_HTTPRequest('GET', '/');
$this->assertEquals('', $req->getURL());
$req = new SS_HTTPRequest('GET', '/assets/somefile.gif');
$this->assertEquals('assets/somefile.gif', $req->getURL());
$req = new SS_HTTPRequest('GET', '/home?test=1');
$this->assertEquals('home?test=1', $req->getURL(true));
$this->assertEquals('home', $req->getURL());
}
}

View File

@ -66,13 +66,40 @@ class DeprecationTest extends SapphireTest {
$this->callThatOriginatesFromFramework();
}
function testMethodNameCalculation() {
$this->assertEquals(DeprecationTest_Deprecation::get_method(), 'DeprecationTest->testMethodNameCalculation');
}
/**
* @expectedException PHPUnit_Framework_Error
* @expectedExceptionMessage DeprecationTest->testScopeMethod is deprecated. Method scope
*/
function testScopeMethod() {
Deprecation::notification_version('2.0.0');
Deprecation::notice('2.0.0', 'Method scope', Deprecation::SCOPE_METHOD);
}
/**
* @expectedException PHPUnit_Framework_Error
* @expectedExceptionMessage DeprecationTest is deprecated. Class scope
*/
function testScopeClass() {
Deprecation::notification_version('2.0.0');
Deprecation::notice('2.0.0', 'Class scope', Deprecation::SCOPE_CLASS);
}
/**
* @expectedException PHPUnit_Framework_Error
* @expectedExceptionMessage Global scope
*/
function testScopeGlobal() {
Deprecation::notification_version('2.0.0');
Deprecation::notice('2.0.0', 'Global scope', Deprecation::SCOPE_GLOBAL);
}
protected function callThatOriginatesFromFramework() {
$this->assertEquals(DeprecationTest_Deprecation::get_module(), FRAMEWORK_DIR);
Deprecation::notice('2.0', 'Deprecation test passed');
}
function testMethodNameCalculation() {
$this->assertEquals(DeprecationTest_Deprecation::get_method(), 'DeprecationTest->testMethodNameCalculation');
}
}

View File

@ -15,7 +15,7 @@ class GridFieldAddExistingAutocompleterTest extends FunctionalTest {
$btns = $parser->getBySelector('.ss-gridfield #action_gridfield_relationfind');
$response = $this->post(
'GridFieldAddExistingAutocompleterTest_Controller/Form/field/testfield/search/Team 2',
'GridFieldAddExistingAutocompleterTest_Controller/Form/field/testfield/search/?gridfield_relationsearch=Team 2',
array(
(string)$btns[0]['name'] => 1
)
@ -26,7 +26,7 @@ class GridFieldAddExistingAutocompleterTest extends FunctionalTest {
$this->assertEquals(array($team2->ID => 'Team 2'), $result);
$response = $this->post(
'GridFieldAddExistingAutocompleterTest_Controller/Form/field/testfield/search/Unknown',
'GridFieldAddExistingAutocompleterTest_Controller/Form/field/testfield/search/?gridfield_relationsearch=Unknown',
array(
(string)$btns[0]['name'] => 1
)

View File

@ -63,8 +63,8 @@ class GridFieldDetailFormTest extends FunctionalTest {
$surname = $parser->getBySelector('#Form_ItemEditForm_Surname');
$this->assertFalse($response->isError());
$this->assertEquals('Joe', (string) $firstName[0]);
$this->assertEquals('Bloggs', (string) $surname[0]);
$this->assertEquals('Jane', (string) $firstName[0]);
$this->assertEquals('Doe', (string) $surname[0]);
}
function testEditForm() {
@ -188,6 +188,8 @@ class GridFieldDetailFormTest_Person extends DataObject implements TestOnly {
'Categories' => 'GridFieldDetailFormTest_Category'
);
static $default_sort = 'FirstName';
function getCMSFields() {
$fields = parent::getCMSFields();
// TODO No longer necessary once FormScaffolder uses GridField
@ -210,6 +212,8 @@ class GridFieldDetailFormTest_PeopleGroup extends DataObject implements TestOnly
'People' => 'GridFieldDetailFormTest_Person'
);
static $default_sort = 'Name';
function getCMSFields() {
$fields = parent::getCMSFields();
// TODO No longer necessary once FormScaffolder uses GridField
@ -232,6 +236,8 @@ class GridFieldDetailFormTest_Category extends DataObject implements TestOnly {
'People' => 'GridFieldDetailFormTest_Person'
);
static $default_sort = 'Name';
function getCMSFields() {
$fields = parent::getCMSFields();
// TODO No longer necessary once FormScaffolder uses GridField

View File

@ -76,7 +76,6 @@ class DBFieldTest extends SapphireTest {
/* Varchar behaviour */
$this->assertEquals($db->prepStringForDB("0"), singleton('Varchar')->prepValueForDB(0));
$this->assertEquals("'0'", singleton('Varchar')->prepValueForDB(0));
$this->assertEquals("null", singleton('Varchar')->prepValueForDB(null));
$this->assertEquals("null", singleton('Varchar')->prepValueForDB(false));
$this->assertEquals("null", singleton('Varchar')->prepValueForDB(''));

31
tests/model/DataListTest.php Executable file → Normal file
View File

@ -14,7 +14,8 @@ class DataListTest extends SapphireTest {
'DataObjectTest_FieldlessSubTable',
'DataObjectTest_ValidatedObject',
'DataObjectTest_Player',
'DataObjectTest_TeamComment'
'DataObjectTest_TeamComment',
'DataObjectTest\NamespacedClass',
);
public function testSubtract(){
@ -64,22 +65,25 @@ class DataListTest extends SapphireTest {
}
function testSql() {
$db = DB::getConn();
$list = DataObjectTest_TeamComment::get();
$expected = 'SELECT DISTINCT "DataObjectTest_TeamComment"."ClassName", "DataObjectTest_TeamComment"."Created", "DataObjectTest_TeamComment"."LastEdited", "DataObjectTest_TeamComment"."Name", "DataObjectTest_TeamComment"."Comment", "DataObjectTest_TeamComment"."TeamID", "DataObjectTest_TeamComment"."ID", CASE WHEN "DataObjectTest_TeamComment"."ClassName" IS NOT NULL THEN "DataObjectTest_TeamComment"."ClassName" ELSE \'DataObjectTest_TeamComment\' END AS "RecordClassName" FROM "DataObjectTest_TeamComment"';
$expected = 'SELECT DISTINCT "DataObjectTest_TeamComment"."ClassName", "DataObjectTest_TeamComment"."Created", "DataObjectTest_TeamComment"."LastEdited", "DataObjectTest_TeamComment"."Name", "DataObjectTest_TeamComment"."Comment", "DataObjectTest_TeamComment"."TeamID", "DataObjectTest_TeamComment"."ID", CASE WHEN "DataObjectTest_TeamComment"."ClassName" IS NOT NULL THEN "DataObjectTest_TeamComment"."ClassName" ELSE '.$db->prepStringForDB('DataObjectTest_TeamComment').' END AS "RecordClassName" FROM "DataObjectTest_TeamComment"';
$this->assertEquals($expected, $list->sql());
}
function testInnerJoin() {
$db = DB::getConn();
$list = DataObjectTest_TeamComment::get();
$list->innerJoin('DataObjectTest_Team', '"DataObjectTest_Team"."ID" = "DataObjectTest_TeamComment"."TeamID"', 'Team');
$expected = 'SELECT DISTINCT "DataObjectTest_TeamComment"."ClassName", "DataObjectTest_TeamComment"."Created", "DataObjectTest_TeamComment"."LastEdited", "DataObjectTest_TeamComment"."Name", "DataObjectTest_TeamComment"."Comment", "DataObjectTest_TeamComment"."TeamID", "DataObjectTest_TeamComment"."ID", CASE WHEN "DataObjectTest_TeamComment"."ClassName" IS NOT NULL THEN "DataObjectTest_TeamComment"."ClassName" ELSE \'DataObjectTest_TeamComment\' END AS "RecordClassName" FROM "DataObjectTest_TeamComment" INNER JOIN "DataObjectTest_Team" AS "Team" ON "DataObjectTest_Team"."ID" = "DataObjectTest_TeamComment"."TeamID"';
$expected = 'SELECT DISTINCT "DataObjectTest_TeamComment"."ClassName", "DataObjectTest_TeamComment"."Created", "DataObjectTest_TeamComment"."LastEdited", "DataObjectTest_TeamComment"."Name", "DataObjectTest_TeamComment"."Comment", "DataObjectTest_TeamComment"."TeamID", "DataObjectTest_TeamComment"."ID", CASE WHEN "DataObjectTest_TeamComment"."ClassName" IS NOT NULL THEN "DataObjectTest_TeamComment"."ClassName" ELSE '.$db->prepStringForDB('DataObjectTest_TeamComment').' END AS "RecordClassName" FROM "DataObjectTest_TeamComment" INNER JOIN "DataObjectTest_Team" AS "Team" ON "DataObjectTest_Team"."ID" = "DataObjectTest_TeamComment"."TeamID"';
$this->assertEquals($expected, $list->sql());
}
function testLeftJoin() {
$db = DB::getConn();
$list = DataObjectTest_TeamComment::get();
$list->leftJoin('DataObjectTest_Team', '"DataObjectTest_Team"."ID" = "DataObjectTest_TeamComment"."TeamID"', 'Team');
$expected = 'SELECT DISTINCT "DataObjectTest_TeamComment"."ClassName", "DataObjectTest_TeamComment"."Created", "DataObjectTest_TeamComment"."LastEdited", "DataObjectTest_TeamComment"."Name", "DataObjectTest_TeamComment"."Comment", "DataObjectTest_TeamComment"."TeamID", "DataObjectTest_TeamComment"."ID", CASE WHEN "DataObjectTest_TeamComment"."ClassName" IS NOT NULL THEN "DataObjectTest_TeamComment"."ClassName" ELSE \'DataObjectTest_TeamComment\' END AS "RecordClassName" FROM "DataObjectTest_TeamComment" LEFT JOIN "DataObjectTest_Team" AS "Team" ON "DataObjectTest_Team"."ID" = "DataObjectTest_TeamComment"."TeamID"';
$expected = 'SELECT DISTINCT "DataObjectTest_TeamComment"."ClassName", "DataObjectTest_TeamComment"."Created", "DataObjectTest_TeamComment"."LastEdited", "DataObjectTest_TeamComment"."Name", "DataObjectTest_TeamComment"."Comment", "DataObjectTest_TeamComment"."TeamID", "DataObjectTest_TeamComment"."ID", CASE WHEN "DataObjectTest_TeamComment"."ClassName" IS NOT NULL THEN "DataObjectTest_TeamComment"."ClassName" ELSE '.$db->prepStringForDB('DataObjectTest_TeamComment').' END AS "RecordClassName" FROM "DataObjectTest_TeamComment" LEFT JOIN "DataObjectTest_Team" AS "Team" ON "DataObjectTest_Team"."ID" = "DataObjectTest_TeamComment"."TeamID"';
$this->assertEquals($expected, $list->sql());
}
@ -398,6 +402,25 @@ class DataListTest extends SapphireTest {
$this->assertEquals('Bob', $list->first()->Name, 'Only comment should be from Bob');
}
public function testFilterAndExcludeById() {
$id = $this->idFromFixture('DataObjectTest_SubTeam', 'subteam1');
$list = DataObjectTest_SubTeam::get()->filter('ID', $id);
$this->assertEquals($id, $list->first()->ID);
$list = DataObjectTest_SubTeam::get();
$this->assertEquals(3, count($list));
$this->assertEquals(2, count($list->exclude('ID', $id)));
// Check that classes with namespaces work.
$obj = new DataObjectTest\NamespacedClass();
$obj->Name = "Test";
$obj->write();
$list = DataObjectTest\NamespacedClass::get()->filter('ID', $obj->ID);
$this->assertEquals('Test', $list->First()->Name);
$this->assertEquals(0, $list->exclude('ID', $obj->ID)->count());
}
/**
* $list->exclude('Name', 'bob'); // exclude bob from list
*/

View File

@ -22,49 +22,53 @@ class DataObjectLazyLoadingTest extends SapphireTest {
);
function testQueriedColumnsID() {
$db = DB::getConn();
$playerList = new DataList('DataObjectTest_SubTeam');
$playerList = $playerList->setQueriedColumns(array('ID'));
$expected = 'SELECT DISTINCT "DataObjectTest_Team"."ClassName", "DataObjectTest_Team"."Created", ' .
'"DataObjectTest_Team"."LastEdited", "DataObjectTest_Team"."ID", CASE WHEN '.
'"DataObjectTest_Team"."ClassName" IS NOT NULL THEN "DataObjectTest_Team"."ClassName" ELSE ' .
'\'DataObjectTest_Team\' END AS "RecordClassName" FROM "DataObjectTest_Team" WHERE ' .
'("DataObjectTest_Team"."ClassName" IN (\'DataObjectTest_SubTeam\'))';
$db->prepStringForDB('DataObjectTest_Team').' END AS "RecordClassName" FROM "DataObjectTest_Team" WHERE ' .
'("DataObjectTest_Team"."ClassName" IN ('.$db->prepStringForDB('DataObjectTest_SubTeam').'))';
$this->assertEquals($expected, $playerList->sql());
}
function testQueriedColumnsFromBaseTableAndSubTable() {
$db = DB::getConn();
$playerList = new DataList('DataObjectTest_SubTeam');
$playerList = $playerList->setQueriedColumns(array('Title', 'SubclassDatabaseField'));
$expected = 'SELECT DISTINCT "DataObjectTest_Team"."ClassName", "DataObjectTest_Team"."Created", ' .
'"DataObjectTest_Team"."LastEdited", "DataObjectTest_Team"."Title", ' .
'"DataObjectTest_SubTeam"."SubclassDatabaseField", "DataObjectTest_Team"."ID", CASE WHEN ' .
'"DataObjectTest_Team"."ClassName" IS NOT NULL THEN "DataObjectTest_Team"."ClassName" ELSE ' .
'\'DataObjectTest_Team\' END AS "RecordClassName" FROM "DataObjectTest_Team" LEFT JOIN ' .
$db->prepStringForDB('DataObjectTest_Team').' END AS "RecordClassName" FROM "DataObjectTest_Team" LEFT JOIN ' .
'"DataObjectTest_SubTeam" ON "DataObjectTest_SubTeam"."ID" = "DataObjectTest_Team"."ID" WHERE ' .
'("DataObjectTest_Team"."ClassName" IN (\'DataObjectTest_SubTeam\'))';
'("DataObjectTest_Team"."ClassName" IN ('.$db->prepStringForDB('DataObjectTest_SubTeam').'))';
$this->assertEquals($expected, $playerList->sql());
}
function testQueriedColumnsFromBaseTable() {
$db = DB::getConn();
$playerList = new DataList('DataObjectTest_SubTeam');
$playerList = $playerList->setQueriedColumns(array('Title'));
$expected = 'SELECT DISTINCT "DataObjectTest_Team"."ClassName", "DataObjectTest_Team"."Created", ' .
'"DataObjectTest_Team"."LastEdited", "DataObjectTest_Team"."Title", "DataObjectTest_Team"."ID", ' .
'CASE WHEN "DataObjectTest_Team"."ClassName" IS NOT NULL THEN "DataObjectTest_Team"."ClassName" ELSE ' .
'\'DataObjectTest_Team\' END AS "RecordClassName" FROM "DataObjectTest_Team" WHERE ' .
'("DataObjectTest_Team"."ClassName" IN (\'DataObjectTest_SubTeam\'))';
$db->prepStringForDB('DataObjectTest_Team').' END AS "RecordClassName" FROM "DataObjectTest_Team" WHERE ' .
'("DataObjectTest_Team"."ClassName" IN ('.$db->prepStringForDB('DataObjectTest_SubTeam').'))';
$this->assertEquals($expected, $playerList->sql());
}
function testQueriedColumnsFromSubTable() {
$db = DB::getConn();
$playerList = new DataList('DataObjectTest_SubTeam');
$playerList = $playerList->setQueriedColumns(array('SubclassDatabaseField'));
$expected = 'SELECT DISTINCT "DataObjectTest_Team"."ClassName", "DataObjectTest_Team"."Created", ' .
'"DataObjectTest_Team"."LastEdited", "DataObjectTest_SubTeam"."SubclassDatabaseField", ' .
'"DataObjectTest_Team"."ID", CASE WHEN "DataObjectTest_Team"."ClassName" IS NOT NULL THEN ' .
'"DataObjectTest_Team"."ClassName" ELSE \'DataObjectTest_Team\' END AS "RecordClassName" FROM ' .
'"DataObjectTest_Team"."ClassName" ELSE '.$db->prepStringForDB('DataObjectTest_Team').' END AS "RecordClassName" FROM ' .
'"DataObjectTest_Team" LEFT JOIN "DataObjectTest_SubTeam" ON "DataObjectTest_SubTeam"."ID" = ' .
'"DataObjectTest_Team"."ID" WHERE ("DataObjectTest_Team"."ClassName" IN (\'DataObjectTest_SubTeam\'))';
'"DataObjectTest_Team"."ID" WHERE ("DataObjectTest_Team"."ClassName" IN ('.$db->prepStringForDB('DataObjectTest_SubTeam').'))';
$this->assertEquals($expected, $playerList->sql());
}

View File

@ -0,0 +1,13 @@
<?php
namespace DataObjectTest;
/**
* Right now this is only used in DataListTest, but extending it to DataObjectTest in the future would make sense.
* Note that it was deliberated named to include "\N" to try and trip bad code up.
*/
class NamespacedClass extends \DataObject {
static $db = array(
'Name' => 'Varchar',
);
}

View File

@ -13,5 +13,53 @@ class DataQueryTest extends SapphireTest {
$dq->leftJoin("Group_Members", "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"");
$this->assertContains("LEFT JOIN \"Group_Members\" ON \"Group_Members\".\"MemberID\" = \"Member\".\"ID\"", $dq->sql());
}
function testRelationReturn() {
$dq = new DataQuery('DataQueryTest_C');
$this->assertEquals('DataQueryTest_A', $dq->applyRelation('TestA'), 'DataQuery::applyRelation should return the name of the related object.');
$this->assertEquals('DataQueryTest_A', $dq->applyRelation('TestAs'), 'DataQuery::applyRelation should return the name of the related object.');
$this->assertEquals('DataQueryTest_A', $dq->applyRelation('ManyTestAs'), 'DataQuery::applyRelation should return the name of the related object.');
$this->assertEquals('DataQueryTest_B', $dq->applyRelation('TestB'), 'DataQuery::applyRelation should return the name of the related object.');
$this->assertEquals('DataQueryTest_B', $dq->applyRelation('TestBs'), 'DataQuery::applyRelation should return the name of the related object.');
$this->assertEquals('DataQueryTest_B', $dq->applyRelation('ManyTestBs'), 'DataQuery::applyRelation should return the name of the related object.');
}
}
class DataQueryTest_A extends DataObject implements TestOnly {
public static $db = array(
'Name' => 'Varchar',
);
public static $has_one = array(
'TestC' => 'DataQueryTest_C',
);
}
class DataQueryTest_B extends DataQueryTest_A {
public static $db = array(
'Title' => 'Varchar',
);
public static $has_one = array(
'TestC' => 'DataQueryTest_C',
);
}
class DataQueryTest_C extends DataObject implements TestOnly {
public static $has_one = array(
'TestA' => 'DataQueryTest_A',
'TestB' => 'DataQueryTest_B',
);
public static $has_many = array(
'TestAs' => 'DataQueryTest_A',
'TestBs' => 'DataQueryTest_B',
);
public static $many_many = array(
'ManyTestAs' => 'DataQueryTest_A',
'ManyTestBs' => 'DataQueryTest_B',
);
}

View File

@ -6,20 +6,16 @@ class DbDatetimeTest extends FunctionalTest {
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
E_USER_ERROR => 1800,
E_USER_NOTICE => 5,
);
protected $offset;
private $adapter;
protected $adapter;
/**
* Check if dates match more or less. This takes into the account the db query
* can overflow to the next second giving offset readings.
*/
private function matchesRoughly($date1, $date2, $comment = '') {
$allowedDifference = 5 + abs(self::$offset); // seconds
private function matchesRoughly($date1, $date2, $comment = '', $offset) {
$allowedDifference = 5 + abs($offset); // seconds
$time1 = is_numeric($date1) ? $date1 : strtotime($date1);
$time2 = is_numeric($date2) ? $date2 : strtotime($date2);
@ -32,103 +28,103 @@ class DbDatetimeTest extends FunctionalTest {
return DB::query($query)->value();
}
function setUpOnce() {
parent::setUpOnce();
/**
* Needs to be run within a test*() context.
*
* @return Int Offset in seconds
*/
private function checkPreconditions() {
// number of seconds of php and db time are out of sync
$offset = time() - strtotime(DB::query('SELECT ' . DB::getConn()->now())->value());
$threshold = 5; // seconds
self::$offset = time() - strtotime(DB::query('SELECT ' . DB::getConn()->now())->value());
foreach(self::$offset_thresholds as $code => $offset) {
if(abs(self::$offset) > $offset) {
if($code == E_USER_NOTICE) {
Debug::show('The time of the database is out of sync with the webserver by ' . abs(self::$offset) . ' seconds.');
} else {
trigger_error('The time of the database is out of sync with the webserver by ' . abs(self::$offset) . ' seconds.', $code);
}
break;
}
if($offset > 5) {
$this->markTestSkipped('The time of the database is out of sync with the webserver by ' . abs($offset) . ' seconds.');
}
if(method_exists($this->adapter, 'supportsTimezoneOverride') && !$this->adapter->supportsTimezoneOverride()) {
$this->markTestSkipped("Database doesn't support timezone overrides");
}
return $offset;
}
function setUp() {
parent::setUp();
$this->adapter = DB::getConn();
$this->supportDbDatetime = method_exists($this->adapter, 'datetimeIntervalClause');
}
function testCorrectNow() {
if($this->supportDbDatetime) {
$clause = $this->adapter->formattedDatetimeClause('now', '%U');
$result = DB::query('SELECT ' . $clause)->value();
$this->assertRegExp('/^\d*$/', (string) $result);
$this->assertTrue($result>0);
}
$offset = $this->checkPreconditions();
$clause = $this->adapter->formattedDatetimeClause('now', '%U');
$result = DB::query('SELECT ' . $clause)->value();
$this->assertRegExp('/^\d*$/', (string) $result);
$this->assertTrue($result>0);
}
function testDbDatetimeFormat() {
if($this->supportDbDatetime) {
$clause = $this->adapter->formattedDatetimeClause('1973-10-14 10:30:00', '%H:%i, %d/%m/%Y');
$result = DB::query('SELECT ' . $clause)->value();
$this->matchesRoughly($result, date('H:i, d/m/Y', strtotime('1973-10-14 10:30:00')), 'nice literal time');
$offset = $this->checkPreconditions();
$clause = $this->adapter->formattedDatetimeClause('now', '%d');
$result = DB::query('SELECT ' . $clause)->value();
$this->matchesRoughly($result, date('d', $this->getDbNow()), 'todays day');
$clause = $this->adapter->formattedDatetimeClause('1973-10-14 10:30:00', '%H:%i, %d/%m/%Y');
$result = DB::query('SELECT ' . $clause)->value();
$this->matchesRoughly($result, date('H:i, d/m/Y', strtotime('1973-10-14 10:30:00')), 'nice literal time', $offset);
$clause = $this->adapter->formattedDatetimeClause('"Created"', '%U') . ' AS test FROM "DbDateTimeTest_Team"';
$result = DB::query('SELECT ' . $clause)->value();
$this->matchesRoughly($result, strtotime(DataObject::get_one('DbDateTimeTest_Team')->Created), 'fixture ->Created as timestamp');
}
$clause = $this->adapter->formattedDatetimeClause('now', '%d');
$result = DB::query('SELECT ' . $clause)->value();
$this->matchesRoughly($result, date('d', $this->getDbNow()), 'todays day', $offset);
$clause = $this->adapter->formattedDatetimeClause('"Created"', '%U') . ' AS test FROM "DbDateTimeTest_Team"';
$result = DB::query('SELECT ' . $clause)->value();
$this->matchesRoughly($result, strtotime(DataObject::get_one('DbDateTimeTest_Team')->Created), 'fixture ->Created as timestamp', $offset);
}
function testDbDatetimeInterval() {
if($this->supportDbDatetime) {
$offset = $this->checkPreconditions();
$clause = $this->adapter->datetimeIntervalClause('1973-10-14 10:30:00', '+18 Years');
$result = DB::query('SELECT ' . $clause)->value();
$this->matchesRoughly($result, '1991-10-14 10:30:00', 'add 18 years');
$clause = $this->adapter->datetimeIntervalClause('1973-10-14 10:30:00', '+18 Years');
$result = DB::query('SELECT ' . $clause)->value();
$this->matchesRoughly($result, '1991-10-14 10:30:00', 'add 18 years', $offset);
$clause = $this->adapter->datetimeIntervalClause('now', '+1 Day');
$result = DB::query('SELECT ' . $clause)->value();
$this->matchesRoughly($result, date('Y-m-d H:i:s', strtotime('+1 Day', $this->getDbNow())), 'tomorrow');
$clause = $this->adapter->datetimeIntervalClause('now', '+1 Day');
$result = DB::query('SELECT ' . $clause)->value();
$this->matchesRoughly($result, date('Y-m-d H:i:s', strtotime('+1 Day', $this->getDbNow())), 'tomorrow', $offset);
$query = new SQLQuery();
$query->setSelect(array());
$query->selectField($this->adapter->datetimeIntervalClause('"Created"', '-15 Minutes'), 'test')
->setFrom('"DbDateTimeTest_Team"')
->setLimit(1);
$query = new SQLQuery();
$query->setSelect(array());
$query->selectField($this->adapter->datetimeIntervalClause('"Created"', '-15 Minutes'), 'test')
->setFrom('"DbDateTimeTest_Team"')
->setLimit(1);
$result = $query->execute()->value();
$this->matchesRoughly($result, date('Y-m-d H:i:s', strtotime(DataObject::get_one('DbDateTimeTest_Team')->Created) - 900), '15 Minutes before creating fixture');
}
$result = $query->execute()->value();
$this->matchesRoughly($result, date('Y-m-d H:i:s', strtotime(DataObject::get_one('DbDateTimeTest_Team')->Created) - 900), '15 Minutes before creating fixture', $offset);
}
function testDbDatetimeDifference() {
if($this->supportDbDatetime) {
$offset = $this->checkPreconditions();
$clause = $this->adapter->datetimeDifferenceClause('1974-10-14 10:30:00', '1973-10-14 10:30:00');
$result = DB::query('SELECT ' . $clause)->value();
$this->matchesRoughly($result/86400, 365, '1974 - 1973 = 365 * 86400 sec');
$clause = $this->adapter->datetimeDifferenceClause('1974-10-14 10:30:00', '1973-10-14 10:30:00');
$result = DB::query('SELECT ' . $clause)->value();
$this->matchesRoughly($result/86400, 365, '1974 - 1973 = 365 * 86400 sec', $offset);
$clause = $this->adapter->datetimeDifferenceClause(date('Y-m-d H:i:s', strtotime('-15 seconds')), 'now');
$result = DB::query('SELECT ' . $clause)->value();
$this->matchesRoughly($result, -15, '15 seconds ago - now');
$clause = $this->adapter->datetimeDifferenceClause(date('Y-m-d H:i:s', strtotime('-15 seconds')), 'now');
$result = DB::query('SELECT ' . $clause)->value();
$this->matchesRoughly($result, -15, '15 seconds ago - now', $offset);
$clause = $this->adapter->datetimeDifferenceClause('now', $this->adapter->datetimeIntervalClause('now', '+45 Minutes'));
$result = DB::query('SELECT ' . $clause)->value();
$this->matchesRoughly($result, -45 * 60, 'now - 45 minutes ahead');
$clause = $this->adapter->datetimeDifferenceClause('now', $this->adapter->datetimeIntervalClause('now', '+45 Minutes'));
$result = DB::query('SELECT ' . $clause)->value();
$this->matchesRoughly($result, -45 * 60, 'now - 45 minutes ahead', $offset);
$query = new SQLQuery();
$query->setSelect(array());
$query->selectField($this->adapter->datetimeDifferenceClause('"LastEdited"', '"Created"'), 'test')
->setFrom('"DbDateTimeTest_Team"')
->setLimit(1);
$query = new SQLQuery();
$query->setSelect(array());
$query->selectField($this->adapter->datetimeDifferenceClause('"LastEdited"', '"Created"'), 'test')
->setFrom('"DbDateTimeTest_Team"')
->setLimit(1);
$result = $query->execute()->value();
$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');
}
$result = $query->execute()->value();
$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', $offset);
}
}

View File

@ -7,6 +7,14 @@
*/
class PaginatedListTest extends SapphireTest {
static $fixture_file = 'DataObjectTest.yml';
protected $extraDataObjects = array(
'DataObjectTest_Team',
'DataObjectTest_SubTeam',
'DataObjectTest_Player'
);
public function testPageStart() {
$list = new PaginatedList(new ArrayList());
$this->assertEquals(0, $list->getPageStart(), 'The start defaults to 0.');
@ -84,6 +92,12 @@ class PaginatedListTest extends SapphireTest {
$list->setCurrentPage(999);
$this->assertDOSEquals(array(), $list->getIterator());
$players = DataObjectTest_Player::get();
$list = new PaginatedList($players);
$list->setPageLength(1);
$list->getIterator();
$this->assertEquals(4, $list->getTotalItems(), 'Getting an iterator should not trim the list to the page length.');
}
public function testPages() {

View File

@ -129,6 +129,30 @@ class SQLQueryTest extends SapphireTest {
$this->assertEquals('SELECT *, RAND() AS "_SortColumn0" FROM MyTable ORDER BY "_SortColumn0" ASC', $query->sql());
}
/**
* @expectedException InvalidArgumentException
*/
public function testNegativeLimit() {
$query = new SQLQuery();
$query->setLimit(-10);
}
/**
* @expectedException InvalidArgumentException
*/
public function testNegativeOffset() {
$query = new SQLQuery();
$query->setLimit(1, -10);
}
/**
* @expectedException InvalidArgumentException
*/
public function testNegativeOffsetAndLimit() {
$query = new SQLQuery();
$query->setLimit(-10, -10);
}
public function testReverseOrderBy() {
$query = new SQLQuery();
$query->setFrom('MyTable');

View File

@ -242,6 +242,30 @@ class VersionedTest extends SapphireTest {
'VersionedTest_Subclass_Live',
), DataObject::get('VersionedTest_Subclass')->dataQuery()->query()->queriedTables());
}
public function testGetVersionWhenClassnameChanged() {
$obj = new VersionedTest_DataObject;
$obj->Name = "test";
$obj->write();
$obj->Name = "test2";
$obj->ClassName = "VersionedTest_Subclass";
$obj->write();
$subclassVersion = $obj->Version;
$obj->Name = "test3";
$obj->ClassName = "VersionedTest_DataObject";
$obj->write();
// We should be able to pass the subclass and still get the correct class back
$obj2 = Versioned::get_version("VersionedTest_Subclass", $obj->ID, $subclassVersion);
$this->assertInstanceOf("VersionedTest_Subclass", $obj2);
$this->assertEquals("test2", $obj2->Name);
$obj3 = Versioned::get_latest_version("VersionedTest_Subclass", $obj->ID);
$this->assertEquals("test3", $obj3->Name);
$this->assertInstanceOf("VersionedTest_DataObject", $obj3);
}
}
class VersionedTest_DataObject extends DataObject implements TestOnly {

View File

@ -1,6 +1,11 @@
<?php
/**
* @package framework
* @subpackage tests
*/
class PermissionTest extends SapphireTest {
static $fixture_file = 'PermissionTest.yml';
function testGetCodesGrouped() {
@ -34,6 +39,23 @@ class PermissionTest extends SapphireTest {
$this->assertFalse(Permission::checkMember($member, "SITETREE_VIEW_ALL"));
}
function testPermissionsForMember() {
$member = $this->objFromFixture('Member', 'access');
$permissions = Permission::permissions_for_member($member->ID);
$this->assertEquals(4, count($permissions));
$this->assertTrue(in_array('CMS_ACCESS_MyAdmin', $permissions));
$this->assertTrue(in_array('CMS_ACCESS_AssetAdmin', $permissions));
$this->assertTrue(in_array('CMS_ACCESS_SecurityAdmin', $permissions));
$this->assertTrue(in_array('EDIT_PERMISSIONS', $permissions));
$group = $this->objFromFixture("Group", "access");
Permission::deny($group->ID, "CMS_ACCESS_MyAdmin");
$permissions = Permission::permissions_for_member($member->ID);
$this->assertEquals(3, count($permissions));
$this->assertFalse(in_array('CMS_ACCESS_MyAdmin', $permissions));
}
function testRolesAndPermissionsFromParentGroupsAreInherited() {
$member = $this->objFromFixture('Member', 'globalauthor');

View File

@ -66,6 +66,14 @@ class SecurityTokenTest extends SapphireTest {
$this->assertTrue($t->check('mytoken'), 'Valid token returns true');
}
function testReset() {
$t = new SecurityToken();
$initialValue = $t->getValue();
$t->reset();
$this->assertNotEquals($t->getValue(), $initialValue);
}
function testCheckRequest() {
$t = new SecurityToken();
$n = $t->getName();

View File

@ -0,0 +1,6 @@
<?xml version="1.0"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Test Custom Template</title>
</channel>
</rss>

View File

@ -13,12 +13,22 @@ class YamlFixtureTest extends SapphireTest {
$absPath = FRAMEWORK_PATH . '/tests/testing/YamlFixtureTest.yml';
$obj = new YamlFixture($absPath);
$this->assertEquals($absPath, $obj->getFixtureFile());
$this->assertNull($obj->getFixtureString());
}
function testRelativeFixturePath() {
$relPath = FRAMEWORK_DIR . '/tests/testing/YamlFixtureTest.yml';
$obj = new YamlFixture($relPath);
$this->assertEquals(Director::baseFolder() . '/' . $relPath, $obj->getFixtureFile());
$this->assertNull($obj->getFixtureString());
}
function testStringFixture() {
$absPath = FRAMEWORK_PATH . '/tests/testing/YamlFixtureTest.yml';
$string = file_get_contents($absPath);
$obj = new YamlFixture($string);
$this->assertEquals($string, $obj->getFixtureString());
$this->assertNull($obj->getFixtureFile());
}
/**

View File

@ -6,7 +6,7 @@ if (!defined('PHPUnit_MAIN_METHOD')) {
/**
* Translate_Adapter_RailsYAML
*/
require_once 'Translate/Adapter/RailsYAML.php';
require_once dirname(__FILE__) . '/../../../library/Translate/Adapter/RailsYAML.php';
/**
* @category Zend

View File

@ -646,8 +646,6 @@ class Requirements_Backend {
* @return string HTML content thats augumented with the requirements before the closing <head> tag.
*/
function includeInHTML($templateFile, $content) {
if(isset($_GET['debug_profile'])) Profiler::mark("Requirements::includeInHTML");
if((strpos($content, '</head>') !== false || strpos($content, '</head ') !== false) && ($this->css || $this->javascript || $this->customCSS || $this->customScript || $this->customHeadTags)) {
$requirements = '';
$jsRequirements = '';
@ -695,9 +693,10 @@ class Requirements_Backend {
// We put script tags into the body, for performance.
// If your template already has script tags in the body, then we put our script
// tags just before those. Otherwise, we put it at the bottom.
$p1 = strripos($content, '<script');
$p2 = stripos($content, '<body');
if($p1 !== false && $p1 > $p2) {
$p1 = stripos($content, '<script', $p2);
if($p1 !== false) {
$content = substr($content,0,$p1) . $jsRequirements . substr($content,$p1);
} else {
$content = preg_replace("/(<\/body[^>]*>)/i", $jsRequirements . "\\1", $content);
@ -711,8 +710,6 @@ class Requirements_Backend {
}
}
if(isset($_GET['debug_profile'])) Profiler::unmark("Requirements::includeInHTML");
return $content;
}
@ -945,7 +942,7 @@ class Requirements_Backend {
if(class_exists('SapphireTest', false)) $runningTest = SapphireTest::is_running_test();
else $runningTest = false;
if((Director::isDev() && !$runningTest) || !$this->combined_files_enabled) {
if((Director::isDev() && !$runningTest && !isset($_REQUEST['combine'])) || !$this->combined_files_enabled) {
return;
}

View File

@ -810,22 +810,16 @@ class SSViewer {
$template = $this->chosenTemplates[$key];
}
if(isset($_GET['debug_profile'])) Profiler::mark("SSViewer::process", " for $template");
$cacheFile = TEMP_FOLDER . "/.cache" . str_replace(array('\\','/',':'), '.', Director::makeRelative(realpath($template)));
$lastEdited = filemtime($template);
if(!file_exists($cacheFile) || filemtime($cacheFile) < $lastEdited || isset($_GET['flush'])) {
if(isset($_GET['debug_profile'])) Profiler::mark("SSViewer::process - compile", " for $template");
$content = file_get_contents($template);
$content = SSViewer::parseTemplateContent($content, $template);
$fh = fopen($cacheFile,'w');
fwrite($fh, $content);
fclose($fh);
if(isset($_GET['debug_profile'])) Profiler::unmark("SSViewer::process - compile", " for $template");
}
$underlay = array('I18NNamespace' => basename($template));
@ -846,8 +840,6 @@ class SSViewer {
array_pop(SSViewer::$topLevel);
if(isset($_GET['debug_profile'])) Profiler::unmark("SSViewer::process", " for $template");
// If we have our crazy base tag, then fix # links referencing the current page.
if($this->rewriteHashlinks && self::$options['rewriteHashlinks']) {
if(strpos($output, '<base') !== false) {

View File

@ -351,10 +351,6 @@ class ViewableData extends Object implements IteratorAggregate {
* @param string $cacheName a custom cache name
*/
public function obj($fieldName, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) {
if(isset($_REQUEST['debug_profile'])) {
Profiler::mark("obj.$fieldName", "on a $this->class object");
}
if(!$cacheName) $cacheName = $arguments ? $fieldName . implode(',', $arguments) : $fieldName;
if(!isset($this->objCache[$cacheName])) {
@ -384,10 +380,6 @@ class ViewableData extends Object implements IteratorAggregate {
$value = $this->objCache[$cacheName];
}
if(isset($_REQUEST['debug_profile'])) {
Profiler::unmark("obj.$fieldName", "on a $this->class object");
}
if(!is_object($value) && $forceReturnedObject) {
$default = Config::inst()->get('ViewableData', 'default_cast', Config::FIRST_SET);
$value = new $default($fieldName);