mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge branch '3'
This commit is contained in:
commit
bcc21c2403
@ -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
|
||||
|
@ -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:
|
||||
|
89
admin/client/dist/js/bundle.js
vendored
89
admin/client/dist/js/bundle.js
vendored
@ -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,
|
||||
|
2
admin/client/dist/js/vendor.js
vendored
2
admin/client/dist/js/vendor.js
vendored
File diff suppressed because one or more lines are too long
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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()]));
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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);
|
||||
|
@ -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
|
||||
|
3
main.php
3
main.php
@ -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
|
||||
*/
|
||||
|
@ -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);
|
||||
|
@ -593,8 +593,13 @@ class FieldList extends ArrayList {
|
||||
public function makeFieldReadonly($field) {
|
||||
$fieldName = ($field instanceof FormField) ? $field->getName() : $field;
|
||||
$srcField = $this->dataFieldByName($fieldName);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the order of fields in this FieldList by specifying an ordered list of field names.
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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\")");
|
||||
}
|
||||
|
||||
/**
|
||||
|
29
src/ORM/FieldType/DBBigint.php
Normal file
29
src/ORM/FieldType/DBBigint.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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.
|
||||
*
|
||||
* @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) {
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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,15 +602,21 @@ 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;
|
||||
|
||||
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);
|
||||
@ -605,13 +627,15 @@ class ShortcodeParser extends Object {
|
||||
// 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);
|
||||
user_error('Couldn\'t decode HTML when processing short codes', E_USER_ERRROR);
|
||||
}
|
||||
else {
|
||||
return $content;
|
||||
$continue = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($continue) {
|
||||
// First, replace any shortcodes that are in attributes
|
||||
$this->replaceAttributeTagsWithContent($htmlvalue);
|
||||
|
||||
@ -661,6 +685,9 @@ class ShortcodeParser extends Object {
|
||||
},
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
$this->extend('onAfterParse', $content);
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
@ -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'
|
||||
);
|
||||
$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),
|
||||
|
@ -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',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -261,9 +261,6 @@ class ArrayListTest extends SapphireTest {
|
||||
(object) array('Name' => 'Bob'),
|
||||
array('Name' => 'John'),
|
||||
array('Name' => 'bonny'),
|
||||
array('Name' => 'bonny1'),
|
||||
array('Name' => 'bonny10'),
|
||||
array('Name' => 'bonny2'),
|
||||
));
|
||||
|
||||
// Unquoted name
|
||||
@ -271,9 +268,6 @@ class ArrayListTest extends SapphireTest {
|
||||
$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());
|
||||
@ -283,9 +277,6 @@ class ArrayListTest extends SapphireTest {
|
||||
$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());
|
||||
@ -295,23 +286,86 @@ class ArrayListTest extends SapphireTest {
|
||||
$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());
|
||||
|
||||
// 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'),
|
||||
(object) array('Name' => 'Bob'),
|
||||
array('Name' => 'John'),
|
||||
array('Name' => 'bonny'),
|
||||
array('Name' => 'bonny1'),
|
||||
array('Name' => 'bonny10'),
|
||||
array('Name' => 'bonny2'),
|
||||
), $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' => 'bonny'),
|
||||
array('Name' => 'bonny1'),
|
||||
//array('Name' => 'bonny10'),
|
||||
array('Name' => 'bonny2'),
|
||||
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'),
|
||||
|
@ -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() {
|
||||
|
@ -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()
|
||||
*/
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user