Merge pull request #5943 from open-sausages/pulls/4.0/template-lookup-redux

API Update template lookup to late resolution for performance reasons
This commit is contained in:
Ingo Schommer 2016-09-06 20:27:11 +12:00 committed by GitHub
commit 3b71b7731a
99 changed files with 322 additions and 187 deletions

View File

@ -15,6 +15,7 @@ use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\Queries\SQLSelect; use SilverStripe\ORM\Queries\SQLSelect;
use SilverStripe\ORM\ManyManyList; use SilverStripe\ORM\ManyManyList;
use SilverStripe\MSSQL\MSSQLDatabase; use SilverStripe\MSSQL\MSSQLDatabase;
use SSViewer;
use TemplateGlobalProvider; use TemplateGlobalProvider;
use Deprecation; use Deprecation;
use i18n; use i18n;
@ -1541,8 +1542,10 @@ class Member extends DataObject implements TemplateGlobalProvider {
$dateFormatMap $dateFormatMap
) )
); );
$formatClass = get_class($dateFormatField);
$dateFormatField->setValue($self->DateFormat); $dateFormatField->setValue($self->DateFormat);
$dateFormatField->setDescriptionTemplate('forms/MemberDatetimeOptionsetField_description_date'); $dateTemplate = SSViewer::get_templates_by_class($formatClass, '_description_date', $formatClass);
$dateFormatField->setDescriptionTemplate($dateTemplate);
$defaultTimeFormat = Zend_Locale_Format::getTimeFormat(new Zend_Locale($self->Locale)); $defaultTimeFormat = Zend_Locale_Format::getTimeFormat(new Zend_Locale($self->Locale));
$timeFormatMap = array( $timeFormatMap = array(
@ -1559,7 +1562,8 @@ class Member extends DataObject implements TemplateGlobalProvider {
) )
); );
$timeFormatField->setValue($self->TimeFormat); $timeFormatField->setValue($self->TimeFormat);
$timeFormatField->setDescriptionTemplate('forms/MemberDatetimeOptionsetField_description_time'); $timeTemplate = SSViewer::get_templates_by_class($formatClass,'_description_time', $formatClass);
$timeFormatField->setDescriptionTemplate($timeTemplate);
}); });
return parent::getCMSFields(); return parent::getCMSFields();

View File

@ -972,7 +972,8 @@ class LeftAndMain extends Controller implements PermissionProvider {
* @return array * @return array
*/ */
public function getTemplatesWithSuffix($suffix) { public function getTemplatesWithSuffix($suffix) {
return SSViewer::get_templates_by_class(get_class($this), $suffix, 'SilverStripe\\Admin\\LeftAndMain'); $templates = SSViewer::get_templates_by_class(get_class($this), $suffix, __CLASS__);
return SSViewer::chooseTemplate($templates);
} }
public function Content() { public function Content() {

View File

@ -165,7 +165,7 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider {
// Add import capabilities. Limit to admin since the import logic can affect assigned permissions // Add import capabilities. Limit to admin since the import logic can affect assigned permissions
if(Permission::check('ADMIN')) { if(Permission::check('ADMIN')) {
$fields->addFieldsToTab('Root.Users', array( $fields->addFieldsToTab('Root.Users', array(
new HeaderField(_t('SecurityAdmin.IMPORTUSERS', 'Import users'), 3), new HeaderField('ImportUsersHeader', _t('SecurityAdmin.IMPORTUSERS', 'Import users'), 3),
new LiteralField( new LiteralField(
'MemberImportFormIframe', 'MemberImportFormIframe',
sprintf( sprintf(
@ -176,7 +176,7 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider {
) )
)); ));
$fields->addFieldsToTab('Root.Groups', array( $fields->addFieldsToTab('Root.Groups', array(
new HeaderField(_t('SecurityAdmin.IMPORTGROUPS', 'Import groups'), 3), new HeaderField('ImportGroupsHeader', _t('SecurityAdmin.IMPORTGROUPS', 'Import groups'), 3),
new LiteralField( new LiteralField(
'GroupImportFormIframe', 'GroupImportFormIframe',
sprintf( sprintf(

View File

@ -1,12 +1,10 @@
<?php <?php
use SilverStripe\ORM\SS_List; use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\FieldType\DBHTMLText;
/** /**
* RSSFeed class * RSSFeed class
* *
@ -91,9 +89,11 @@ class RSSFeed extends ViewableData {
protected $etag; protected $etag;
/** /**
* Custom template
*
* @var string * @var string
*/ */
protected $template = 'RSSFeed'; protected $template = null;
/** /**
* Constructor * Constructor
@ -219,7 +219,7 @@ class RSSFeed extends ViewableData {
Config::inst()->update('SSViewer', 'source_file_comments', $prevState); Config::inst()->update('SSViewer', 'source_file_comments', $prevState);
return $this->renderWith($this->getTemplate()); return $this->renderWith($this->getTemplates());
} }
/** /**
@ -240,6 +240,21 @@ class RSSFeed extends ViewableData {
public function getTemplate() { public function getTemplate() {
return $this->template; return $this->template;
} }
/**
* Returns the ordered list of preferred templates for rendering this object.
* Will prioritise any custom template first, and then templates based on class hiearchy next.
*
* @return array
*/
public function getTemplates() {
$templates = SSViewer::get_templates_by_class(get_class($this), '', __CLASS__);
// Prefer any custom template
if($this->getTemplate()) {
array_unshift($templates, $this->getTemplate());
}
return $templates;
}
} }
/** /**

View File

@ -87,15 +87,17 @@ class ClassInfo {
* *
* @todo Move this into {@see DataObjectSchema} * @todo Move this into {@see DataObjectSchema}
* *
* @param string|object $class * @param string|object $nameOrObject Class or object instance
* @return array * @return array
*/ */
public static function dataClassesFor($class) { public static function dataClassesFor($nameOrObject) {
if(is_string($class) && !class_exists($class)) return array(); if(is_string($nameOrObject) && !class_exists($nameOrObject)) {
return array();
}
$result = array(); $result = array();
$class = self::class_name($class); $class = self::class_name($nameOrObject);
$classes = array_merge( $classes = array_merge(
self::ancestry($class), self::ancestry($class),
@ -134,17 +136,17 @@ class ClassInfo {
* ) * )
* </code> * </code>
* *
* @param mixed $class string of the classname or instance of the class * @param string|object $nameOrObject The classname or object
* @return array Names of all subclasses as an associative array. * @return array Names of all subclasses as an associative array.
*/ */
public static function subclassesFor($class) { public static function subclassesFor($nameOrObject) {
if(is_string($class) && !class_exists($class)) { if(is_string($nameOrObject) && !class_exists($nameOrObject)) {
return []; return [];
} }
//normalise class case //normalise class case
$className = self::class_name($class); $className = self::class_name($nameOrObject);
$descendants = SS_ClassLoader::instance()->getManifest()->getDescendantsOf($class); $descendants = SS_ClassLoader::instance()->getManifest()->getDescendantsOf($className);
$result = array($className => $className); $result = array($className => $className);
if ($descendants) { if ($descendants) {
@ -174,14 +176,16 @@ class ClassInfo {
* Returns the passed class name along with all its parent class names in an * Returns the passed class name along with all its parent class names in an
* array, sorted with the root class first. * array, sorted with the root class first.
* *
* @param string $class * @param string|object $nameOrObject Class or object instance
* @param bool $tablesOnly Only return classes that have a table in the db. * @param bool $tablesOnly Only return classes that have a table in the db.
* @return array * @return array
*/ */
public static function ancestry($class, $tablesOnly = false) { public static function ancestry($nameOrObject, $tablesOnly = false) {
if(is_string($class) && !class_exists($class)) return array(); if(is_string($nameOrObject) && !class_exists($nameOrObject)) {
return array();
}
$class = self::class_name($class); $class = self::class_name($nameOrObject);
$lClass = strtolower($class); $lClass = strtolower($class);

View File

@ -201,11 +201,13 @@ You can use inequalities like `<`, `<=`, `>`, `>=` to compare numbers.
## Includes ## Includes
Within SilverStripe templates we have the ability to include other templates using the `<% include %>` tag. The includes Within SilverStripe templates we have the ability to include other templates using the `<% include %>` tag. The includes
will be searched for using the same filename look-up rules as a regular template, so this will include will be searched for using the same filename look-up rules as a regular template. However in the case of the include tag
`templates/Includes/Sidebar.ss` an additional `Includes` directory will be inserted into the resolved path just prior to the filename.
:::ss E.g.
<% include Includes\SideBar %>
* `<% include SideBar %>` will include `templates/Includes/Sidebar.ss`
* `<% include MyNamespace/SideBar %>` will include `templates/MyNamespace/Includes/Sidebar.ss`
Note that in SilverStripe 3, you didn't have to specify a namespace in your `include` tag, as the template engine didn't Note that in SilverStripe 3, you didn't have to specify a namespace in your `include` tag, as the template engine didn't
use namespaces. As of SilverStripe 4, the template namespaces need to match the folder structure of your template files. use namespaces. As of SilverStripe 4, the template namespaces need to match the folder structure of your template files.

View File

@ -63,6 +63,7 @@
* Search filter classes (e.g. `ExactMatchFilter`) are now registered with `Injector` * Search filter classes (e.g. `ExactMatchFilter`) are now registered with `Injector`
via a new `DataListFilter.` prefix convention. via a new `DataListFilter.` prefix convention.
see [search filter documentation](/developer_guides/model/searchfilters) for more information. see [search filter documentation](/developer_guides/model/searchfilters) for more information.
* FormField templates no longer look in the 'forms' folder for templates.
## New API ## New API
@ -126,6 +127,9 @@
* `PopoverField` added to provide popup-menu behaviour in react forms (currently not available for * `PopoverField` added to provide popup-menu behaviour in react forms (currently not available for
non-react forms). non-react forms).
* Admin URL can now be configured via custom Director routing rule * Admin URL can now be configured via custom Director routing rule
* Templates now use a standard template lookup system via `SSViewer::get_templates_by_class`
which builds a candidate list for a given class. Actual resolution of existing templates
for any list of candidates is actually performed by `SSViewer::chooseTemplate`
### Front-end build tooling for CMS interface ### Front-end build tooling for CMS interface
@ -415,8 +419,14 @@ require manual intervention, please see the below upgrading notes.
Templates are now much more strict about their locations. You can no longer put a template in an arbitrary Templates are now much more strict about their locations. You can no longer put a template in an arbitrary
folder and have it be found. Case is now also checked on case-sensitive filesystems. folder and have it be found. Case is now also checked on case-sensitive filesystems.
Either include the folder in the template name (`renderWith('Field.ss')` => `renderWith('forms/Field.ss')`), move the Either include the folder in the template name (`renderWith('MyEmail.ss')` => `renderWith('emails/MyEmail.ss')`),
template into the correct directory, or both. move the template into the correct directory, or both.
When using `<% include %>` template tag you should continue to leave out the `Includes` folder.
`<% include Sidebar %>` will match only match `Includes/Sidebar.ss`, not `Sidebar.ss`.
Please refer to our [template syntax](/developer_guides/templates/syntax) for details.
The `forms` template folder is no longer used to lookup templates for `FormField` instances.
### Update code that uses SQLQuery ### Update code that uses SQLQuery

View File

@ -479,9 +479,11 @@ class Email extends ViewableData {
if($this->ss_template && !$isPlain) { if($this->ss_template && !$isPlain) {
// Requery data so that updated versions of To, From, Subject, etc are included // Requery data so that updated versions of To, From, Subject, etc are included
$data = $this->templateData(); $data = $this->templateData();
$candidateTemplates = [
$template = new SSViewer('email/'.$this->ss_template); $this->ss_template,
[ 'type' => 'email', $this->ss_template ]
];
$template = new SSViewer($candidateTemplates);
if($template->exists()) { if($template->exists()) {
$fullBody = $template->process($data); $fullBody = $template->process($data);
} }

View File

@ -1025,14 +1025,27 @@ class Form extends RequestHandler {
/** /**
* Return the template to render this form with. * Return the template to render this form with.
* If the template isn't set, then default to the
* form class name e.g "Form".
* *
* @return string * @return string
*/ */
public function getTemplate() { public function getTemplate() {
if($this->template) return $this->template; return $this->template;
else return $this->class; }
/**
* Returs the ordered list of preferred templates for rendering this form
* If the template isn't set, then default to the
* form class name e.g "Form".
*
* @return array
*/
public function getTemplates() {
$templates = SSViewer::get_templates_by_class(get_class($this), '', __CLASS__);
// Prefer any custom template
if($this->getTemplate()) {
array_unshift($templates, $this->getTemplate());
}
return $templates;
} }
/** /**
@ -1639,10 +1652,7 @@ class Form extends RequestHandler {
* @return DBHTMLText * @return DBHTMLText
*/ */
public function forTemplate() { public function forTemplate() {
$return = $this->renderWith(array_merge( $return = $this->renderWith($this->getTemplates());
(array)$this->getTemplate(),
array('Includes/Form')
));
// Now that we're rendered, clear message // Now that we're rendered, clear message
$this->clearMessage(); $this->clearMessage();
@ -1742,7 +1752,7 @@ class Form extends RequestHandler {
/** /**
* Get a list of all actions, including those in the main "fields" FieldList * Get a list of all actions, including those in the main "fields" FieldList
* *
* @return array * @return array
*/ */
protected function getAllActions() { protected function getAllActions() {

View File

@ -761,7 +761,6 @@ class FormField extends RequestHandler {
* *
* @param mixed $value * @param mixed $value
* @param null|array|DataObject $data {@see Form::loadDataFrom} * @param null|array|DataObject $data {@see Form::loadDataFrom}
*
* @return $this * @return $this
*/ */
public function setValue($value) { public function setValue($value) {
@ -1048,22 +1047,14 @@ class FormField extends RequestHandler {
* *
* @return array * @return array
*/ */
private function _templates($customTemplate = null, $customTemplateSuffix = null) { protected function _templates($customTemplate = null, $customTemplateSuffix = null) {
$matches = array(); $templates = SSViewer::get_templates_by_class(get_class($this), $customTemplateSuffix, __CLASS__);
// Prefer any custom template
foreach(array_reverse(ClassInfo::ancestry($this)) as $className) {
$matches[] = 'forms/'. $className . $customTemplateSuffix;
if($className == "FormField") {
break;
}
}
if($customTemplate) { if($customTemplate) {
array_unshift($matches, 'forms/'.$customTemplate); // Prioritise direct template
array_unshift($templates, $customTemplate);
} }
return $templates;
return $matches;
} }
/** /**

View File

@ -25,13 +25,6 @@ use SilverStripe\ORM\DataList;
class GridFieldAddExistingAutocompleter class GridFieldAddExistingAutocompleter
implements GridField_HTMLProvider, GridField_ActionProvider, GridField_DataManipulator, GridField_URLHandler { implements GridField_HTMLProvider, GridField_ActionProvider, GridField_DataManipulator, GridField_URLHandler {
/**
* Which template to use for rendering
*
* @var string $itemClass
*/
protected $itemClass = 'GridFieldAddExistingAutocompleter';
/** /**
* The HTML fragment to write this component into * The HTML fragment to write this component into
*/ */
@ -84,6 +77,7 @@ class GridFieldAddExistingAutocompleter
/** /**
* *
* @param string $targetFragment
* @param array $searchFields Which fields on the object in the list should be searched * @param array $searchFields Which fields on the object in the list should be searched
*/ */
public function __construct($targetFragment = 'before', $searchFields = null) { public function __construct($targetFragment = 'before', $searchFields = null) {
@ -97,7 +91,7 @@ class GridFieldAddExistingAutocompleter
* @return string[] - HTML * @return string[] - HTML
*/ */
public function getHTMLFragments($gridField) { public function getHTMLFragments($gridField) {
$dataClass = $gridField->getList()->dataClass(); $dataClass = $gridField->getModelClass();
$forTemplate = new ArrayData(array()); $forTemplate = new ArrayData(array());
$forTemplate->Fields = new FieldList(); $forTemplate->Fields = new FieldList();
@ -130,8 +124,9 @@ class GridFieldAddExistingAutocompleter
$forTemplate->Fields->setForm($form); $forTemplate->Fields->setForm($form);
} }
$template = SSViewer::get_templates_by_class($this, '', __CLASS__);
return array( return array(
$this->targetFragment => $forTemplate->renderWith('Includes/'.$this->itemClass) $this->targetFragment => $forTemplate->renderWith($template)
); );
} }
@ -174,7 +169,7 @@ class GridFieldAddExistingAutocompleter
if(empty($objectID)) { if(empty($objectID)) {
return $dataList; return $dataList;
} }
$object = DataObject::get_by_id($dataList->dataclass(), $objectID); $object = DataObject::get_by_id($gridField->getModelClass(), $objectID);
if($object) { if($object) {
$dataList->add($object); $dataList->add($object);
} }
@ -198,9 +193,10 @@ class GridFieldAddExistingAutocompleter
* *
* @param GridField $gridField * @param GridField $gridField
* @param SS_HTTPRequest $request * @param SS_HTTPRequest $request
* @return string
*/ */
public function doSearch($gridField, $request) { public function doSearch($gridField, $request) {
$dataClass = $gridField->getList()->dataClass(); $dataClass = $gridField->getModelClass();
$allList = $this->searchList ? $this->searchList : DataList::create($dataClass); $allList = $this->searchList ? $this->searchList : DataList::create($dataClass);
$searchFields = ($this->getSearchFields()) $searchFields = ($this->getSearchFields())
@ -269,6 +265,7 @@ class GridFieldAddExistingAutocompleter
/** /**
* @param array $fields * @param array $fields
* @return $this
*/ */
public function setSearchFields($fields) { public function setSearchFields($fields) {
$this->searchFields = $fields; $this->searchFields = $fields;

View File

@ -43,8 +43,9 @@ class GridFieldAddNewButton implements GridField_HTMLProvider {
'ButtonName' => $this->buttonName, 'ButtonName' => $this->buttonName,
)); ));
$templates = SSViewer::get_templates_by_class($this, '', __CLASS__);
return array( return array(
$this->targetFragment => $data->renderWith('Includes/GridFieldAddNewButton'), $this->targetFragment => $data->renderWith($templates),
); );
} }

View File

@ -26,8 +26,9 @@ class GridFieldButtonRow implements GridField_HTMLProvider {
"RightFragment" => "\$DefineFragment(buttons-{$this->targetFragment}-right)", "RightFragment" => "\$DefineFragment(buttons-{$this->targetFragment}-right)",
)); ));
$templates = SSViewer::get_templates_by_class($this, '', __CLASS__);
return array( return array(
$this->targetFragment => $data->renderWith('Includes/GridFieldButtonRow') $this->targetFragment => $data->renderWith($templates)
); );
} }
} }

View File

@ -127,7 +127,7 @@ class GridFieldDetailForm implements GridField_URLHandler {
$class, $class,
array($gridField, $this, $record, $requestHandler, $this->name) array($gridField, $this, $record, $requestHandler, $this->name)
); );
$handler->setTemplate($this->template); $handler->setTemplate($this->getTemplate());
$this->extend('updateItemRequestHandler', $handler); $this->extend('updateItemRequestHandler', $handler);
return $handler; return $handler;
} }
@ -214,7 +214,7 @@ class GridFieldDetailForm implements GridField_URLHandler {
} else if(ClassInfo::exists(get_class($this) . "_ItemRequest")) { } else if(ClassInfo::exists(get_class($this) . "_ItemRequest")) {
return get_class($this) . "_ItemRequest"; return get_class($this) . "_ItemRequest";
} else { } else {
return 'GridFieldDetailForm_ItemRequest'; return __CLASS__ . '_ItemRequest';
} }
} }
@ -256,7 +256,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
/** /**
* *
* @var GridField_URLHandler * @var GridFieldDetailForm
*/ */
protected $component; protected $component;
@ -283,7 +283,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
/** /**
* @var String * @var String
*/ */
protected $template = 'GridFieldItemEditView'; protected $template = null;
private static $url_handlers = array( private static $url_handlers = array(
'$Action!' => '$Action', '$Action!' => '$Action',
@ -293,7 +293,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
/** /**
* *
* @param GridFIeld $gridField * @param GridFIeld $gridField
* @param GridField_URLHandler $component * @param GridFieldDetailForm $component
* @param DataObject $record * @param DataObject $record
* @param RequestHandler $requestHandler * @param RequestHandler $requestHandler
* @param string $popupFormName * @param string $popupFormName
@ -319,14 +319,14 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
$controller = $this->getToplevelController(); $controller = $this->getToplevelController();
$form = $this->ItemEditForm($this->gridField, $request); $form = $this->ItemEditForm();
$form->makeReadonly(); $form->makeReadonly();
$data = new ArrayData(array( $data = new ArrayData(array(
'Backlink' => $controller->Link(), 'Backlink' => $controller->Link(),
'ItemEditForm' => $form 'ItemEditForm' => $form
)); ));
$return = $data->renderWith($this->template); $return = $data->renderWith($this->getTemplates());
if($request->isAjax()) { if($request->isAjax()) {
return $return; return $return;
@ -337,12 +337,12 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
public function edit($request) { public function edit($request) {
$controller = $this->getToplevelController(); $controller = $this->getToplevelController();
$form = $this->ItemEditForm($this->gridField, $request); $form = $this->ItemEditForm();
$return = $this->customise(array( $return = $this->customise(array(
'Backlink' => $controller->hasMethod('Backlink') ? $controller->Backlink() : $controller->Link(), 'Backlink' => $controller->hasMethod('Backlink') ? $controller->Backlink() : $controller->Link(),
'ItemEditForm' => $form, 'ItemEditForm' => $form,
))->renderWith($this->template); ))->renderWith($this->getTemplates());
if($request->isAjax()) { if($request->isAjax()) {
return $return; return $return;
@ -751,6 +751,21 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
return $this->template; return $this->template;
} }
/**
* Get list of templates to use
*
* @return array
*/
public function getTemplates()
{
$templates = SSViewer::get_templates_by_class($this, '', __CLASS__);
// Prefer any custom template
if($this->getTemplate()) {
array_unshift($templates, $this->getTemplate());
}
return $templates;
}
/** /**
* @return Controller * @return Controller
*/ */

View File

@ -49,6 +49,7 @@ class GridFieldEditButton implements GridField_ColumnProvider {
if($columnName == 'Actions') { if($columnName == 'Actions') {
return array('title' => ''); return array('title' => '');
} }
return [];
} }
/** /**
@ -75,8 +76,7 @@ class GridFieldEditButton implements GridField_ColumnProvider {
* @param GridField $gridField * @param GridField $gridField
* @param DataObject $record * @param DataObject $record
* @param string $columnName * @param string $columnName
* * @return string The HTML for the column
* @return string - the HTML for the column
*/ */
public function getColumnContent($gridField, $record, $columnName) { public function getColumnContent($gridField, $record, $columnName) {
// No permission checks, handled through GridFieldDetailForm, // No permission checks, handled through GridFieldDetailForm,
@ -86,7 +86,8 @@ class GridFieldEditButton implements GridField_ColumnProvider {
'Link' => Controller::join_links($gridField->Link('item'), $record->ID, 'edit') 'Link' => Controller::join_links($gridField->Link('item'), $record->ID, 'edit')
)); ));
return $data->renderWith('Includes/GridFieldEditButton'); $template = SSViewer::get_templates_by_class($this, '', __CLASS__);
return $data->renderWith($template);
} }
/** /**

View File

@ -161,8 +161,9 @@ class GridFieldFilterHeader implements GridField_HTMLProvider, GridField_DataMan
$forTemplate->Fields->push($fields); $forTemplate->Fields->push($fields);
} }
$templates = SSViewer::get_templates_by_class($this, '_Row', __CLASS__);
return array( return array(
'header' => $forTemplate->renderWith('Includes/GridFieldFilterHeader_Row'), 'header' => $forTemplate->renderWith($templates),
); );
} }
} }

View File

@ -25,7 +25,8 @@ class GridFieldFooter implements GridField_HTMLProvider {
/** /**
* *
* @param string $message - a message to display in the footer * @param string $message A message to display in the footer
* @param bool $showrecordcount
*/ */
public function __construct($message = null, $showrecordcount = true) { public function __construct($message = null, $showrecordcount = true) {
if($message) { if($message) {
@ -46,9 +47,10 @@ class GridFieldFooter implements GridField_HTMLProvider {
'NumRecords' => $count 'NumRecords' => $count
)); ));
$template = SSViewer::get_templates_by_class($this, '', __CLASS__);
return array( return array(
'footer' => $forTemplate->renderWith( 'footer' => $forTemplate->renderWith(
'Includes/GridFieldFooter', $template,
array( array(
'Colspan' => count($gridField->getColumns()) 'Colspan' => count($gridField->getColumns())
) )

View File

@ -35,40 +35,48 @@ class GridFieldLevelup extends Object implements GridField_HTMLProvider {
* @param integer $currentID - The ID of the current item; this button will find that item's parent * @param integer $currentID - The ID of the current item; this button will find that item's parent
*/ */
public function __construct($currentID) { public function __construct($currentID) {
if($currentID && is_numeric($currentID)) $this->currentID = $currentID; parent::__construct();
if($currentID && is_numeric($currentID)) {
$this->currentID = $currentID;
}
} }
public function getHTMLFragments($gridField) { public function getHTMLFragments($gridField) {
$modelClass = $gridField->getModelClass(); $modelClass = $gridField->getModelClass();
$parentID = 0; $parentID = 0;
if($this->currentID) { if(!$this->currentID) {
$modelObj = DataObject::get_by_id($modelClass, $this->currentID); return null;
if($modelObj->hasMethod('getParent')) {
$parent = $modelObj->getParent();
} elseif($modelObj->ParentID) {
$parent = $modelObj->Parent();
}
if($parent) $parentID = $parent->ID;
// Attributes
$attrs = array_merge($this->attributes, array(
'href' => sprintf($this->linkSpec, $parentID),
'class' => 'cms-panel-link ss-ui-button font-icon-level-up no-text grid-levelup'
));
$attrsStr = '';
foreach($attrs as $k => $v) $attrsStr .= " $k=\"" . Convert::raw2att($v) . "\"";
$forTemplate = new ArrayData(array(
'UpLink' => DBField::create_field('HTMLFragment', sprintf('<a%s></a>', $attrsStr))
));
return array(
'before' => $forTemplate->renderWith('Includes/GridFieldLevelup'),
);
} }
$modelObj = DataObject::get_by_id($modelClass, $this->currentID);
$parent = null;
if($modelObj->hasMethod('getParent')) {
$parent = $modelObj->getParent();
} elseif($modelObj->ParentID) {
$parent = $modelObj->Parent();
}
if ($parent) {
$parentID = $parent->ID;
}
// Attributes
$attrs = array_merge($this->attributes, array(
'href' => sprintf($this->linkSpec, $parentID),
'class' => 'cms-panel-link ss-ui-button font-icon-level-up no-text grid-levelup'
));
$linkTag = FormField::create_tag('a', $attrs);
$forTemplate = new ArrayData(array(
'UpLink' => DBField::create_field('HTMLFragment', $linkTag)
));
$template = SSViewer::get_templates_by_class($this, '', __CLASS__);
return array(
'before' => $forTemplate->renderWith($template),
);
} }
public function setAttributes($attrs) { public function setAttributes($attrs) {

View File

@ -16,14 +16,7 @@ class GridFieldPageCount implements GridField_HTMLProvider {
protected $targetFragment; protected $targetFragment;
/** /**
* Which template to use for rendering * @param string $targetFragment The fragment indicating the placement of this page count
*
* @var string
*/
protected $itemClass = 'GridFieldPageCount';
/**
* @param string $targetFrament The fragment indicating the placement of this page count
*/ */
public function __construct($targetFragment = 'before') { public function __construct($targetFragment = 'before') {
$this->targetFragment = $targetFragment; $this->targetFragment = $targetFragment;
@ -62,14 +55,16 @@ class GridFieldPageCount implements GridField_HTMLProvider {
* @return array * @return array
*/ */
public function getHTMLFragments($gridField) { public function getHTMLFragments($gridField) {
// Retrieve paging parameters from the directing paginator component // Retrieve paging parameters from the directing paginator component
$paginator = $this->getPaginator($gridField); $paginator = $this->getPaginator($gridField);
if ($paginator && ($forTemplate = $paginator->getTemplateParameters($gridField))) { if ($paginator && ($forTemplate = $paginator->getTemplateParameters($gridField))) {
$template = SSViewer::get_templates_by_class($this, '', __CLASS__);
return array( return array(
$this->targetFragment => $forTemplate->renderWith('Includes/'.$this->itemClass) $this->targetFragment => $forTemplate->renderWith($template)
); );
} }
return null;
} }
} }

View File

@ -25,13 +25,6 @@ class GridFieldPaginator implements GridField_HTMLProvider, GridField_DataManipu
*/ */
protected $itemsPerPage; protected $itemsPerPage;
/**
* Which template to use for rendering
*
* @var string
*/
protected $itemClass = 'GridFieldPaginator_Row';
/** /**
* See {@link setThrowExceptionOnBadDataType()} * See {@link setThrowExceptionOnBadDataType()}
*/ */
@ -255,18 +248,22 @@ class GridFieldPaginator implements GridField_HTMLProvider, GridField_DataManipu
* @return array * @return array
*/ */
public function getHTMLFragments($gridField) { public function getHTMLFragments($gridField) {
$forTemplate = $this->getTemplateParameters($gridField); $forTemplate = $this->getTemplateParameters($gridField);
if($forTemplate) { if(!$forTemplate) {
return array( return null;
'footer' => $forTemplate->renderWith('Includes/'.$this->itemClass,
array('Colspan'=>count($gridField->getColumns())))
);
} }
$template = SSViewer::get_templates_by_class($this, '_Row', __CLASS__);
return array(
'footer' => $forTemplate->renderWith(
$template,
array('Colspan' => count($gridField->getColumns()))
)
);
} }
/** /**
* @param Int * @param int $num
* @return $this
*/ */
public function setItemsPerPage($num) { public function setItemsPerPage($num) {
$this->itemsPerPage = $num; $this->itemsPerPage = $num;

View File

@ -121,8 +121,10 @@ class GridFieldPrintButton implements GridField_HTMLProvider, GridField_ActionPr
Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/GridField_print.css'); Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/GridField_print.css');
if($data = $this->generatePrintData($gridField)){ if($data = $this->generatePrintData($gridField)){
return $data->renderWith("GridField_print"); return $data->renderWith(get_class($gridField)."_print");
} }
return null;
} }
/** /**

View File

@ -163,8 +163,9 @@ class GridFieldSortableHeader implements GridField_HTMLProvider, GridField_DataM
$forTemplate->Fields->push($field); $forTemplate->Fields->push($field);
} }
$template = SSViewer::get_templates_by_class($this, '_Row', __CLASS__);
return array( return array(
'header' => $forTemplate->renderWith('Includes/GridFieldSortableHeader_Row'), 'header' => $forTemplate->renderWith($template),
); );
} }

View File

@ -11,9 +11,14 @@
*/ */
class GridFieldToolbarHeader implements GridField_HTMLProvider { class GridFieldToolbarHeader implements GridField_HTMLProvider {
public function getHTMLFragments( $gridField) { /**
* @param GridField $gridField
* @return array
*/
public function getHTMLFragments($gridField) {
$templates = SSViewer::get_templates_by_class($this, '', __CLASS__);
return array( return array(
'header' => $gridField->renderWith('Includes/GridFieldToolbarHeader') 'header' => $gridField->renderWith($templates)
); );
} }
} }

View File

@ -10,7 +10,9 @@
class GridFieldViewButton implements GridField_ColumnProvider { class GridFieldViewButton implements GridField_ColumnProvider {
public function augmentColumns($field, &$cols) { public function augmentColumns($field, &$cols) {
if(!in_array('Actions', $cols)) $cols[] = 'Actions'; if(!in_array('Actions', $cols)) {
$cols[] = 'Actions';
}
} }
public function getColumnsHandled($field) { public function getColumnsHandled($field) {
@ -18,12 +20,14 @@ class GridFieldViewButton implements GridField_ColumnProvider {
} }
public function getColumnContent($field, $record, $col) { public function getColumnContent($field, $record, $col) {
if($record->canView()) { if(!$record->canView()) {
$data = new ArrayData(array( return null;
'Link' => Controller::join_links($field->Link('item'), $record->ID, 'view')
));
return $data->renderWith('Includes/GridFieldViewButton');
} }
$data = new ArrayData(array(
'Link' => Controller::join_links($field->Link('item'), $record->ID, 'view')
));
$template = SSViewer::get_templates_by_class($this, '', __CLASS__);
return $data->renderWith($template);
} }
public function getColumnAttributes($field, $record, $col) { public function getColumnAttributes($field, $record, $col) {

View File

@ -1,5 +0,0 @@
<% if Backlink %>
<a href="$Backlink"><%t GridFieldItemEditView.Go_back 'Go back' %></a>
<% end_if %>
$ItemEditForm

View File

@ -76,7 +76,7 @@ class RSSFeedTest extends SapphireTest {
$content = $rssFeed->outputToBrowser(); $content = $rssFeed->outputToBrowser();
$this->assertContains('<title>Test Custom Template</title>', $content); $this->assertContains('<title>Test Custom Template</title>', $content);
$rssFeed->setTemplate('RSSFeed'); $rssFeed->setTemplate(null);
$content = $rssFeed->outputToBrowser(); $content = $rssFeed->outputToBrowser();
$this->assertNotContains('<title>Test Custom Template</title>', $content); $this->assertNotContains('<title>Test Custom Template</title>', $content);
} }

View File

@ -123,6 +123,31 @@ class ThemeResourceLoaderTest extends SapphireTest {
); );
} }
public function testFindTemplatesByPath() {
// Items given as full paths are returned directly
$this->assertEquals(
"$this->base/themes/theme/templates/Page.ss",
$this->loader->findTemplate("$this->base/themes/theme/templates/Page.ss", ['theme'])
);
$this->assertEquals(
"$this->base/themes/theme/templates/Page.ss",
$this->loader->findTemplate([
"$this->base/themes/theme/templates/Page.ss",
"Page"
], ['theme'])
);
// Ensure checks for file_exists
$this->assertEquals(
"$this->base/themes/theme/templates/Page.ss",
$this->loader->findTemplate([
"$this->base/themes/theme/templates/NotAPage.ss",
"$this->base/themes/theme/templates/Page.ss",
], ['theme'])
);
}
/** /**
* Test that 'main' and 'Layout' templates are loaded from set theme * Test that 'main' and 'Layout' templates are loaded from set theme
*/ */

View File

@ -110,7 +110,7 @@ class MemberDatetimeOptionsetFieldTest extends SapphireTest {
$field->setDescription('Test description'); $field->setDescription('Test description');
$this->assertEquals('Test description', $field->getDescription()); $this->assertEquals('Test description', $field->getDescription());
$field->setDescriptionTemplate('forms/MemberDatetimeOptionsetField_description_time'); $field->setDescriptionTemplate('MemberDatetimeOptionsetField_description_time');
$this->assertNotEmpty($field->getDescription()); $this->assertNotEmpty($field->getDescription());
$this->assertNotEquals('Test description', $field->getDescription()); $this->assertNotEquals('Test description', $field->getDescription());
} }

View File

@ -1154,22 +1154,57 @@ after')
$this->useTestTheme(dirname(__FILE__), 'layouttest', function() use ($self) { $this->useTestTheme(dirname(__FILE__), 'layouttest', function() use ($self) {
// Test passing a string // Test passing a string
$templates = SSViewer::get_templates_by_class( $templates = SSViewer::get_templates_by_class(
'TestNamespace\SSViewerTestModel_Controller', 'TestNamespace\\SSViewerTestModel_Controller',
'', '',
'Controller' 'Controller'
); );
$self->assertEquals([ $self->assertEquals([
'TestNamespace\SSViewerTestModel_Controller', 'TestNamespace\\SSViewerTestModel_Controller',
[
'type' => 'Includes',
'TestNamespace\\SSViewerTestModel_Controller',
],
'TestNamespace\\SSViewerTestModel',
'Controller', 'Controller',
[
'type' => 'Includes',
'Controller',
],
], $templates); ], $templates);
// Test to ensure we're stopping at the base class. // Test to ensure we're stopping at the base class.
$templates = SSViewer::get_templates_by_class('TestNamespace\SSViewerTestModel_Controller', '', 'TestNamespace\SSViewerTestModel_Controller'); $templates = SSViewer::get_templates_by_class(
$self->assertCount(1, $templates); 'TestNamespace\SSViewerTestModel_Controller',
'',
'TestNamespace\SSViewerTestModel_Controller'
);
$self->assertEquals([
'TestNamespace\\SSViewerTestModel_Controller',
[
'type' => 'Includes',
'TestNamespace\\SSViewerTestModel_Controller',
],
'TestNamespace\\SSViewerTestModel',
], $templates);
// Make sure we can filter our templates by suffix. // Make sure we can search templates by suffix.
$templates = SSViewer::get_templates_by_class('TestNamespace\SSViewerTestModel', '_Controller'); $templates = SSViewer::get_templates_by_class(
$self->assertCount(1, $templates); 'TestNamespace\\SSViewerTestModel',
'_Controller',
'SilverStripe\\ORM\\DataObject'
);
$self->assertEquals([
'TestNamespace\\SSViewerTestModel_Controller',
[
'type' => 'Includes',
'TestNamespace\\SSViewerTestModel_Controller',
],
'SilverStripe\\ORM\\DataObject_Controller',
[
'type' => 'Includes',
'SilverStripe\\ORM\\DataObject_Controller',
],
], $templates);
// Let's throw something random in there. // Let's throw something random in there.
$self->setExpectedException('InvalidArgumentException'); $self->setExpectedException('InvalidArgumentException');

View File

@ -859,51 +859,52 @@ class SSViewer implements Flushable {
} }
/** /**
* Traverses the given the given class context looking for templates with the relevant name. * Traverses the given the given class context looking for candidate template names
* * which match each item in the class hierarchy. The resulting list of template candidates
* @param $className string - valid class name * may or may not exist, but you can invoke {@see SSViewer::chooseTemplate} on any list
* @param $suffix string * to determine the best candidate based on the current themes.
* @param $baseClass string
* *
* @param string|object $classOrObject Valid class name, or object
* @param string $suffix
* @param string $baseClass Class to halt ancestry search at
* @return array * @return array
*/ */
public static function get_templates_by_class($className, $suffix = '', $baseClass = null) { public static function get_templates_by_class($classOrObject, $suffix = '', $baseClass = null) {
// Figure out the class name from the supplied context. // Figure out the class name from the supplied context.
if(!is_string($className) || !class_exists($className)) { if (!is_object($classOrObject) && !(
is_string($classOrObject) && class_exists($classOrObject)
)) {
throw new InvalidArgumentException( throw new InvalidArgumentException(
'SSViewer::get_templates_by_class() expects a valid class name as its first parameter.' 'SSViewer::get_templates_by_class() expects a valid class name as its first parameter.'
); );
} }
$templates = array(); $templates = array();
$classes = array_reverse(ClassInfo::ancestry($className)); $classes = array_reverse(ClassInfo::ancestry($classOrObject));
foreach($classes as $class) { foreach($classes as $class) {
$template = $class . $suffix; $template = $class . $suffix;
if(SSViewer::hasTemplate($template)) { $templates[] = $template;
$templates[] = $template; $templates[] = ['type' => 'Includes', $template];
} elseif(SSViewer::hasTemplate($template = ['type' => 'Includes', $template])) {
$templates[] = $template;
}
// If the class is "Page_Controller", look for Page.ss // If the class is "Page_Controller", look for Page.ss
if(stripos($class,'_controller') !== false) { if (stripos($class, '_controller') !== false) {
$template = str_ireplace('_controller','',$class) . $suffix; $templates[] = str_ireplace('_controller', '', $class) . $suffix;
if(SSViewer::hasTemplate($template)) {
$templates[] = $template;
}
} }
if($baseClass && $class == $baseClass) break; if($baseClass && $class == $baseClass) {
break;
}
} }
return $templates; return $templates;
} }
/** /**
* @param string|array $templateList If passed as a string with .ss extension, used as the "main" template. * @param string|array $templates If passed as a string with .ss extension, used as the "main" template.
* If passed as an array, it can be used for template inheritance (first found template "wins"). * If passed as an array, it can be used for template inheritance (first found template "wins").
* Usually the array values are PHP class names, which directly correlate to template names. * Usually the array values are PHP class names, which directly correlate to template names.
* <code> * <code>
* array('MySpecificPage', 'MyPage', 'Page') * array('MySpecificPage', 'MyPage', 'Page')
* </code> * </code>
* @param TemplateParser $parser
*/ */
public function __construct($templates, TemplateParser $parser = null) { public function __construct($templates, TemplateParser $parser = null) {
if ($parser) { if ($parser) {
@ -929,10 +930,20 @@ class SSViewer implements Flushable {
public function setTemplate($templates) { public function setTemplate($templates) {
$this->templates = $templates; $this->templates = $templates;
$this->chosen = ThemeResourceLoader::instance()->findTemplate($templates, self::get_themes()); $this->chosen = $this->chooseTemplate($templates);
$this->subTemplates = []; $this->subTemplates = [];
} }
/**
* Find the template to use for a given list
*
* @param array|string $templates
* @return string
*/
public static function chooseTemplate($templates) {
return ThemeResourceLoader::instance()->findTemplate($templates, self::get_themes());
}
/** /**
* Set the template parser that will be used in template generation * Set the template parser that will be used in template generation
* @param \TemplateParser $parser * @param \TemplateParser $parser

View File

@ -155,13 +155,6 @@ class ThemeResourceLoader {
$templateList = array($template); $templateList = array($template);
} }
// If we have an .ss extension, this is a path, not a template name. We should
// pass in templates without extensions in order for template manifest to find
// files dynamically.
if(count($templateList) == 1 && is_string($templateList[0]) && substr($templateList[0], -3) == '.ss') {
return $templateList[0];
}
foreach($templateList as $i => $template) { foreach($templateList as $i => $template) {
// Check if passed list of templates in array format // Check if passed list of templates in array format
if (is_array($template)) { if (is_array($template)) {
@ -172,6 +165,13 @@ class ThemeResourceLoader {
continue; continue;
} }
// If we have an .ss extension, this is a path, not a template name. We should
// pass in templates without extensions in order for template manifest to find
// files dynamically.
if(substr($template, -3) == '.ss' && file_exists($template)) {
return $template;
}
// Check string template identifier // Check string template identifier
$template = str_replace('\\', '/', $template); $template = str_replace('\\', '/', $template);
$parts = explode('/', $template); $parts = explode('/', $template);