mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge branch '3.1'
Conflicts: tests/view/SSViewerTest.php
This commit is contained in:
commit
fe8dc50ffc
@ -3,7 +3,6 @@
|
||||
HtmlEditorConfig::get('cms')->setOptions(array(
|
||||
'friendly_name' => 'Default CMS',
|
||||
'priority' => '50',
|
||||
'mode' => 'none', // initialized through LeftAndMain.EditFor.js logic
|
||||
|
||||
'body_class' => 'typography',
|
||||
'document_base_url' => isset($_SERVER['HTTP_HOST']) ? Director::absoluteBaseURL() : null,
|
||||
|
@ -234,12 +234,14 @@ class SS_HTTPResponse {
|
||||
}
|
||||
|
||||
if(in_array($this->statusCode, self::$redirect_codes) && headers_sent($file, $line)) {
|
||||
$url = $this->headers['Location'];
|
||||
$url = (string)$this->headers['Location'];
|
||||
$urlATT = Convert::raw2htmlatt($url);
|
||||
$urlJS = Convert::raw2js($url);
|
||||
echo
|
||||
"<p>Redirecting to <a href=\"$url\" title=\"Click this link if your browser does not redirect you\">"
|
||||
. "$url... (output started on $file, line $line)</a></p>
|
||||
<meta http-equiv=\"refresh\" content=\"1; url=$url\" />
|
||||
<script type=\"text/javascript\">setTimeout('window.location.href = \"$url\"', 50);</script>";
|
||||
"<p>Redirecting to <a href=\"$urlATT\" title=\"Click this link if your browser does not redirect you\">"
|
||||
. "$urlATT... (output started on $file, line $line)</a></p>
|
||||
<meta http-equiv=\"refresh\" content=\"1; url=$urlATT\" />
|
||||
<script type=\"text/javascript\">setTimeout(function(){ window.location.href = \"$urlJS\"; }, 50);</script>";
|
||||
} else {
|
||||
$line = $file = null;
|
||||
if(!headers_sent($file, $line)) {
|
||||
|
@ -64,27 +64,26 @@ class SS_TemplateLoader {
|
||||
public function findTemplates($templates, $theme = null) {
|
||||
$result = array();
|
||||
$project = project();
|
||||
|
||||
|
||||
foreach ((array) $templates as $template) {
|
||||
$found = false;
|
||||
|
||||
|
||||
if (strpos($template, '/')) {
|
||||
list($type, $template) = explode('/', $template, 2);
|
||||
} else {
|
||||
$type = null;
|
||||
}
|
||||
|
||||
|
||||
if ($found = $this->getManifest()->getCandidateTemplate($template, $theme)) {
|
||||
if ($type && isset($found[$type])) {
|
||||
$found = array(
|
||||
'main' => $found[$type]
|
||||
);
|
||||
$found = array(
|
||||
'main' => $found[$type]
|
||||
);
|
||||
}
|
||||
|
||||
$result = array_merge($found, $result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
@ -110,17 +110,23 @@ class SS_TemplateManifest {
|
||||
* @return array
|
||||
*/
|
||||
public function getCandidateTemplate($name, $theme = null) {
|
||||
$found = array();
|
||||
$candidates = $this->getTemplate($name);
|
||||
|
||||
if ($this->project && isset($candidates[$this->project])) {
|
||||
$found = $candidates[$this->project];
|
||||
} else if ($theme && isset($candidates['themes'][$theme])) {
|
||||
|
||||
// theme overrides modules
|
||||
if ($theme && isset($candidates['themes'][$theme])) {
|
||||
$found = array_merge($candidates, $candidates['themes'][$theme]);
|
||||
} else {
|
||||
$found = $candidates;
|
||||
}
|
||||
if(isset($found['themes'])) unset($found['themes']);
|
||||
|
||||
// project overrides theme
|
||||
if ($this->project && isset($candidates[$this->project])) {
|
||||
$found = array_merge($found, $candidates[$this->project]);
|
||||
}
|
||||
|
||||
$found = ($found) ? $found : $candidates;
|
||||
|
||||
if (isset($found['themes'])) unset($found['themes']);
|
||||
if (isset($found[$this->project])) unset($found[$this->project]);
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
|
@ -53,3 +53,4 @@ Used in side panels and action tabs
|
||||
|
||||
.ss-upload .clear { clear: both; }
|
||||
.ss-upload .ss-uploadfield-fromcomputer input { /* since we can't really style the file input, we use this hack to make it as big as the button and hide it */ position: absolute; top: 0; right: 0; margin: 0; opacity: 0; filter: alpha(opacity=0); transform: translate(-300px, 0) scale(4); font-size: 23px; direction: ltr; cursor: pointer; height: 30px; line-height: 30px; }
|
||||
.ss-upload .loader { height: 94px; background: transparent url(../admin/images/spinner.gif) no-repeat 50% 50%; }
|
||||
|
12
docs/en/changelogs/3.0.10.md
Normal file
12
docs/en/changelogs/3.0.10.md
Normal file
@ -0,0 +1,12 @@
|
||||
# 3.0.10
|
||||
|
||||
## Overview
|
||||
|
||||
* Security: Partially cached content from stage or other reading modes is no longer emitted to live
|
||||
|
||||
## Upgrading
|
||||
|
||||
* If relying on partial caching of content between logged in users, be aware that the cache is now automatically
|
||||
segmented based on both the current member ID, and the versioned reading mode. If this is not an appropriate
|
||||
method (such as if the same content is served to logged in users within partial caching) then it is necessary
|
||||
to adjust the config value of `SSViewer::global_key` to something more or less sensitive.
|
@ -83,10 +83,17 @@ a new instance of the class to the [api:GridFieldConfig] object. The `GridField`
|
||||
manipulating the `GridFieldConfig` instance if required.
|
||||
|
||||
:::php
|
||||
// option 1: creating a new GridField with the CustomAction
|
||||
$config = GridFieldConfig::create();
|
||||
$config->addComponent(new GridFieldCustomAction());
|
||||
|
||||
$gridField = new GridField('Teams', 'Teams', $this->Teams(), $config);
|
||||
|
||||
// option 2: adding the CustomAction to an exisitng GridField
|
||||
$gridField->getConfig()->addComponent(new GridFieldCustomAction());
|
||||
|
||||
For documentation on adding a Component to a `GridField` created by `ModelAdmin`
|
||||
please view the [ModelAdmin Reference](/reference/modeladmin#gridfield-customization) section `GridField Customization`
|
||||
|
||||
Now let's go back and dive through the `GridFieldCustomAction` class in more
|
||||
detail.
|
||||
|
@ -28,7 +28,7 @@ This is a highlevel overview of available `[api:FormField]` subclasses. An autom
|
||||
* `[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: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.
|
||||
|
@ -160,19 +160,42 @@ For example, we might want to have a checkbox which limits search results to exp
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
|
||||
### GridField Customization
|
||||
|
||||
To alter how the results are displayed (via `[api:GridField]`), you can also overload the `getEditForm()` method. For example, to add a new component.
|
||||
|
||||
:::php
|
||||
class MyAdmin extends ModelAdmin {
|
||||
private static $managed_models = array('Product','Category');
|
||||
// ...
|
||||
public function getEditForm($id = null, $fields = null) {
|
||||
$form = parent::getEditForm($id, $fields);
|
||||
$gridField = $form->Fields()->fieldByName($this->sanitiseClassName($this->modelClass));
|
||||
// $gridFieldName is generated from the ModelClass, eg if the Class 'Product'
|
||||
// is managed by this ModelAdmin, the GridField for it will also be named 'Product'
|
||||
$gridFieldName = $this->sanitiseClassName($this->modelClass);
|
||||
$gridField = $form->Fields()->fieldByName($gridFieldName);
|
||||
$gridField->getConfig()->addComponent(new GridFieldFilterHeader());
|
||||
return $form;
|
||||
}
|
||||
}
|
||||
|
||||
The above example will add the component to all `GridField`s (of all managed models). Alternatively we can also add it to only one specific `GridField`:
|
||||
|
||||
:::php
|
||||
class MyAdmin extends ModelAdmin {
|
||||
private static $managed_models = array('Product','Category');
|
||||
// ...
|
||||
public function getEditForm($id = null, $fields = null) {
|
||||
$form = parent::getEditForm($id, $fields);
|
||||
$gridFieldName = 'Product';
|
||||
$gridField = $form->Fields()->fieldByName($gridFieldName);
|
||||
if ($gridField) {
|
||||
$gridField->getConfig()->addComponent(new GridFieldFilterHeader());
|
||||
}
|
||||
return $form;
|
||||
}
|
||||
}
|
||||
|
||||
## Managing Relationships
|
||||
|
||||
|
@ -51,6 +51,19 @@ From a block that shows a summary of the page edits if administrator, nothing if
|
||||
<% cached 'loginblock', LastEdited, CurrentMember.isAdmin %>
|
||||
|
||||
|
||||
An additional global key is incorporated in the cache lookup. The default value for this is
|
||||
`$CurrentReadingMode, $CurrentUser.ID`, which ensures that the current `[api:Versioned]` state and user ID are
|
||||
used. This may be configured by changing the config value of `SSViewer.global_key`. It is also necessary
|
||||
to flush the template caching when modifying this config, as this key is cached within the template itself.
|
||||
|
||||
For example, to ensure that the cache is configured to respect another variable, and if the current logged in
|
||||
user does not influence your template content, you can update this key as below;
|
||||
|
||||
:::yaml
|
||||
SSViewer:
|
||||
global_key: '$CurrentReadingMode, $Locale'
|
||||
|
||||
|
||||
## Aggregates
|
||||
|
||||
Often you want to invalidate a cache when any in a set of objects change, or when the objects in a relationship change.
|
||||
|
@ -2,8 +2,7 @@
|
||||
|
||||
## Introduction
|
||||
|
||||
`[api:RestfulService]` enables connecting to remote web services which supports REST interface and consume those web services
|
||||
(for example [Flickr](http://www.flickr.com/services/api/), [Youtube](http://code.google.com/apis/youtube/overview.html), Amazon and etc). `[api:RestfulService]` can parse the XML response (sorry no JSON support)
|
||||
`[api:RestfulService]` uses the php curl library, enabling connections to remote web services which support a REST interface and consuming those web services. (Examples: [Flickr](http://www.flickr.com/services/api/), [Youtube](http://code.google.com/apis/youtube/overview.html), Amazon and etc). `[api:RestfulService]` can parse the XML response (sorry no JSON support)
|
||||
returned from the web service. Further it supports caching of the response, and you can customize the cache interval.
|
||||
|
||||
To gain the functionality you can either create a new `[api:RestfulService]` object or create a class extending the
|
||||
|
@ -107,6 +107,10 @@ class File extends DataObject {
|
||||
private static $extensions = array(
|
||||
"Hierarchy",
|
||||
);
|
||||
|
||||
private static $casting = array (
|
||||
'TreeTitle' => 'HTMLText'
|
||||
);
|
||||
|
||||
/**
|
||||
* @config
|
||||
|
@ -24,10 +24,6 @@ class Folder extends File {
|
||||
private static $plural_name = "Folders";
|
||||
|
||||
private static $default_sort = "\"Name\"";
|
||||
|
||||
private static $casting = array (
|
||||
'TreeTitle' => 'HTMLText'
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -69,7 +69,7 @@ class HtmlEditorConfig {
|
||||
protected $settings = array(
|
||||
'friendly_name' => '(Please set a friendly name for this config)',
|
||||
'priority' => 0,
|
||||
'mode' => "specific_textareas",
|
||||
'mode' => "none", // initialized through HtmlEditorField.js redraw() logic
|
||||
'editor_selector' => "htmleditor",
|
||||
'width' => "100%",
|
||||
'auto_resize' => false,
|
||||
|
@ -295,18 +295,30 @@
|
||||
dialog.ssdialog('open');
|
||||
},
|
||||
attachFiles: function(ids, uploadedFileId) {
|
||||
var self = this, config = this.getConfig();
|
||||
$.post(
|
||||
config['urlAttach'],
|
||||
{'ids': ids},
|
||||
function(data, status, xhr) {
|
||||
var self = this,
|
||||
config = this.getConfig(),
|
||||
indicator = $('<div class="loader" />'),
|
||||
target = (uploadedFileId) ? this.find(".ss-uploadfield-item[data-fileid='"+uploadedFileId+"']") : this.find('.ss-uploadfield-addfile');
|
||||
|
||||
target.children().hide();
|
||||
target.append(indicator);
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: config['urlAttach'],
|
||||
data: {'ids': ids},
|
||||
complete: function(xhr, status) {
|
||||
target.children().show();
|
||||
indicator.remove();
|
||||
},
|
||||
success: function(data, status, xhr) {
|
||||
self.fileupload('attach', {
|
||||
files: data,
|
||||
options: self.fileupload('option'),
|
||||
replaceFileID: uploadedFileId
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
$('div.ss-upload *').entwine({
|
||||
|
16
model/DB.php
16
model/DB.php
@ -147,12 +147,18 @@ class DB {
|
||||
|
||||
/**
|
||||
* Connect to a database.
|
||||
* Given the database configuration, this method will create the correct subclass of SS_Database,
|
||||
* and set it as the global connection.
|
||||
*
|
||||
* Given the database configuration, this method will create the correct
|
||||
* subclass of {@link SS_Database}.
|
||||
*
|
||||
* @param array $database A map of options. The 'type' is the name of the subclass of SS_Database to use. For the
|
||||
* rest of the options, see the specific class.
|
||||
* @param string $name identifier for the connection
|
||||
*
|
||||
* @return SS_Database
|
||||
*/
|
||||
public static function connect($databaseConfig) {
|
||||
public static function connect($databaseConfig, $label = 'default') {
|
||||
|
||||
// This is used by the "testsession" module to test up a test session using an alternative name
|
||||
if($name = self::get_alternative_database_name()) {
|
||||
$databaseConfig['database'] = $name;
|
||||
@ -167,7 +173,9 @@ class DB {
|
||||
$dbClass = $databaseConfig['type'];
|
||||
$conn = new $dbClass($databaseConfig);
|
||||
|
||||
self::setConn($conn);
|
||||
self::setConn($conn, $label);
|
||||
|
||||
return $conn;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -67,7 +67,7 @@ class MySQLDatabase extends SS_Database {
|
||||
} else {
|
||||
$this->dbConn = new MySQLi($parameters['server'], $parameters['username'], $parameters['password']);
|
||||
}
|
||||
|
||||
|
||||
if($this->dbConn->connect_error) {
|
||||
$this->databaseError("Couldn't connect to MySQL database | " . $this->dbConn->connect_error);
|
||||
}
|
||||
@ -86,6 +86,12 @@ class MySQLDatabase extends SS_Database {
|
||||
}
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
if($this->dbConn) {
|
||||
mysqli_close($this->dbConn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Not implemented, needed for PDO
|
||||
*/
|
||||
|
@ -8,8 +8,7 @@
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
class Versioned extends DataExtension {
|
||||
|
||||
class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
/**
|
||||
* An array of possible stages.
|
||||
* @var array
|
||||
@ -1358,6 +1357,12 @@ class Versioned extends DataExtension {
|
||||
public function getDefaultStage() {
|
||||
return $this->defaultStage;
|
||||
}
|
||||
|
||||
public static function get_template_global_variables() {
|
||||
return array(
|
||||
'CurrentReadingMode' => 'get_reading_mode'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -152,6 +152,28 @@ class Date extends DBField {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a date formatted as per a CMS user's settings.
|
||||
*
|
||||
* @param Member $member
|
||||
* @return boolean | string A date formatted as per user-defined settings.
|
||||
*/
|
||||
public function FormatFromSettings($member = null) {
|
||||
require_once 'Zend/Date.php';
|
||||
|
||||
if(!$member) {
|
||||
if(!Member::currentUserID()) {
|
||||
return false;
|
||||
}
|
||||
$member = Member::currentUser();
|
||||
}
|
||||
|
||||
$formatD = $member->getDateFormat();
|
||||
$zendDate = new Zend_Date($this->getValue());
|
||||
|
||||
return $zendDate->toString($formatD);
|
||||
}
|
||||
|
||||
/*
|
||||
* Return a string in the form "12 - 16 Sept" or "12 Aug - 16 Sept"
|
||||
* @param Date $otherDateObj Another date object specifying the end of the range
|
||||
|
@ -92,6 +92,29 @@ class SS_Datetime extends Date implements TemplateGlobalProvider {
|
||||
public function Time24() {
|
||||
if($this->value) return $this->Format('H:i');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a date and time formatted as per a CMS user's settings.
|
||||
*
|
||||
* @param Member $member
|
||||
* @return boolean | string A time and date pair formatted as per user-defined settings.
|
||||
*/
|
||||
public function FormatFromSettings($member = null) {
|
||||
require_once 'Zend/Date.php';
|
||||
|
||||
if(!$member) {
|
||||
if(!Member::currentUserID()) {
|
||||
return false;
|
||||
}
|
||||
$member = Member::currentUser();
|
||||
}
|
||||
|
||||
$formatD = $member->getDateFormat();
|
||||
$formatT = $member->getTimeFormat();
|
||||
|
||||
$zendDate = new Zend_Date($this->getValue());
|
||||
return $zendDate->toString($formatD).' '.$zendDate->toString($formatT);
|
||||
}
|
||||
|
||||
public function requireField() {
|
||||
$parts=Array('datatype'=>'datetime', 'arrayValue'=>$this->arrayValue);
|
||||
|
@ -284,4 +284,8 @@
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
.loader {
|
||||
height: 94px; // Approxmiately matches the height of the field once a file is attached, avoids a 'jump' in size
|
||||
background: transparent url(../admin/images/spinner.gif) no-repeat 50% 50%;
|
||||
}
|
||||
}
|
@ -6,64 +6,184 @@
|
||||
* @subpackage tests
|
||||
*/
|
||||
class TemplateLoaderTest extends SapphireTest {
|
||||
|
||||
public function testFindTemplates() {
|
||||
$base = dirname(__FILE__) . '/fixtures/templatemanifest';
|
||||
$manifest = new SS_TemplateManifest($base, 'myproject', false, true);
|
||||
$loader = new SS_TemplateLoader();
|
||||
|
||||
$manifest->regenerate(false);
|
||||
$loader->pushManifest($manifest);
|
||||
|
||||
$expectPage = array(
|
||||
'main' => "$base/module/templates/Page.ss",
|
||||
'Layout' => "$base/module/templates/Layout/Page.ss"
|
||||
);
|
||||
$expectPageThemed = array(
|
||||
'main' => "$base/themes/theme/templates/Page.ss",
|
||||
'Layout' => "$base/themes/theme/templates/Layout/Page.ss"
|
||||
);
|
||||
|
||||
$this->assertEquals($expectPage, $loader->findTemplates('Page'));
|
||||
$this->assertEquals($expectPage, $loader->findTemplates(array('Foo', 'Page')));
|
||||
$this->assertEquals($expectPage, $loader->findTemplates('PAGE'));
|
||||
$this->assertEquals($expectPageThemed, $loader->findTemplates('Page', 'theme'));
|
||||
|
||||
$expectPageLayout = array('main' => "$base/module/templates/Layout/Page.ss");
|
||||
$expectPageLayoutThemed = array('main' => "$base/themes/theme/templates/Layout/Page.ss");
|
||||
|
||||
$this->assertEquals($expectPageLayout, $loader->findTemplates('Layout/Page'));
|
||||
$this->assertEquals($expectPageLayout, $loader->findTemplates('Layout/PAGE'));
|
||||
$this->assertEquals($expectPageLayoutThemed, $loader->findTemplates('Layout/Page', 'theme'));
|
||||
|
||||
$expectCustomPage = array(
|
||||
'main' => "$base/module/templates/Page.ss",
|
||||
'Layout' => "$base/module/templates/Layout/CustomPage.ss"
|
||||
);
|
||||
|
||||
$this->assertEquals($expectCustomPage, $loader->findTemplates(array('CustomPage', 'Page')));
|
||||
|
||||
// 'main' template only exists in theme, and 'Layout' template only exists in module
|
||||
$expectCustomThemePage = array(
|
||||
'main' => "$base/themes/theme/templates/CustomThemePage.ss",
|
||||
'Layout' => "$base/module/templates/Layout/CustomThemePage.ss"
|
||||
);
|
||||
$this->assertEquals($expectCustomThemePage, $loader->findTemplates(array('CustomThemePage', 'Page'), 'theme'));
|
||||
|
||||
private $base;
|
||||
private $manifest;
|
||||
private $loader;
|
||||
|
||||
/**
|
||||
* Set up manifest before each test
|
||||
*/
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
$this->base = dirname(__FILE__) . '/fixtures/templatemanifest';
|
||||
$this->manifest = new SS_TemplateManifest($this->base, 'myproject', false, true);
|
||||
$this->loader = new SS_TemplateLoader();
|
||||
$this->refreshLoader();
|
||||
}
|
||||
|
||||
public function testFindTemplatesApplicationOverridesModule() {
|
||||
$base = dirname(__FILE__) . '/fixtures/templatemanifest';
|
||||
$manifest = new SS_TemplateManifest($base, 'myproject', false, true);
|
||||
$loader = new SS_TemplateLoader();
|
||||
|
||||
$manifest->regenerate(false);
|
||||
$loader->pushManifest($manifest);
|
||||
|
||||
$expectPage = array(
|
||||
'main' => "$base/myproject/templates/CustomTemplate.ss"
|
||||
|
||||
/**
|
||||
* Test that 'main' and 'Layout' templates are loaded from module
|
||||
*/
|
||||
public function testFindTemplatesInModule() {
|
||||
$expect = array(
|
||||
'main' => "$this->base/module/templates/Page.ss",
|
||||
'Layout' => "$this->base/module/templates/Layout/Page.ss"
|
||||
);
|
||||
|
||||
$this->assertEquals($expectPage, $loader->findTemplates('CustomTemplate'));
|
||||
$this->assertEquals($expect, $this->loader->findTemplates('Page'));
|
||||
$this->assertEquals($expect, $this->loader->findTemplates('PAGE'));
|
||||
$this->assertEquals($expect, $this->loader->findTemplates(array('Foo', 'Page')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that 'main' and 'Layout' templates are loaded from set theme
|
||||
*/
|
||||
public function testFindTemplatesInTheme() {
|
||||
$expect = array(
|
||||
'main' => "$this->base/themes/theme/templates/Page.ss",
|
||||
'Layout' => "$this->base/themes/theme/templates/Layout/Page.ss"
|
||||
);
|
||||
$this->assertEquals($expect, $this->loader->findTemplates('Page', 'theme'));
|
||||
$this->assertEquals($expect, $this->loader->findTemplates('PAGE', 'theme'));
|
||||
$this->assertEquals($expect, $this->loader->findTemplates(array('Foo', 'Page'), 'theme'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that 'main' and 'Layout' templates are loaded from project without a set theme
|
||||
*/
|
||||
public function testFindTemplatesInApplication() {
|
||||
$templates = array(
|
||||
$this->base . '/myproject/templates/Page.ss',
|
||||
$this->base . '/myproject/templates/Layout/Page.ss'
|
||||
);
|
||||
$this->createTestTemplates($templates);
|
||||
$this->refreshLoader();
|
||||
|
||||
$expect = array(
|
||||
'main' => "$this->base/myproject/templates/Page.ss",
|
||||
'Layout' => "$this->base/myproject/templates/Layout/Page.ss"
|
||||
);
|
||||
$this->assertEquals($expect, $this->loader->findTemplates('Page'));
|
||||
$this->assertEquals($expect, $this->loader->findTemplates('PAGE'));
|
||||
$this->assertEquals($expect, $this->loader->findTemplates(array('Foo', 'Page')));
|
||||
|
||||
$this->removeTestTemplates($templates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that 'Layout' template is loaded from module
|
||||
*/
|
||||
public function testFindTemplatesInModuleLayout() {
|
||||
$expect = array(
|
||||
'main' => "$this->base/module/templates/Layout/Page.ss"
|
||||
);
|
||||
$this->assertEquals($expect, $this->loader->findTemplates('Layout/Page'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that 'Layout' template is loaded from theme
|
||||
*/
|
||||
public function testFindTemplatesInThemeLayout() {
|
||||
$expect = array(
|
||||
'main' => "$this->base/themes/theme/templates/Layout/Page.ss"
|
||||
);
|
||||
$this->assertEquals($expect, $this->loader->findTemplates('Layout/Page', 'theme'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that 'main' template is found in theme and 'Layout' is found in module
|
||||
*/
|
||||
public function testFindTemplatesMainThemeLayoutModule() {
|
||||
$expect = array(
|
||||
'main' => "$this->base/themes/theme/templates/CustomThemePage.ss",
|
||||
'Layout' => "$this->base/module/templates/Layout/CustomThemePage.ss"
|
||||
);
|
||||
$this->assertEquals($expect, $this->loader->findTemplates(array('CustomThemePage', 'Page'), 'theme'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that project template overrides module template of same name
|
||||
*/
|
||||
public function testFindTemplatesApplicationOverridesModule() {
|
||||
$expect = array(
|
||||
'main' => "$this->base/myproject/templates/CustomTemplate.ss"
|
||||
);
|
||||
$this->assertEquals($expect, $this->loader->findTemplates('CustomTemplate'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that project templates overrides theme templates
|
||||
*/
|
||||
public function testFindTemplatesApplicationOverridesTheme() {
|
||||
$templates = array(
|
||||
$this->base . '/myproject/templates/Page.ss',
|
||||
$this->base . '/myproject/templates/Layout/Page.ss'
|
||||
);
|
||||
$this->createTestTemplates($templates);
|
||||
$this->refreshLoader();
|
||||
|
||||
$expect = array(
|
||||
'main' => "$this->base/myproject/templates/Page.ss",
|
||||
'Layout' => "$this->base/myproject/templates/Layout/Page.ss"
|
||||
);
|
||||
$this->assertEquals($expect, $this->loader->findTemplates('Page'), 'theme');
|
||||
|
||||
$this->removeTestTemplates($templates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that project 'Layout' template overrides theme 'Layout' template
|
||||
*/
|
||||
public function testFindTemplatesApplicationLayoutOverridesThemeLayout() {
|
||||
$templates = array(
|
||||
$this->base . '/myproject/templates/Layout/Page.ss'
|
||||
);
|
||||
$this->createTestTemplates($templates);
|
||||
$this->refreshLoader();
|
||||
|
||||
$expect = array(
|
||||
'main' => "$this->base/themes/theme/templates/Page.ss",
|
||||
'Layout' => "$this->base/myproject/templates/Layout/Page.ss"
|
||||
);
|
||||
$this->assertEquals($expect, $this->loader->findTemplates('Page', 'theme'));
|
||||
|
||||
$this->removeTestTemplates($templates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that project 'main' template overrides theme 'main' template
|
||||
*/
|
||||
public function testFindTemplatesApplicationMainOverridesThemeMain() {
|
||||
$templates = array(
|
||||
$this->base . '/myproject/templates/Page.ss'
|
||||
);
|
||||
$this->createTestTemplates($templates);
|
||||
$this->refreshLoader();
|
||||
|
||||
$expect = array(
|
||||
'main' => "$this->base/myproject/templates/Page.ss",
|
||||
'Layout' => "$this->base/themes/theme/templates/Layout/Page.ss"
|
||||
);
|
||||
$this->assertEquals($expect, $this->loader->findTemplates('Page', 'theme'));
|
||||
|
||||
$this->removeTestTemplates($templates);
|
||||
}
|
||||
|
||||
protected function refreshLoader() {
|
||||
$this->manifest->regenerate(false);
|
||||
$this->loader->pushManifest($this->manifest);
|
||||
}
|
||||
|
||||
protected function createTestTemplates($templates) {
|
||||
foreach ($templates as $template) {
|
||||
file_put_contents($template, '');
|
||||
}
|
||||
}
|
||||
|
||||
protected function removeTestTemplates($templates) {
|
||||
foreach ($templates as $template) {
|
||||
unlink($template);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
0
tests/core/manifest/fixtures/templatemanifest/myproject/templates/Layout/.gitignore
vendored
Normal file
0
tests/core/manifest/fixtures/templatemanifest/myproject/templates/Layout/.gitignore
vendored
Normal file
@ -201,5 +201,27 @@ class DateTest extends SapphireTest {
|
||||
|
||||
SS_Datetime::clear_mock_now();
|
||||
}
|
||||
|
||||
public function testFormatFromSettings() {
|
||||
|
||||
$memberID = $this->logInWithPermission();
|
||||
$member = DataObject::get_by_id('Member', $memberID);
|
||||
$member->DateFormat = 'dd/MM/YYYY';
|
||||
$member->write();
|
||||
|
||||
$fixtures = array(
|
||||
'2000-12-31' => '31/12/2000',
|
||||
'31-12-2000' => '31/12/2000',
|
||||
'31/12/2000' => '31/12/2000'
|
||||
);
|
||||
|
||||
foreach($fixtures as $from => $to) {
|
||||
$date = DBField::create_field('Date', $from);
|
||||
// With member
|
||||
$this->assertEquals($to, $date->FormatFromSettings($member));
|
||||
// Without member
|
||||
$this->assertEquals($to, $date->FormatFromSettings());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -148,5 +148,30 @@ class SS_DatetimeTest extends SapphireTest {
|
||||
|
||||
SS_Datetime::clear_mock_now();
|
||||
}
|
||||
|
||||
public function testFormatFromSettings() {
|
||||
|
||||
$memberID = $this->logInWithPermission();
|
||||
$member = DataObject::get_by_id('Member', $memberID);
|
||||
$member->DateFormat = 'dd/MM/YYYY';
|
||||
$member->TimeFormat = 'hh:mm:ss';
|
||||
$member->write();
|
||||
|
||||
$fixtures = array(
|
||||
'2000-12-31 10:11:01' => '31/12/2000 10:11:01',
|
||||
'2000-12-31 1:11:01' => '31/12/2000 01:11:01',
|
||||
'12/12/2000 1:11:01' => '12/12/2000 01:11:01',
|
||||
'2000-12-31' => '31/12/2000 12:00:00',
|
||||
'10:11:01' => date('d/m/Y').' 10:11:01'
|
||||
);
|
||||
|
||||
foreach($fixtures as $from => $to) {
|
||||
$date = DBField::create_field('Datetime', $from);
|
||||
// With member
|
||||
$this->assertEquals($to, $date->FormatFromSettings($member));
|
||||
// Without member
|
||||
$this->assertEquals($to, $date->FormatFromSettings());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -20,9 +20,29 @@ class SSViewerCacheBlockTest_Model extends DataObject implements TestOnly {
|
||||
}
|
||||
}
|
||||
|
||||
class SSViewerCacheBlockTest_VersionedModel extends DataObject implements TestOnly {
|
||||
|
||||
protected $entropy = 'default';
|
||||
|
||||
public static $extensions = array(
|
||||
"Versioned('Stage', 'Live')"
|
||||
);
|
||||
|
||||
public function setEntropy($entropy) {
|
||||
$this->entropy = $entropy;
|
||||
}
|
||||
|
||||
public function Inspect() {
|
||||
return $this->entropy . ' ' . Versioned::get_reading_mode();
|
||||
}
|
||||
}
|
||||
|
||||
class SSViewerCacheBlockTest extends SapphireTest {
|
||||
|
||||
protected $extraDataObjects = array('SSViewerCacheBlockTest_Model');
|
||||
protected $extraDataObjects = array(
|
||||
'SSViewerCacheBlockTest_Model',
|
||||
'SSViewerCacheBlockTest_VersionedModel'
|
||||
);
|
||||
|
||||
protected $data = null;
|
||||
|
||||
@ -37,8 +57,7 @@ class SSViewerCacheBlockTest extends SapphireTest {
|
||||
if ($data === null) $data = $this->data;
|
||||
if (is_array($data)) $data = $this->data->customise($data);
|
||||
|
||||
$viewer = SSViewer::fromString($template);
|
||||
return $viewer->process($data);
|
||||
return SSViewer::execute_string($template, $data);
|
||||
}
|
||||
|
||||
public function testParsing() {
|
||||
@ -104,6 +123,77 @@ class SSViewerCacheBlockTest extends SapphireTest {
|
||||
$this->assertEquals($this->_runtemplate('<% cached %>$Foo<% end_cached %>', array('Foo' => 1)), '1');
|
||||
$this->assertEquals($this->_runtemplate('<% cached %>$Foo<% end_cached %>', array('Foo' => 2)), '1');
|
||||
}
|
||||
|
||||
public function testVersionedCache() {
|
||||
|
||||
$origStage = Versioned::current_stage();
|
||||
|
||||
// Run without caching in stage to prove data is uncached
|
||||
$this->_reset(false);
|
||||
Versioned::reading_stage("Stage");
|
||||
$data = new SSViewerCacheBlockTest_VersionedModel();
|
||||
$data->setEntropy('default');
|
||||
$this->assertEquals(
|
||||
'default Stage.Stage',
|
||||
SSViewer::execute_string('<% cached %>$Inspect<% end_cached %>', $data)
|
||||
);
|
||||
$data = new SSViewerCacheBlockTest_VersionedModel();
|
||||
$data->setEntropy('first');
|
||||
$this->assertEquals(
|
||||
'first Stage.Stage',
|
||||
SSViewer::execute_string('<% cached %>$Inspect<% end_cached %>', $data)
|
||||
);
|
||||
|
||||
// Run without caching in live to prove data is uncached
|
||||
$this->_reset(false);
|
||||
Versioned::reading_stage("Live");
|
||||
$data = new SSViewerCacheBlockTest_VersionedModel();
|
||||
$data->setEntropy('default');
|
||||
$this->assertEquals(
|
||||
'default Stage.Live',
|
||||
$this->_runtemplate('<% cached %>$Inspect<% end_cached %>', $data)
|
||||
);
|
||||
$data = new SSViewerCacheBlockTest_VersionedModel();
|
||||
$data->setEntropy('first');
|
||||
$this->assertEquals(
|
||||
'first Stage.Live',
|
||||
$this->_runtemplate('<% cached %>$Inspect<% end_cached %>', $data)
|
||||
);
|
||||
|
||||
// Then with caching, initially in draft, and then in live, to prove that
|
||||
// changing the versioned reading mode doesn't cache between modes, but it does
|
||||
// within them
|
||||
$this->_reset(true);
|
||||
Versioned::reading_stage("Stage");
|
||||
$data = new SSViewerCacheBlockTest_VersionedModel();
|
||||
$data->setEntropy('default');
|
||||
$this->assertEquals(
|
||||
'default Stage.Stage',
|
||||
$this->_runtemplate('<% cached %>$Inspect<% end_cached %>', $data)
|
||||
);
|
||||
$data = new SSViewerCacheBlockTest_VersionedModel();
|
||||
$data->setEntropy('first');
|
||||
$this->assertEquals(
|
||||
'default Stage.Stage', // entropy should be ignored due to caching
|
||||
$this->_runtemplate('<% cached %>$Inspect<% end_cached %>', $data)
|
||||
);
|
||||
|
||||
Versioned::reading_stage('Live');
|
||||
$data = new SSViewerCacheBlockTest_VersionedModel();
|
||||
$data->setEntropy('first');
|
||||
$this->assertEquals(
|
||||
'first Stage.Live', // First hit in live, so display current entropy
|
||||
$this->_runtemplate('<% cached %>$Inspect<% end_cached %>', $data)
|
||||
);
|
||||
$data = new SSViewerCacheBlockTest_VersionedModel();
|
||||
$data->setEntropy('second');
|
||||
$this->assertEquals(
|
||||
'first Stage.Live', // entropy should be ignored due to caching
|
||||
$this->_runtemplate('<% cached %>$Inspect<% end_cached %>', $data)
|
||||
);
|
||||
|
||||
Versioned::reading_stage($origStage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that cacheblocks conditionally cache with if
|
||||
|
@ -67,7 +67,7 @@ class SSTemplateParseException extends Exception {
|
||||
*
|
||||
* Angle Bracket: angle brackets "<" and ">" are used to eat whitespace between template elements
|
||||
* N: eats white space including newlines (using in legacy _t support)
|
||||
*
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage view
|
||||
*/
|
||||
@ -2938,10 +2938,25 @@ class SSTemplateParser extends Parser implements TemplateParser {
|
||||
function CacheBlock_CacheBlockTemplate(&$res, $sub){
|
||||
// Get the block counter
|
||||
$block = ++$res['subblocks'];
|
||||
// Build the key for this block from the passed cache key, the block index, and the sha hash of the template
|
||||
// itself
|
||||
$key = "'" . sha1($sub['php']) . (isset($res['key']) && $res['key'] ? "_'.sha1(".$res['key'].")" : "'") .
|
||||
".'_$block'";
|
||||
// Build the key for this block from the global key (evaluated in a closure within the template),
|
||||
// the passed cache key, the block index, and the sha hash of the template.
|
||||
$res['php'] .= '$keyExpression = function() use ($scope, $cache) {' . PHP_EOL;
|
||||
$res['php'] .= '$val = \'\';' . PHP_EOL;
|
||||
if($globalKey = Config::inst()->get('SSViewer', 'global_key')) {
|
||||
// Embed the code necessary to evaluate the globalKey directly into the template,
|
||||
// so that SSTemplateParser only needs to be called during template regeneration.
|
||||
// Warning: If the global key is changed, it's necessary to flush the template cache.
|
||||
$parser = new SSTemplateParser($globalKey);
|
||||
$result = $parser->match_Template();
|
||||
if(!$result) throw new SSTemplateParseException('Unexpected problem parsing template', $parser);
|
||||
$res['php'] .= $result['php'] . PHP_EOL;
|
||||
}
|
||||
$res['php'] .= 'return $val;' . PHP_EOL;
|
||||
$res['php'] .= '};' . PHP_EOL;
|
||||
$key = 'sha1($keyExpression())' // Global key
|
||||
. '.\'_' . sha1($sub['php']) // sha of template
|
||||
. (isset($res['key']) && $res['key'] ? "_'.sha1(".$res['key'].")" : "'") // Passed key
|
||||
. ".'_$block'"; // block index
|
||||
// Get any condition
|
||||
$condition = isset($res['condition']) ? $res['condition'] : '';
|
||||
|
||||
|
@ -673,10 +673,25 @@ class SSTemplateParser extends Parser implements TemplateParser {
|
||||
function CacheBlock_CacheBlockTemplate(&$res, $sub){
|
||||
// Get the block counter
|
||||
$block = ++$res['subblocks'];
|
||||
// Build the key for this block from the passed cache key, the block index, and the sha hash of the template
|
||||
// itself
|
||||
$key = "'" . sha1($sub['php']) . (isset($res['key']) && $res['key'] ? "_'.sha1(".$res['key'].")" : "'") .
|
||||
".'_$block'";
|
||||
// Build the key for this block from the global key (evaluated in a closure within the template),
|
||||
// the passed cache key, the block index, and the sha hash of the template.
|
||||
$res['php'] .= '$keyExpression = function() use ($scope, $cache) {' . PHP_EOL;
|
||||
$res['php'] .= '$val = \'\';' . PHP_EOL;
|
||||
if($globalKey = Config::inst()->get('SSViewer', 'global_key')) {
|
||||
// Embed the code necessary to evaluate the globalKey directly into the template,
|
||||
// so that SSTemplateParser only needs to be called during template regeneration.
|
||||
// Warning: If the global key is changed, it's necessary to flush the template cache.
|
||||
$parser = new SSTemplateParser($globalKey);
|
||||
$result = $parser->match_Template();
|
||||
if(!$result) throw new SSTemplateParseException('Unexpected problem parsing template', $parser);
|
||||
$res['php'] .= $result['php'] . PHP_EOL;
|
||||
}
|
||||
$res['php'] .= 'return $val;' . PHP_EOL;
|
||||
$res['php'] .= '};' . PHP_EOL;
|
||||
$key = 'sha1($keyExpression())' // Global key
|
||||
. '.\'_' . sha1($sub['php']) // sha of template
|
||||
. (isset($res['key']) && $res['key'] ? "_'.sha1(".$res['key'].")" : "'") // Passed key
|
||||
. ".'_$block'"; // block index
|
||||
// Get any condition
|
||||
$condition = isset($res['condition']) ? $res['condition'] : '';
|
||||
|
||||
|
@ -48,7 +48,6 @@ class SSViewer_Scope {
|
||||
|
||||
private $localIndex;
|
||||
|
||||
|
||||
public function __construct($item, $inheritedScope = null) {
|
||||
$this->item = $item;
|
||||
$this->localIndex = 0;
|
||||
@ -631,6 +630,14 @@ class SSViewer {
|
||||
*/
|
||||
protected $parser;
|
||||
|
||||
/*
|
||||
* Default prepended cache key for partial caching
|
||||
*
|
||||
* @var string
|
||||
* @config
|
||||
*/
|
||||
private static $global_key = '$CurrentReadingMode, $CurrentUser.ID';
|
||||
|
||||
/**
|
||||
* Create a template from a string instead of a .ss file
|
||||
*
|
||||
@ -1096,6 +1103,11 @@ class SSViewer {
|
||||
/**
|
||||
* Execute the given template, passing it the given data.
|
||||
* Used by the <% include %> template tag to process templates.
|
||||
*
|
||||
* @param string $template Template name
|
||||
* @param mixed $data Data context
|
||||
* @param array $arguments Additional arguments
|
||||
* @return string Evaluated result
|
||||
*/
|
||||
public static function execute_template($template, $data, $arguments = null, $scope = null) {
|
||||
$v = new SSViewer($template);
|
||||
@ -1103,6 +1115,23 @@ class SSViewer {
|
||||
|
||||
return $v->process($data, $arguments, $scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the evaluated string, passing it the given data.
|
||||
* Used by partial caching to evaluate custom cache keys expressed using
|
||||
* template expressions
|
||||
*
|
||||
* @param string $content Input string
|
||||
* @param mixed $data Data context
|
||||
* @param array $arguments Additional arguments
|
||||
* @return string Evaluated result
|
||||
*/
|
||||
public static function execute_string($content, $data, $arguments = null) {
|
||||
$v = SSViewer::fromString($content);
|
||||
$v->includeRequirements(false);
|
||||
|
||||
return $v->process($data, $arguments);
|
||||
}
|
||||
|
||||
public function parseTemplateContent($content, $template="") {
|
||||
return $this->parser->compileString(
|
||||
|
Loading…
Reference in New Issue
Block a user