Merge branch '3'

This commit is contained in:
Daniel Hensby 2016-11-09 22:54:33 +00:00
commit bcc21c2403
No known key found for this signature in database
GPG Key ID: 229831A941962E26
32 changed files with 605 additions and 247 deletions

View File

@ -10,10 +10,10 @@ addons:
env:
global:
- TRAVIS_NODE_VERSION="4"
- ARTIFACTS_REGION=us-east-1
- ARTIFACTS_BUCKET=silverstripe-travis-artifacts
- secure: "jVR0iLTuvVfA6jKX5+A3AdUEs8Ps+r3SbL0zGR687K8IoSp3a/+JLH12zFCEexOuxwCtOhlMq8zoZsptCEduCDq+0payk5k6GjNVywFaWjJCV573JScdaHAtoumoHMUvua+Pxds0qKAD2XEYAcOR4Qu7S4HLJV6E1QqHg9PRW5s=" # Encrypted ARTIFACTS_KEY
- secure: "SDGv49c2Ee2YBz7dATE3WnHSVSvJiRJ2BVtRasVshdNDNz3NBRzh13C2fDwTGBU1J6PxiQaGTXBy/BGsvbYk2BvdzHVwozkBpHVSaCNdarpCJ5yZZTqKC3mpA1S5353r5tqronwFuMDpftzXnRMfLZGGQ4kYb9hjV55+FPUTFPk=" # Encrypted ARTIFACTS_SECRET
- "ARTIFACTS_AWS_REGION=us-east-1"
- "ARTIFACTS_S3_BUCKET=silverstripe-travis-artifacts"
- secure: "DjwZKhY/c0wXppGmd8oEMiTV0ayfOXiCmi9Lg1aXoSXNnj+sjLmhYwhUWjehjR6IX0MRtzJG6v7V5Y+4nSGe+i+XIrBQnhPQ95Jrkm1gKofX2mznWTl9npQElNS1DXi58NLPbiB3qxHWGFBRAWmRQrsAouyZabkPnChnSa9ldOg="
- secure: "UmbXCNLK0f2Dk+7qX8bOVcgIt4QhRvccoWvMUxaPtIU+95HCbG10eeCxvfOeBax+tHcRXmeCG4vM4tcuT/WoANkAma/VX74DylFjbWhks2tsKOcr2kjTrOwe6Q9CXOBjVAlcx0lnV/a+w83KARjXGnCrIbE7p7r4EDw31rkVufg="
matrix:
fast_finish: true
@ -34,6 +34,7 @@ matrix:
env: DB=MYSQL CMS_TEST=1 BEHAT_TEST=1
before_script:
- export CORE_RELEASE=$TRAVIS_BRANCH
- printf "\n" | pecl install imagick
- composer self-update || true
- phpenv rehash

View File

@ -36,6 +36,8 @@ SilverStripe\Core\Injector\Injector:
class: SilverStripe\ORM\FieldType\DBHTMLVarchar
Int:
class: SilverStripe\ORM\FieldType\DBInt
BigInt:
class: SilverStripe\ORM\FieldType\DBBigInt
Locale:
class: SilverStripe\ORM\FieldType\DBLocale
DBLocale:

View File

@ -58,64 +58,69 @@ return this.fetch(e,{method:"put",credentials:"same-origin",body:s(t),headers:n}
return this.fetch(e,{method:"delete",credentials:"same-origin",body:s(t),headers:n}).then(a)}}]),e}(),O=new P
t["default"]=O},function(e,t,n){n(8),e.exports=self.fetch.bind(self)},function(e,t){!function(e){"use strict"
function t(e){if("string"!=typeof e&&(e=String(e)),/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(e))throw new TypeError("Invalid character in header field name")
return e.toLowerCase()}function n(e){return"string"!=typeof e&&(e=String(e)),e}function i(e){this.map={},e instanceof i?e.forEach(function(e,t){this.append(t,e)},this):e&&Object.getOwnPropertyNames(e).forEach(function(t){
this.append(t,e[t])},this)}function r(e){return e.bodyUsed?Promise.reject(new TypeError("Already read")):void(e.bodyUsed=!0)}function o(e){return new Promise(function(t,n){e.onload=function(){t(e.result)
return e.toLowerCase()}function n(e){return"string"!=typeof e&&(e=String(e)),e}function i(e){var t={next:function(){var t=e.shift()
return{done:void 0===t,value:t}}}
return m.iterable&&(t[Symbol.iterator]=function(){return t}),t}function r(e){this.map={},e instanceof r?e.forEach(function(e,t){this.append(t,e)},this):e&&Object.getOwnPropertyNames(e).forEach(function(t){
this.append(t,e[t])},this)}function o(e){return e.bodyUsed?Promise.reject(new TypeError("Already read")):void(e.bodyUsed=!0)}function a(e){return new Promise(function(t,n){e.onload=function(){t(e.result)
},e.onerror=function(){n(e.error)}})}function a(e){var t=new FileReader
return t.readAsArrayBuffer(e),o(t)}function s(e){var t=new FileReader
return t.readAsText(e),o(t)}function l(){return this.bodyUsed=!1,this._initBody=function(e){if(this._bodyInit=e,"string"==typeof e)this._bodyText=e
else if(h.blob&&Blob.prototype.isPrototypeOf(e))this._bodyBlob=e
else if(h.formData&&FormData.prototype.isPrototypeOf(e))this._bodyFormData=e
else if(e){if(!h.arrayBuffer||!ArrayBuffer.prototype.isPrototypeOf(e))throw new Error("unsupported BodyInit type")}else this._bodyText=""
this.headers.get("content-type")||("string"==typeof e?this.headers.set("content-type","text/plain;charset=UTF-8"):this._bodyBlob&&this._bodyBlob.type&&this.headers.set("content-type",this._bodyBlob.type))
},e.onerror=function(){n(e.error)}})}function s(e){var t=new FileReader
return t.readAsArrayBuffer(e),a(t)}function l(e){var t=new FileReader
return t.readAsText(e),a(t)}function u(){return this.bodyUsed=!1,this._initBody=function(e){if(this._bodyInit=e,"string"==typeof e)this._bodyText=e
else if(m.blob&&Blob.prototype.isPrototypeOf(e))this._bodyBlob=e
else if(m.formData&&FormData.prototype.isPrototypeOf(e))this._bodyFormData=e
else if(m.searchParams&&URLSearchParams.prototype.isPrototypeOf(e))this._bodyText=e.toString()
else if(e){if(!m.arrayBuffer||!ArrayBuffer.prototype.isPrototypeOf(e))throw new Error("unsupported BodyInit type")}else this._bodyText=""
this.headers.get("content-type")||("string"==typeof e?this.headers.set("content-type","text/plain;charset=UTF-8"):this._bodyBlob&&this._bodyBlob.type?this.headers.set("content-type",this._bodyBlob.type):m.searchParams&&URLSearchParams.prototype.isPrototypeOf(e)&&this.headers.set("content-type","application/x-www-form-urlencoded;charset=UTF-8"))
},h.blob?(this.blob=function(){var e=r(this)
},m.blob?(this.blob=function(){var e=o(this)
if(e)return e
if(this._bodyBlob)return Promise.resolve(this._bodyBlob)
if(this._bodyFormData)throw new Error("could not read FormData body as blob")
return Promise.resolve(new Blob([this._bodyText]))},this.arrayBuffer=function(){return this.blob().then(a)},this.text=function(){var e=r(this)
return Promise.resolve(new Blob([this._bodyText]))},this.arrayBuffer=function(){return this.blob().then(s)},this.text=function(){var e=o(this)
if(e)return e
if(this._bodyBlob)return s(this._bodyBlob)
if(this._bodyBlob)return l(this._bodyBlob)
if(this._bodyFormData)throw new Error("could not read FormData body as text")
return Promise.resolve(this._bodyText)}):this.text=function(){var e=r(this)
return e?e:Promise.resolve(this._bodyText)},h.formData&&(this.formData=function(){return this.text().then(d)}),this.json=function(){return this.text().then(JSON.parse)},this}function u(e){var t=e.toUpperCase()
return Promise.resolve(this._bodyText)}):this.text=function(){var e=o(this)
return e?e:Promise.resolve(this._bodyText)},m.formData&&(this.formData=function(){return this.text().then(f)}),this.json=function(){return this.text().then(JSON.parse)},this}function c(e){var t=e.toUpperCase()
return m.indexOf(t)>-1?t:e}function c(e,t){t=t||{}
return g.indexOf(t)>-1?t:e}function d(e,t){t=t||{}
var n=t.body
if(c.prototype.isPrototypeOf(e)){if(e.bodyUsed)throw new TypeError("Already read")
this.url=e.url,this.credentials=e.credentials,t.headers||(this.headers=new i(e.headers)),this.method=e.method,this.mode=e.mode,n||(n=e._bodyInit,e.bodyUsed=!0)}else this.url=e
if(this.credentials=t.credentials||this.credentials||"omit",!t.headers&&this.headers||(this.headers=new i(t.headers)),this.method=u(t.method||this.method||"GET"),this.mode=t.mode||this.mode||null,this.referrer=null,
if(d.prototype.isPrototypeOf(e)){if(e.bodyUsed)throw new TypeError("Already read")
this.url=e.url,this.credentials=e.credentials,t.headers||(this.headers=new r(e.headers)),this.method=e.method,this.mode=e.mode,n||(n=e._bodyInit,e.bodyUsed=!0)}else this.url=e
if(this.credentials=t.credentials||this.credentials||"omit",!t.headers&&this.headers||(this.headers=new r(t.headers)),this.method=c(t.method||this.method||"GET"),this.mode=t.mode||this.mode||null,this.referrer=null,
("GET"===this.method||"HEAD"===this.method)&&n)throw new TypeError("Body not allowed for GET or HEAD requests")
this._initBody(n)}function d(e){var t=new FormData
this._initBody(n)}function f(e){var t=new FormData
return e.trim().split("&").forEach(function(e){if(e){var n=e.split("="),i=n.shift().replace(/\+/g," "),r=n.join("=").replace(/\+/g," ")
t.append(decodeURIComponent(i),decodeURIComponent(r))}}),t}function f(e){var t=new i,n=e.getAllResponseHeaders().trim().split("\n")
t.append(decodeURIComponent(i),decodeURIComponent(r))}}),t}function p(e){var t=new r,n=(e.getAllResponseHeaders()||"").trim().split("\n")
return n.forEach(function(e){var n=e.trim().split(":"),i=n.shift().trim(),r=n.join(":").trim()
t.append(i,r)}),t}function p(e,t){t||(t={}),this.type="default",this.status=t.status,this.ok=this.status>=200&&this.status<300,this.statusText=t.statusText,this.headers=t.headers instanceof i?t.headers:new i(t.headers),
this.url=t.url||"",this._initBody(e)}if(!e.fetch){i.prototype.append=function(e,i){e=t(e),i=n(i)
t.append(i,r)}),t}function h(e,t){t||(t={}),this.type="default",this.status=t.status,this.ok=this.status>=200&&this.status<300,this.statusText=t.statusText,this.headers=t.headers instanceof r?t.headers:new r(t.headers),
this.url=t.url||"",this._initBody(e)}if(!e.fetch){var m={searchParams:"URLSearchParams"in e,iterable:"Symbol"in e&&"iterator"in Symbol,blob:"FileReader"in e&&"Blob"in e&&function(){try{return new Blob,
!0}catch(e){return!1}}(),formData:"FormData"in e,arrayBuffer:"ArrayBuffer"in e}
r.prototype.append=function(e,i){e=t(e),i=n(i)
var r=this.map[e]
r||(r=[],this.map[e]=r),r.push(i)},i.prototype["delete"]=function(e){delete this.map[t(e)]},i.prototype.get=function(e){var n=this.map[t(e)]
return n?n[0]:null},i.prototype.getAll=function(e){return this.map[t(e)]||[]},i.prototype.has=function(e){return this.map.hasOwnProperty(t(e))},i.prototype.set=function(e,i){this.map[t(e)]=[n(i)]},i.prototype.forEach=function(e,t){
Object.getOwnPropertyNames(this.map).forEach(function(n){this.map[n].forEach(function(i){e.call(t,i,n,this)},this)},this)}
var h={blob:"FileReader"in e&&"Blob"in e&&function(){try{return new Blob,!0}catch(e){return!1}}(),formData:"FormData"in e,arrayBuffer:"ArrayBuffer"in e},m=["DELETE","GET","HEAD","OPTIONS","POST","PUT"]
c.prototype.clone=function(){return new c(this)},l.call(c.prototype),l.call(p.prototype),p.prototype.clone=function(){return new p(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new i(this.headers),
url:this.url})},p.error=function(){var e=new p(null,{status:0,statusText:""})
r||(r=[],this.map[e]=r),r.push(i)},r.prototype["delete"]=function(e){delete this.map[t(e)]},r.prototype.get=function(e){var n=this.map[t(e)]
return n?n[0]:null},r.prototype.getAll=function(e){return this.map[t(e)]||[]},r.prototype.has=function(e){return this.map.hasOwnProperty(t(e))},r.prototype.set=function(e,i){this.map[t(e)]=[n(i)]},r.prototype.forEach=function(e,t){
Object.getOwnPropertyNames(this.map).forEach(function(n){this.map[n].forEach(function(i){e.call(t,i,n,this)},this)},this)},r.prototype.keys=function(){var e=[]
return this.forEach(function(t,n){e.push(n)}),i(e)},r.prototype.values=function(){var e=[]
return this.forEach(function(t){e.push(t)}),i(e)},r.prototype.entries=function(){var e=[]
return this.forEach(function(t,n){e.push([n,t])}),i(e)},m.iterable&&(r.prototype[Symbol.iterator]=r.prototype.entries)
var g=["DELETE","GET","HEAD","OPTIONS","POST","PUT"]
d.prototype.clone=function(){return new d(this)},u.call(d.prototype),u.call(h.prototype),h.prototype.clone=function(){return new h(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new r(this.headers),
url:this.url})},h.error=function(){var e=new h(null,{status:0,statusText:""})
return e.type="error",e}
var g=[301,302,303,307,308]
p.redirect=function(e,t){if(g.indexOf(t)===-1)throw new RangeError("Invalid status code")
return new p(null,{status:t,headers:{location:e}})},e.Headers=i,e.Request=c,e.Response=p,e.fetch=function(e,t){return new Promise(function(n,i){function r(){return"responseURL"in a?a.responseURL:/^X-Request-URL:/m.test(a.getAllResponseHeaders())?a.getResponseHeader("X-Request-URL"):void 0
var v=[301,302,303,307,308]
h.redirect=function(e,t){if(v.indexOf(t)===-1)throw new RangeError("Invalid status code")
return new h(null,{status:t,headers:{location:e}})},e.Headers=r,e.Request=d,e.Response=h,e.fetch=function(e,t){return new Promise(function(n,i){function r(){return"responseURL"in a?a.responseURL:/^X-Request-URL:/m.test(a.getAllResponseHeaders())?a.getResponseHeader("X-Request-URL"):void 0
}var o
o=c.prototype.isPrototypeOf(e)&&!t?e:new c(e,t)
o=d.prototype.isPrototypeOf(e)&&!t?e:new d(e,t)
var a=new XMLHttpRequest
a.onload=function(){var e=1223===a.status?204:a.status
if(e<100||e>599)return void i(new TypeError("Network request failed"))
var t={status:e,statusText:a.statusText,headers:f(a),url:r()},o="response"in a?a.response:a.responseText
n(new p(o,t))},a.onerror=function(){i(new TypeError("Network request failed"))},a.open(o.method,o.url,!0),"include"===o.credentials&&(a.withCredentials=!0),"responseType"in a&&h.blob&&(a.responseType="blob"),
o.headers.forEach(function(e,t){a.setRequestHeader(t,e)}),a.send("undefined"==typeof o._bodyInit?null:o._bodyInit)})},e.fetch.polyfill=!0}}("undefined"!=typeof self?self:this)},function(e,t,n){var i;(function(t,r){
!function(t,n){e.exports=n()}(this,function(){"use strict"
a.onload=function(){var e={status:a.status,statusText:a.statusText,headers:p(a),url:r()},t="response"in a?a.response:a.responseText
n(new h(t,e))},a.onerror=function(){i(new TypeError("Network request failed"))},a.ontimeout=function(){i(new TypeError("Network request failed"))},a.open(o.method,o.url,!0),"include"===o.credentials&&(a.withCredentials=!0),
"responseType"in a&&m.blob&&(a.responseType="blob"),o.headers.forEach(function(e,t){a.setRequestHeader(t,e)}),a.send("undefined"==typeof o._bodyInit?null:o._bodyInit)})},e.fetch.polyfill=!0}}("undefined"!=typeof self?self:this)
},function(e,t,n){var i;(function(t,r){!function(t,n){e.exports=n()}(this,function(){"use strict"
function e(e){return"function"==typeof e||"object"==typeof e&&null!==e}function o(e){return"function"==typeof e}function a(e){K=e}function s(e){Y=e}function l(){return function(){return t.nextTick(p)}}
function u(){return function(){Q(p)}}function c(){var e=0,t=new ee(p),n=document.createTextNode("")
return t.observe(n,{characterData:!0}),function(){n.data=e=++e%2}}function d(){var e=new MessageChannel
@ -1824,8 +1829,8 @@ e.save(),(0,_jQuery2["default"])(e.getElement()).trigger("change")},create:funct
},selectNode:function h(e){this.getInstance().selection.select(e)},setContent:function m(e,t){this.getInstance().setContent(e,t)},insertContent:function g(e,t){this.getInstance().insertContent(e,t)},replaceContent:function v(e,t){
this.getInstance().execCommand("mceReplaceContent",!1,e,t)},insertLink:function y(e,t){this.getInstance().execCommand("mceInsertLink",!1,e,t)},removeLink:function b(){this.getInstance().execCommand("unlink",!1)
},cleanLink:function cleanLink(href,node){var settings=this.getConfig,cb=settings.urlconverter_callback
return cb&&(href=eval(cb+"(href, node, true);")),href.match(new RegExp("^"+tinyMCE.settings.document_base_url+"(.*)$"))&&(href=RegExp.$1),href.match(/^javascript:\s*mctmp/)&&(href=""),href},createBookmark:function w(){
},cleanLink:function cleanLink(href,node){var settings=this.getConfig,cb=settings.urlconverter_callback,cu=tinyMCE.settings.convert_urls
return cb&&(href=eval(cb+"(href, node, true);")),cu&&href.match(new RegExp("^"+tinyMCE.settings.document_base_url+"(.*)$"))&&(href=RegExp.$1),href.match(/^javascript:\s*mctmp/)&&(href=""),href},createBookmark:function w(){
return this.getInstance().selection.getBookmark()},moveToBookmark:function _(e){this.getInstance().selection.moveToBookmark(e),this.getInstance().focus()},blur:function C(){this.getInstance().selection.collapse()
},addUndo:function T(){this.getInstance().undoManager.add()}}},ss.editorWrappers["default"]=ss.editorWrappers.tinyMCE,_jQuery2["default"].entwine("ss",function(e){e("textarea.htmleditor").entwine({Editor:null,

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,4 @@
import jQuery from 'jQuery';
import i18n from 'i18n';
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

View File

@ -216,11 +216,12 @@ ss.editorWrappers.tinyMCE = (function() {
*/
cleanLink: function(href, node) {
var settings = this.getConfig,
cb = settings['urlconverter_callback'];
cb = settings['urlconverter_callback'],
cu = tinyMCE.settings['convert_urls'];
if(cb) href = eval(cb + "(href, node, true);");
// Turn into relative
if(href.match(new RegExp('^' + tinyMCE.settings['document_base_url'] + '(.*)$'))) {
// Turn into relative, if set in TinyMCE config
if(cu && href.match(new RegExp('^' + tinyMCE.settings['document_base_url'] + '(.*)$'))) {
href = RegExp.$1;
}

View File

@ -249,7 +249,7 @@ abstract class ModelAdmin extends LeftAndMain {
// Parse all DateFields to handle user input non ISO 8601 dates
foreach($context->getFields() as $field) {
if($field instanceof DatetimeField) {
if($field instanceof DatetimeField && !empty($params[$field->getName()])) {
$params[$field->getName()] = date('Y-m-d', strtotime($params[$field->getName()]));
}
}

View File

@ -435,7 +435,7 @@ When in a particular scope, `$Up` takes the scope back to the previous level.
<% end_loop %>
Given the following structure, it will output the text.
My Page
|
+-+ Child 1
@ -450,6 +450,16 @@ Given the following structure, it will output the text.
Page 'Grandchild 1' is a grandchild of 'My Page'
Page 'Child 2' is a child of 'MyPage'
<div class="notice" markdown="1">
Additional selectors implicitely change the scope so you need to put additional `$Up` to get what you expect.
</div>
:::ss
<h1>Children of '$Title'</h1>
<% loop $Children.Sort('Title').First %>
<%-- We have two additional selectors in the loop expression so... --%>
<p>Page '$Title' is a child of '$Up.Up.Up.Title'</p>
<% end_loop %>
#### Top
@ -467,8 +477,6 @@ page. The previous example could be rewritten to use the following syntax.
<% end_loop %>
<% end_loop %>
### With
The `<% with %>` tag lets you change into a new scope. Consider the following example:
@ -489,7 +497,12 @@ Outside the `<% with %>.`, we are in the page scope. Inside it, we are in the sc
refer directly to properties and methods of the [api:Member] object. `$FirstName` inside the scope is equivalent to
`$CurrentMember.FirstName`.
### Me
`$Me` outputs the current object in scope. This will call the `forTemplate` of the object.
:::ss
$Me
## Comments

View File

@ -8,7 +8,9 @@ exhaustive list. From your template you can call any method, database field, or
currently in scope as well as its' subclasses or extensions.
Knowing what methods you can call can be tricky, but the first step is to understand the scope you're in. Scope is
explained in more detail on the [syntax](syntax#scope) page.
explained in more detail on the [syntax](syntax#scope) page. Many of the methods listed below can be called from any
scope, and you can specify additional static methods to be available globally in templates by implementing the
[api:TemplateGlobalProvider] interface.
<div class="notice" markdown="1">
Want a quick way of knowing what scope you're in? Try putting `$ClassName` in your template. You should see a string
@ -302,62 +304,7 @@ For example, imagine you're on the "bob marley" page, which is three levels in:
## Navigating Scope
### Me
`$Me` outputs the current object in scope. This will call the `forTemplate` of the object.
:::ss
$Me
### Up
When in a particular scope, `$Up` takes the scope back to the previous level.
:::ss
<h1>Children of '$Title'</h1>
<% loop $Children %>
<p>Page '$Title' is a child of '$Up.Title'</p>
<% loop $Children %>
<p>Page '$Title' is a grandchild of '$Up.Up.Title'</p>
<% end_loop %>
<% end_loop %>
Given the following structure, it will output the text.
My Page
|
+-+ Child 1
| |
| +- Grandchild 1
|
+-+ Child 2
Children of 'My Page'
Page 'Child 1' is a child of 'My Page'
Page 'Grandchild 1' is a grandchild of 'My Page'
Page 'Child 2' is a child of 'MyPage'
### Top
While `$Up` provides us a way to go up one level of scope, `$Top` is a shortcut to jump to the top most scope of the
page. The previous example could be rewritten to use the following syntax.
:::ss
<h1>Children of '$Title'</h1>
<% loop $Children %>
<p>Page '$Title' is a child of '$Top.Title'</p>
<% loop $Children %>
<p>Page '$Title' is a grandchild of '$Top.Title'</p>
<% end_loop %>
<% end_loop %>
See [scope](syntax#scope).
## Breadcrumbs

View File

@ -10,6 +10,11 @@ or even their own code to make it more reusable.
Extensions are defined as subclasses of either [api:DataExtension] for extending a [api:DataObject] subclass or
the [api:Extension] class for non DataObject subclasses (such as [api:Controllers])
<div class="info" markdown="1">
For performance reasons a few classes are excluded from receiving extensions, including `Object`, `ViewableData`
and `RequestHandler`. You can still apply extensions to descendants of these classes.
</div>
**mysite/code/extensions/MyMemberExtension.php**
:::php

View File

@ -0,0 +1,19 @@
title: Template debugging
summary: Track down which template rendered a piece of html
# Debugging templates
## Source code comments
If there is a problem with the rendered html your page is outputting you may need
to track down a template or two. The template engine can help you along by displaying
source code comments indicating which template is responsible for rendering each
block of html on your page.
::::yaml
---
Only:
environment: 'dev'
---
SSViewer:
source_file_comments: true

View File

@ -74,34 +74,36 @@ Note the use of both `.max('LastEdited')` and `.count()` - this takes care of bo
edited since the cache was last built, and also when an object has been deleted since the cache was last built.
</div>
We can also calculate aggregates on relationships. A block that shows the current member's favorites needs to update
whenever the relationship `Member::$has_many = array('Favourites' => Favourite')` changes.
:::ss
<% cached 'favourites', $CurrentMember.ID, $CurrentMember.Favourites.max('LastEdited') %>
We can also calculate aggregates on relationships. The logic for that can get a bit complex, so we can extract that on
to the controller so it's not cluttering up our template.
## Cache key calculated in controller
In the previous example the cache key is getting a bit large, and is complicating our template up. Better would be to
extract that logic into the controller.
If your caching logic is complex or re-usable, you can define a method on your controller to generate a cache key
fragment.
For example, a block that shows a collection of rotating slides needs to update whenever the relationship
`Page::$many_many = array('Slides' => 'Slide')` changes. In Page_Controller:
:::php
public function FavouriteCacheKey() {
$member = Member::currentUser();
return implode('_', array(
'favourites',
$member->ID,
$member->Favourites()->max('LastEdited')
));
public function SliderCacheKey() {
$fragments = array(
'Page-Slides',
$this->ID,
// identify which objects are in the list and their sort order
implode('-', $this->Slides()->Column('ID')),
$this->Slides()->max('LastEdited')
);
return implode('-_-', $fragments);
}
Then using that function in the cache key:
Then reference that function in the cache key:
:::ss
<% cached $FavouriteCacheKey %>
<% cached $SliderCacheKey %>
The example above would work for both a has_many and many_many relationship.
## Cache blocks and template changes
@ -207,8 +209,8 @@ could also write the last example as:
<% end_cached %>
<div class="warning" markdown="1">
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.
Currently a nested cache block can not be contained within an if or loop block. The template engine will throw an error
letting you know if you've done this. You can often get around this using aggregates or by un-nesting the block.
</div>
Failing example:
@ -217,7 +219,7 @@ Failing example:
<% cached $LastEdited %>
<% loop $Children %>
<% cached LastEdited %>
<% cached $LastEdited %>
$Name
<% end_cached %>
<% end_loop %>
@ -236,3 +238,29 @@ Can be re-written as:
<% end_cached %>
<% end_cached %>
Or:
:::ss
<% cached $LastEdited %>
(other code)
<% end_cached %>
<% loop $Children %>
<% cached $LastEdited %>
$Name
<% end_cached %>
<% end_loop %>
## Cache expiry
The default expiry for partial caches is 10 minutes. The advantage of a short cache expiry is that if you have a problem
with your caching logic, the window in which stale content may be shown is short. The disadvantage, particularly for
low-traffic sites, is that cache blocks may expire before they can be utilised. If you're confident that you're caching
logic is sound, you could increase the expiry dramatically.
**mysite/_config.php**
:::php
// Set partial cache expiry to 7 days
SS_Cache::set_cache_lifetime('cacheblock', 60 * 60 * 24 * 7);

View File

@ -9,7 +9,7 @@ covers how to create an `Email` instance, customise it with a HTML template, the
Out of the box, SilverStripe will use the built-in PHP `mail()` command. If you are not running an SMTP server, you
will need to either configure PHP's SMTP settings (see [PHP documentation](http://php.net/mail) to include your mail
server configuration or use one of the third party SMTP services like [Mandrill](https://github.com/lekoala/silverstripe-mandrill)
server configuration or use one of the third party SMTP services like [SparkPost](https://github.com/lekoala/silverstripe-sparkpost)
and [Postmark](https://github.com/fullscreeninteractive/silverstripe-postmarkmailer).
## Usage

View File

@ -77,6 +77,9 @@ if(!empty($_SERVER['HTTP_X_ORIGINAL_URL'])) {
$_SERVER['REQUEST_URI'] = $_SERVER['HTTP_X_ORIGINAL_URL'];
}
// Enable the entity loader to be able to load XML in Zend_Locale_Data
libxml_disable_entity_loader(false);
/**
* Figure out the request URL
*/

View File

@ -250,6 +250,16 @@ class Session {
$session_path = Config::inst()->get('SilverStripe\\Control\\Session', 'session_store_path');
$timeout = Config::inst()->get('SilverStripe\\Control\\Session', 'timeout');
// Director::baseURL can return absolute domain names - this extracts the relevant parts
// for the session otherwise we can get broken session cookies
if (Director::is_absolute_url($path)) {
$urlParts = parse_url($path);
$path = $urlParts['path'];
if (!$domain) {
$domain = $urlParts['host'];
}
}
if(!session_id() && !headers_sent()) {
if($domain) {
session_set_cookie_params($timeout, $path, $domain, $secure, true);

View File

@ -593,7 +593,12 @@ class FieldList extends ArrayList {
public function makeFieldReadonly($field) {
$fieldName = ($field instanceof FormField) ? $field->getName() : $field;
$srcField = $this->dataFieldByName($fieldName);
$this->replaceField($fieldName, $srcField->performReadonlyTransformation());
if($srcField) {
$this->replaceField($fieldName, $srcField->performReadonlyTransformation());
}
else {
user_error("Trying to make field '$fieldName' readonly, but it does not exist in the list",E_USER_WARNING);
}
}
/**

View File

@ -455,7 +455,7 @@ class ArrayList extends ViewableData implements SS_List, Filterable, Sortable, L
// First argument is the direction to be sorted,
$multisortArgs[] = &$sortDirection[$column];
if ($firstRun) {
$multisortArgs[] = defined('SORT_NATURAL') ? SORT_NATURAL : SORT_STRING;
$multisortArgs[] = SORT_REGULAR;
}
$firstRun = false;
}

View File

@ -480,6 +480,22 @@ class MySQLSchemaManager extends DBSchemaManager {
return "int(11) not null" . $this->defaultClause($values);
}
/**
* Return a bigint type-formatted string
*
* @param array $values Contains a tokenised list of info about this data type
* @return string
*/
public function bigint($values) {
//For reference, this is what typically gets passed to this function:
//$parts=Array('datatype'=>'bigint', 'precision'=>20, 'null'=>'not null', 'default'=>$this->defaultVal,
// 'arrayValue'=>$this->arrayValue);
//$values=Array('type'=>'bigint', 'parts'=>$parts);
//DB::requireField($this->tableName, $this->name, $values);
return 'bigint(20) not null' . $this->defaultClause($values);
}
/**
* Return a datetime type-formatted string
* For MySQL, we simply return the word 'datetime', no other parameters are necessary

View File

@ -444,7 +444,7 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
} elseif($numberFuncArgs == 2) {
$whereArguments[func_get_arg(0)] = func_get_arg(1);
} else {
throw new InvalidArgumentException('Incorrect number of arguments passed to exclude()');
throw new InvalidArgumentException('Incorrect number of arguments passed to filterAny()');
}
return $this->alterDataQuery(function(DataQuery $query) use ($whereArguments) {

View File

@ -3062,7 +3062,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @uses DataExtension->requireDefaultRecords()
*/
public function requireDefaultRecords() {
$defaultRecords = $this->stat('default_records');
$defaultRecords = $this->config()->get('default_records', Config::UNINHERITED);
if(!empty($defaultRecords)) {
$hasData = DataObject::get_one($this->class);

View File

@ -436,7 +436,8 @@ class DataQuery {
* @return string
*/
public function max($field) {
return $this->aggregate("MAX(\"$field\")");
$table = ClassInfo::table_for_object_field($this->dataClass, $field);
return $this->aggregate("MAX(\"$table\".\"$field\")");
}
/**
@ -447,7 +448,8 @@ class DataQuery {
* @return string
*/
public function min($field) {
return $this->aggregate("MIN(\"$field\")");
$table = ClassInfo::table_for_object_field($this->dataClass, $field);
return $this->aggregate("MIN(\"$table\".\"$field\")");
}
/**
@ -458,7 +460,8 @@ class DataQuery {
* @return string
*/
public function avg($field) {
return $this->aggregate("AVG(\"$field\")");
$table = ClassInfo::table_for_object_field($this->dataClass, $field);
return $this->aggregate("AVG(\"$table\".\"$field\")");
}
/**
@ -469,7 +472,8 @@ class DataQuery {
* @return string
*/
public function sum($field) {
return $this->aggregate("SUM(\"$field\")");
$table = ClassInfo::table_for_object_field($this->dataClass, $field);
return $this->aggregate("SUM(\"$table\".\"$field\")");
}
/**

View File

@ -0,0 +1,29 @@
<?php
namespace SilverStripe\ORM\FieldType;
use SilverStripe\ORM\DB;
/**
* Represents a signed 8 byte integer field. Do note PHP running as 32-bit might not work with Bigint properly, as it
* would convert the value to a float when queried from the database since the value is a 64-bit one.
*
* @package framework
* @subpackage model
* @see Int
*/
class DBBigInt extends DBInt {
public function requireField() {
$parts = array(
'datatype' => 'bigint',
'precision' => 8,
'null' => 'not null',
'default' => $this->defaultVal,
'arrayValue' => $this->arrayValue
);
$values = array('type' => 'bigint', 'parts' => $parts);
DB::require_field($this->tableName, $this->name, $values);
}
}

View File

@ -10,22 +10,21 @@ use SilverStripe\Core\Object;
*/
class ValidationResult extends Object {
/**
* Boolean - is the result valid or not
* @var bool - is the result valid or not
*/
protected $isValid;
/**
* Array of errors
* @var array of errors
*/
protected $errorList = array();
/**
* Create a new ValidationResult.
* By default, it is a successful result. Call $this->error() to record errors.
*
* By default, it is a successful result. Call $this->error() to record errors.
* @param bool $valid
* @param string $message
* @param string|null $message
*/
public function __construct($valid = true, $message = null) {
$this->isValid = $valid;
@ -36,7 +35,7 @@ class ValidationResult extends Object {
/**
* Record an error against this validation result,
* @param string $message The validation error message
* @param int $code An optional error code string, that can be accessed with {@link $this->codeList()}.
* @param string $code An optional error code string, that can be accessed with {@link $this->codeList()}.
* @return $this
*/
public function error($message, $code = null) {

View File

@ -119,6 +119,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
/**
* @var array
* @config
*/
private static $db = array(
'Version' => 'Int'
@ -1426,7 +1427,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
}
/**
* Determines if the current draft version is the same as live
* Determines if the current draft version is the same as live or rather, that there are no outstanding draft changes
*
* @return bool
*/

View File

@ -28,6 +28,13 @@ class ShortcodeParser extends Object {
// --------------------------------------------------------------------------------------------------------------
/**
* Registered shortcodes. Items follow this structure:
* [shortcode_name] => Array(
* [0] => class_containing_handler
* [1] => name_of_shortcode_handler_method
* )
*/
protected $shortcodes = array();
// --------------------------------------------------------------------------------------------------------------
@ -109,6 +116,15 @@ class ShortcodeParser extends Object {
if($this->registered($shortcode)) unset($this->shortcodes[$shortcode]);
}
/**
* Get an array containing information about registered shortcodes
*
* @return array
*/
public function getRegisteredShortcodes() {
return $this->shortcodes;
}
/**
* Remove all registered shortcodes.
*/
@ -586,81 +602,92 @@ class ShortcodeParser extends Object {
* @return string
*/
public function parse($content) {
$this->extend('onBeforeParse', $content);
$continue = true;
// If no shortcodes defined, don't try and parse any
if(!$this->shortcodes) return $content;
if(!$this->shortcodes) $continue = false;
// If no content, don't try and parse it
if (!trim($content)) return $content;
else if (!trim($content)) $continue = false;
// If no shortcode tag, don't try and parse it
if (strpos($content, '[') === false) return $content;
else if (strpos($content, '[') === false) $continue = false;
// First we operate in text mode, replacing any shortcodes with marker elements so that later we can
// use a proper DOM
list($content, $tags) = $this->replaceElementTagsWithMarkers($content);
if ($continue) {
// First we operate in text mode, replacing any shortcodes with marker elements so that later we can
// use a proper DOM
list($content, $tags) = $this->replaceElementTagsWithMarkers($content);
/** @var HTMLValue $htmlvalue */
$htmlvalue = Injector::inst()->create('HTMLValue', $content);
// Now parse the result into a DOM
if (!$htmlvalue->isValid()){
if(self::$error_behavior == self::ERROR) {
user_error('Couldn\'t decode HTML when processing short codes', E_USER_ERROR);
}
else {
return $content;
// Now parse the result into a DOM
if (!$htmlvalue->isValid()){
if(self::$error_behavior == self::ERROR) {
user_error('Couldn\'t decode HTML when processing short codes', E_USER_ERRROR);
}
else {
$continue = false;
}
}
}
// First, replace any shortcodes that are in attributes
$this->replaceAttributeTagsWithContent($htmlvalue);
if ($continue) {
// First, replace any shortcodes that are in attributes
$this->replaceAttributeTagsWithContent($htmlvalue);
// Find all the element scoped shortcode markers
$shortcodes = $htmlvalue->query('//img[@class="'.self::$marker_class.'"]');
// Find all the element scoped shortcode markers
$shortcodes = $htmlvalue->query('//img[@class="'.self::$marker_class.'"]');
// Find the parents. Do this before DOM modification, since SPLIT might cause parents to move otherwise
$parents = $this->findParentsForMarkers($shortcodes);
// Find the parents. Do this before DOM modification, since SPLIT might cause parents to move otherwise
$parents = $this->findParentsForMarkers($shortcodes);
/** @var DOMElement $shortcode */
foreach($shortcodes as $shortcode) {
$tag = $tags[$shortcode->getAttribute('data-tagid')];
$parent = $parents[$shortcode->getAttribute('data-parentid')];
$class = null;
if(!empty($tag['attrs']['location'])) $class = $tag['attrs']['location'];
else if(!empty($tag['attrs']['class'])) $class = $tag['attrs']['class'];
$class = null;
if(!empty($tag['attrs']['location'])) $class = $tag['attrs']['location'];
else if(!empty($tag['attrs']['class'])) $class = $tag['attrs']['class'];
$location = self::INLINE;
if($class == 'left' || $class == 'right') $location = self::BEFORE;
if($class == 'center' || $class == 'leftALone') $location = self::SPLIT;
$location = self::INLINE;
if($class == 'left' || $class == 'right') $location = self::BEFORE;
if($class == 'center' || $class == 'leftALone') $location = self::SPLIT;
if(!$parent) {
if($location !== self::INLINE) {
user_error("Parent block for shortcode couldn't be found, but location wasn't INLINE",
E_USER_ERROR);
if(!$parent) {
if($location !== self::INLINE) {
user_error("Parent block for shortcode couldn't be found, but location wasn't INLINE",
E_USER_ERROR);
}
}
}
else {
$this->moveMarkerToCompliantHome($shortcode, $parent, $location);
else {
$this->moveMarkerToCompliantHome($shortcode, $parent, $location);
}
$this->replaceMarkerWithContent($shortcode, $tag);
}
$this->replaceMarkerWithContent($shortcode, $tag);
$content = $htmlvalue->getContent();
// Clean up any marker classes left over, for example, those injected into <script> tags
$parser = $this;
$content = preg_replace_callback(
// Not a general-case parser; assumes that the HTML generated in replaceElementTagsWithMarkers()
// hasn't been heavily modified
'/<img[^>]+class="'.preg_quote(self::$marker_class).'"[^>]+data-tagid="([^"]+)"[^>]+>/i',
function ($matches) use ($tags, $parser) {
$tag = $tags[$matches[1]];
return $parser->getShortcodeReplacementText($tag);
},
$content
);
}
$content = $htmlvalue->getContent();
// Clean up any marker classes left over, for example, those injected into <script> tags
$parser = $this;
$content = preg_replace_callback(
// Not a general-case parser; assumes that the HTML generated in replaceElementTagsWithMarkers()
// hasn't been heavily modified
'/<img[^>]+class="'.preg_quote(self::$marker_class).'"[^>]+data-tagid="([^"]+)"[^>]+>/i',
function ($matches) use ($tags, $parser) {
$tag = $tags[$matches[1]];
return $parser->getShortcodeReplacementText($tag);
},
$content
);
$this->extend('onAfterParse', $content);
return $content;
}

View File

@ -15,10 +15,6 @@ use SilverStripe\Forms\Form;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\View\ArrayData;
/**
* @package framework
* @subpackage tests
@ -235,28 +231,77 @@ class CheckboxSetFieldTest extends SapphireTest {
$tag1 = $this->objFromFixture('CheckboxSetFieldTest_Tag', 'tag1');
$tag2 = $this->objFromFixture('CheckboxSetFieldTest_Tag', 'tag2');
$tag3 = $this->objFromFixture('CheckboxSetFieldTest_Tag', 'tag3');
$field = CheckboxSetField::create('Test', 'Testing', $checkboxTestArticle->Tags() ->map());
$field = CheckboxSetField::create('Test', 'Testing', $checkboxTestArticle->Tags());
$validator = new RequiredFields();
$field->setValue(array(
$tag1->ID => $tag1->ID,
$tag2->ID => $tag2->ID
));
$field->setValue(array( $tag1->ID, $tag2->ID ));
$isValid = $field->validate($validator);
$this->assertTrue(
$field->validate($validator),
$isValid,
'Validates values in source map'
);
//invalid value should fail
// Invalid value should fail
$validator = new RequiredFields();
$fakeID = CheckboxSetFieldTest_Tag::get()->max('ID') + 1;
$field->setValue(array($fakeID => $fakeID));
$field->setValue(array($fakeID));
$this->assertFalse(
$field->validate($validator),
'Field does not valid values outside of source map'
);
//non valid value included with valid options should succeed
$errors = $validator->getErrors();
$error = reset($errors);
$this->assertEquals(
_t(
'MultiSelectField.SOURCE_VALIDATION',
"Please select values within the list provided. Invalid option(s) {value} given",
array('value' => $fakeID)
),
$error['message']
);
// Multiple invalid values should fail
$validator = new RequiredFields();
$fakeID = CheckboxSetFieldTest_Tag::get()->max('ID') + 1;
$field->setValue(array($fakeID, $tag3->ID));
$this->assertFalse(
$field->validate($validator),
'Field does not valid values outside of source map'
);
$errors = $validator->getErrors();
$error = reset($errors);
$this->assertEquals(
_t(
'MultiSelectField.SOURCE_VALIDATION',
"Please select values within the list provided. Invalid option(s) {value} given",
array('value' => implode(',', [$fakeID, $tag3->ID]))
),
$error['message']
);
// Invalid value with non-array value
$validator = new RequiredFields();
$field->setValue($fakeID);
$this->assertFalse(
$field->validate($validator),
'Field does not valid values outside of source map'
);
$errors = $validator->getErrors();
$error = reset($errors);
$this->assertEquals(
_t(
'MultiSelectField.SOURCE_VALIDATION',
"Please select values within the list provided. Invalid option(s) {value} given",
array('value' => $fakeID)
),
$error['message']
);
// non valid value included with valid options should succeed
$validator = new RequiredFields();
$field->setValue(array(
$tag1->ID => $tag1->ID,
$tag2->ID => $tag2->ID,
$tag3->ID => $tag3->ID
$tag1->ID,
$tag2->ID,
$tag3->ID
));
$this->assertFalse(
$field->validate($validator),

View File

@ -8,7 +8,7 @@ use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\View\ArrayData;
use SilverStripe\ORM\Map;
/**
@ -18,12 +18,37 @@ use SilverStripe\View\ArrayData;
class DropdownFieldTest extends SapphireTest {
public function testGetSource() {
$source = array(1=>'one');
$source = array(1=>'one', 2 => 'two');
$field = new DropdownField('Field', null, $source);
$this->assertEquals(
$source,
$field->getSource()
);
$this->assertEquals(
$source,
$field->getSource()
);
$items = new ArrayList(array(
array( 'ID' => 1, 'Title' => 'ichi', 'OtherField' => 'notone' ),
array( 'ID' => 2, 'Title' => 'ni', 'OtherField' => 'nottwo' ),
));
$field->setSource($items);
$this->assertEquals(
$field->getSource(),
array(
1 => 'one'
1 => 'ichi',
2 => 'ni',
)
);
$map = new Map($items, 'ID', 'OtherField');
$field->setSource($map);
$this->assertEquals(
$field->getSource(),
array(
1 => 'notone',
2 => 'nottwo',
)
);
}

View File

@ -257,61 +257,115 @@ class ArrayListTest extends SapphireTest {
public function testSortSimpleDefaultIsSortedASC() {
$list = new ArrayList(array(
array('Name' => 'Steve'),
array('Name' => 'Steve'),
(object) array('Name' => 'Bob'),
array('Name' => 'John'),
array('Name' => 'bonny'),
array('Name' => 'bonny1'),
array('Name' => 'bonny10'),
array('Name' => 'bonny2'),
));
// Unquoted name
$list1 = $list->sort('Name');
$this->assertEquals(array(
(object) array('Name' => 'Bob'),
array('Name' => 'bonny'),
array('Name' => 'bonny1'),
array('Name' => 'bonny2'),
array('Name' => 'bonny10'),
array('Name' => 'John'),
array('Name' => 'Steve'),
), $list1->toArray());
(object) array('Name' => 'Bob'),
array('Name' => 'bonny'),
array('Name' => 'John'),
array('Name' => 'Steve'),
), $list1->toArray());
// Quoted name name
$list2 = $list->sort('"Name"');
$this->assertEquals(array(
(object) array('Name' => 'Bob'),
array('Name' => 'bonny'),
array('Name' => 'bonny1'),
array('Name' => 'bonny2'),
array('Name' => 'bonny10'),
array('Name' => 'John'),
array('Name' => 'Steve'),
), $list2->toArray());
(object) array('Name' => 'Bob'),
array('Name' => 'bonny'),
array('Name' => 'John'),
array('Name' => 'Steve'),
), $list2->toArray());
// Array (non-associative)
$list3 = $list->sort(array('"Name"'));
$this->assertEquals(array(
(object) array('Name' => 'Bob'),
array('Name' => 'bonny'),
array('Name' => 'bonny1'),
array('Name' => 'bonny2'),
array('Name' => 'bonny10'),
array('Name' => 'John'),
array('Name' => 'Steve'),
), $list3->toArray());
(object) array('Name' => 'Bob'),
array('Name' => 'bonny'),
array('Name' => 'John'),
array('Name' => 'Steve'),
), $list3->toArray());
// Quoted name name with table
$list4 = $list->sort('"Record"."Name"');
$this->assertEquals(array(
(object) array('Name' => 'Bob'),
array('Name' => 'bonny'),
array('Name' => 'John'),
array('Name' => 'Steve')
), $list4->toArray());
// Quoted name name with table (desc)
$list5 = $list->sort('"Record"."Name" DESC');
$this->assertEquals(array(
array('Name' => 'Steve'),
array('Name' => 'John'),
array('Name' => 'bonny'),
(object) array('Name' => 'Bob')
), $list5->toArray());
// Table without quotes
$list6 = $list->sort('Record.Name');
$this->assertEquals(array(
(object) array('Name' => 'Bob'),
array('Name' => 'bonny'),
array('Name' => 'John'),
array('Name' => 'Steve')
), $list6->toArray());
// Check original list isn't altered
$this->assertEquals(array(
array('Name' => 'Steve'),
array('Name' => 'Steve'),
(object) array('Name' => 'Bob'),
array('Name' => 'John'),
array('Name' => 'bonny'),
), $list->toArray());
}
public function testMixedCaseSort() {
// Note: Natural sorting is not expected, so if 'bonny10' were included
// below we would expect it to appear between bonny1 and bonny2. That's
// undesirable though so we're not enforcing it in tests.
$original = array(
array('Name' => 'Steve'),
(object) array('Name' => 'Bob'),
array('Name' => 'John'),
array('Name' => 'bonny'),
array('Name' => 'bonny1'),
//array('Name' => 'bonny10'),
array('Name' => 'bonny2'),
);
$list = new ArrayList($original);
$expected = array(
(object) array('Name' => 'Bob'),
array('Name' => 'John'),
array('Name' => 'bonny'),
array('Name' => 'bonny1'),
array('Name' => 'bonny10'),
//array('Name' => 'bonny10'),
array('Name' => 'bonny2'),
), $list->toArray());
array('Name' => 'John'),
array('Name' => 'Steve'),
);
// Unquoted name
$list1 = $list->sort('Name');
$this->assertEquals($expected, $list1->toArray());
// Quoted name name
$list2 = $list->sort('"Name"');
$this->assertEquals($expected, $list2->toArray());
// Array (non-associative)
$list3 = $list->sort(array('"Name"'));
$this->assertEquals($expected, $list3->toArray());
// Check original list isn't altered
$this->assertEquals($original, $list->toArray());
}
@ -409,6 +463,42 @@ class ArrayListTest extends SapphireTest {
));
}
public function testSortNumeric() {
$list = new ArrayList(array(
array('Sort' => 0),
array('Sort' => -1),
array('Sort' => 1),
array('Sort' => -2),
array('Sort' => 2),
array('Sort' => -10),
array('Sort' => 10)
));
// Sort descending
$list1 = $list->sort('Sort', 'DESC');
$this->assertEquals(array(
array('Sort' => 10),
array('Sort' => 2),
array('Sort' => 1),
array('Sort' => 0),
array('Sort' => -1),
array('Sort' => -2),
array('Sort' => -10)
), $list1->toArray());
// Sort ascending
$list1 = $list->sort('Sort', 'ASC');
$this->assertEquals(array(
array('Sort' => -10),
array('Sort' => -2),
array('Sort' => -1),
array('Sort' => 0),
array('Sort' => 1),
array('Sort' => 2),
array('Sort' => 10)
), $list1->toArray());
}
public function testReverse() {
$list = new ArrayList(array(
array('Name' => 'John'),

View File

@ -171,6 +171,11 @@ class DBFieldTest extends SapphireTest {
$this->assertEquals("00:00:00", $time->getValue());
$time->setValue('00:00:00');
$this->assertEquals("00:00:00", $time->getValue());
/* BigInt behaviour */
$bigInt = singleton('SilverStripe\\ORM\FieldType\\DBBigInt');
$bigInt->setValue(PHP_INT_MAX);
$this->assertEquals(PHP_INT_MAX, $bigInt->getValue());
}
public function testExists() {

View File

@ -353,6 +353,17 @@ class DataListTest extends SapphireTest {
$this->assertEquals($otherExpected, $otherMap);
}
public function testAmbiguousAggregate() {
// Test that we avoid ambiguity error when a field exists on two joined tables
// Fetch the sponsors in a round-about way to simulate this
$teamID = $this->idFromFixture('DataObjectTest_Team','team2');
$sponsors = DataObjectTest_EquipmentCompany::get()->filter('SponsoredTeams.ID', $teamID);
$this->assertNotNull($sponsors->Max('ID'));
$this->assertNotNull($sponsors->Min('ID'));
$this->assertNotNull($sponsors->Avg('ID'));
$this->assertNotNull($sponsors->Sum('ID'));
}
public function testEach() {
$list = DataObjectTest_TeamComment::get();
@ -544,6 +555,34 @@ class DataListTest extends SapphireTest {
public function testSortNumeric() {
$list = DataObjectTest_Sortable::get();
$list1 = $list->sort('Sort', 'ASC');
$this->assertEquals(array(
-10,
-2,
-1,
0,
1,
2,
10
), $list1->column('Sort'));
}
public function testSortMixedCase() {
$list = DataObjectTest_Sortable::get();
$list1 = $list->sort('Name', 'ASC');
$this->assertEquals(array(
'Bob',
'bonny',
'jane',
'John',
'sam',
'Steve',
'steven'
), $list1->column('Name'));
}
/**
* Test DataList->canFilterBy()
*/

View File

@ -40,6 +40,7 @@ class DataObjectTest extends SapphireTest {
'DataObjectTest_EquipmentCompany',
'DataObjectTest_SubEquipmentCompany',
'DataObjectTest\NamespacedClass',
'DataObjectTest_Sortable',
'DataObjectTest\RelationClass',
'DataObjectTest_ExtendedTeamComment',
'DataObjectTest_Company',
@ -1771,6 +1772,20 @@ class DataObjectTest extends SapphireTest {
}
public function testBigIntField() {
$staff = new DataObjectTest_Staff();
$staff->Salary = PHP_INT_MAX;
$staff->write();
$this->assertEquals(PHP_INT_MAX, DataObjectTest_Staff::get()->byID($staff->ID)->Salary);
}
}
class DataObjectTest_Sortable extends DataObject implements TestOnly {
private static $db = array(
'Sort' => 'Int',
'Name' => 'Varchar',
);
}
class DataObjectTest_Player extends Member implements TestOnly {
@ -1988,11 +2003,14 @@ class DataObjectTest_EquipmentCompany extends DataObjectTest_Company implements
class DataObjectTest_SubEquipmentCompany extends DataObjectTest_EquipmentCompany implements TestOnly {
private static $db = array(
'SubclassDatabaseField' => 'Varchar'
'SubclassDatabaseField' => 'Varchar',
);
}
class DataObjectTest_Staff extends DataObject implements TestOnly {
private static $db = array(
'Salary' => 'BigInt',
);
private static $has_one = array (
'CurrentCompany' => 'DataObjectTest_Company',
'PreviousCompany' => 'DataObjectTest_Company'

View File

@ -1,3 +1,25 @@
DataObjectTest_Sortable:
numeric1:
Sort: 0
Name: steven
numeric2:
Sort: -1
Name: bonny
numeric3:
Sort: 1
Name: sam
numeric4:
Sort: -2
Name: Bob
numeric5:
Sort: 2
Name: jane
numeric6:
Sort: -10
Name: Steve
numeric7:
Sort: 10
Name: John
DataObjectTest_EquipmentCompany:
equipmentcompany1:
Name: Company corp