Merge 3 into master

# Conflicts:
#	CONTRIBUTING.md
#	admin/css/screen.css
#	admin/css/screen.css.map
#	admin/javascript/LeftAndMain.EditForm.js
#	admin/javascript/LeftAndMain.js
#	admin/scss/_forms.scss
#	dev/Debug.php
#	docs/en/05_Contributing/01_Code.md
#	forms/DropdownField.php
#	model/DataObject.php
#	model/Versioned.php
#	model/fieldtypes/DBLocale.php
#	tests/forms/gridfield/GridFieldExportButtonTest.yml
#	tests/model/MoneyTest.php
#	tests/model/MoneyTest.yml
#	tests/model/SQLQueryTest.php
This commit is contained in:
Damian Mooyman 2016-05-18 18:36:10 +12:00
commit 574bc6038b
21 changed files with 657 additions and 260 deletions

View File

@ -4,24 +4,6 @@ Any open source product is only as good as the community behind it. You can part
See our [high level overview](http://silverstripe.org/contributing-to-silverstripe) on silverstripe.org on how you can help out.
## Contributing to the correct version
## Contributing code
SilverStripe core and module releases (since the 3.1.8 release) follow the [Semantic Versioning](http://semver.org)
(SemVar) specification for releases. Using this specification declares to the entire development community the severity
and intention of each release. It gives developers the ability to safely declare their dependencies and understand the
scope involved in each upgrade.
Each release is labeled in the format `$MAJOR`.`$MINOR`.`$PATCH`. For example, 3.1.8 or 3.2.0.
* `$MAJOR` version is incremented if any backwards incompatible changes are introduced to the public API.
* `$MINOR` version is incremented if new, backwards compatible **functionality** is introduced to the public API or
improvements are introduced within the private code.
* `$PATCH` version is incremented if only backwards compatible **bug fixes** are introduced. A bug fix is defined as
an internal change that fixes incorrect behavior.
Git Branches are setup for each `$MINOR` version (e.g. 3.1, 3.2). Each `$PATCH` release is a git tag off the `$MINOR`
branch. For example, 3.1.8 will be a git tag of 3.1.8.
When contributing code, be aware of the scope of your changes. If your change is backwards incompatible, raise your
change against the `master` branch. The master branch contains the next `$MAJOR` release. If the change is backwards
compatible raise it against the correct `$MINOR` branch.
See [contributing code](docs/en/05_Contributing/01_Code.md)

View File

@ -77,13 +77,35 @@ class ErrorControlChain {
*/
public function setSuppression($suppression) {
$this->suppression = (bool)$suppression;
// Don't modify errors unless handling fatal errors, and if errors were
// originally allowed to be displayed.
if ($this->handleFatalErrors && $this->originalDisplayErrors) {
ini_set('display_errors', !$suppression);
// If handling fatal errors, conditionally disable, or restore error display
// Note: original value of display_errors could also evaluate to "off"
if ($this->handleFatalErrors) {
if($suppression) {
$this->setDisplayErrors(0);
} else {
$this->setDisplayErrors($this->originalDisplayErrors);
}
}
}
/**
* Set display_errors
*
* @param mixed $errors
*/
protected function setDisplayErrors($errors) {
ini_set('display_errors', $errors);
}
/**
* Get value of display_errors ini value
*
* @return mixed
*/
protected function getDisplayErrors() {
return ini_get('display_errors');
}
/**
* Add this callback to the chain of callbacks to call along with the state
* that $error must be in this point in the chain for the callback to be called
@ -178,7 +200,7 @@ class ErrorControlChain {
register_shutdown_function(array($this, 'handleFatalError'));
$this->handleFatalErrors = true;
$this->originalDisplayErrors = ini_get('display_errors');
$this->originalDisplayErrors = $this->getDisplayErrors();
$this->setSuppression($this->suppression);
$this->step();
@ -202,7 +224,7 @@ class ErrorControlChain {
else {
// Now clean up
$this->handleFatalErrors = false;
ini_set('display_errors', $this->originalDisplayErrors);
$this->setDisplayErrors($this->originalDisplayErrors);
}
}
}

View File

@ -14,3 +14,20 @@ was affected by these:
* When FormFields are rendered, leading & trailing whitespace is now stripped. The resulting HTML for form fields is
the same for the default fields, but if you have a custom form field that is relying on trailing whitespace being
outputted.
* DataObject::isChanged() now defaults to only checking database fields. If you rely on this method
for checking changes to non-db field properties, use getChangedFields() instead.
### Error handling
Up until 3.4.0 error responses handled by SilverStripe have normally returned HTTP 200. The correct http response
code can be turned on by setting `Debug.friendly_error_httpcode` config to true. This option will be removed in
4.0 and fixed to always on.
:::yaml
---
Name: mydebug
---
Debug:
friendly_error_httpcode: true

View File

@ -69,6 +69,39 @@ We ask for this so that the ownership in the license is clear and unambiguous, a
The core team is then responsible for reviewing patches and deciding if they will make it into core. If
there are any problems they will follow up with you, so please ensure they have a way to contact you!
### Picking the right version
SilverStripe core and module releases (since the 3.1.8 release) follow the [Semantic Versioning](http://semver.org)
(SemVer) specification for releases. Using this specification declares to the entire development community the severity
and intention of each release. It gives developers the ability to safely declare their dependencies and understand the
scope involved in each upgrade.
Each release is labeled in the format `$MAJOR`.`$MINOR`.`$PATCH`. For example, 3.1.8 or 3.2.0.
* `$MAJOR` version is incremented if any backwards incompatible changes are introduced to the public API.
* `$MINOR` version is incremented if new, backwards compatible **functionality** is introduced to the public API or
improvements are introduced within the private code.
* `$PATCH` version is incremented if only backwards compatible **bug fixes** are introduced. A bug fix is defined as
an internal change that fixes incorrect behavior.
**Public API** refers to any aspect of the system that has been designed to be used by SilverStripe modules & site developers. In SilverStripe 3, because we haven't been clear, in principle we have to treat every public or protected method as *potentially* part of the public API, but sometimes it comes to a judgement call about how likely it is that a given method will have been used in a particular way. If we were strict about never changing publicly exposed behaviour, it would be difficult to fix any bug whatsoever, which isn't in the interests of our user community.
In future major releases of SilverStripe, we will endeavour to be more explicit about documenting the public API.
**Contributing bug fixes**
Bug fixes should be raised against the most recent MINOR release branch. For example, If your project is on 3.3.1 and 3.4.0 is released, please raise your bugfix against the `3.4` branch. Older MINOR release branches are primarily intended for critical bugfixes and security issues.
**Contributing features**
When contributing a backwards compatible change, raise it against the same MAJOR branch as your project. For example, if your project is on 3.3.1, raise it against the `3` branch. It will be included in the next MINOR release, e.g. 3.4.0. And then when it is released, you should upgrade your project to use it. As it is a MINOR change, it shouldn't break anything, and be a relatively painless upgrade.
**Contributing backwards-incompatible public API changes, and removing or radically changing existing feautres**
When contributing a backwards incompatible change, you must raise it against the `master` branch.
### The Pull Request Process
Once your pull request is issued, it's not the end of the road. A [core committer](/contributing/core_committers/) will most likely have some questions for you and may ask you to make some changes depending on discussions you have.
@ -123,10 +156,7 @@ If you're familiar with it, here's the short version of what you need to know. O
* **Squash your commits, so that each commit addresses a single issue.** After you rebase your work on top of the upstream master, you can squash multiple commits into one. Say, for instance, you've got three commits in related to Issue #100. Squash all three into one with the message "Description of the issue here (fixes #100)" We won't accept pull requests for multiple commits related to a single issue; it's up to you to squash and clean your commit tree. (Remember, if you squash commits you've already pushed to GitHub, you won't be able to push that same branch again. Create a new local branch, squash, and push the new squashed branch.)
* **Choose the correct branch**: Assume the current release is 3.0.3, and 3.1.0 is in beta state.
Most pull requests should go against the `3.1` *pre-release branch*, only critical bugfixes
against the `3.0` *release branch*. If you're changing an API or introducing a major feature,
the pull request should go against `master` (read more about our [release process](03_Release_Process.md)). Branches are periodically merged "upwards" (3.0 into 3.1, 3.1 into master).
* **Choose the correct branch**: see [Picking the right version](#picking-the-right-version).
### Editing files directly on GitHub.com

View File

@ -54,13 +54,28 @@ class ConfirmedPasswordField extends FormField {
*/
protected $showOnClick = false;
/**
* Check if the existing password should be entered first
*
* @var bool
*/
protected $requireExistingPassword = false;
/**
* A place to temporarly store the confirm password value
* A place to temporarily store the confirm password value
*
* @var string
*/
protected $confirmValue;
/**
* Store value of "Current Password" field
*
* @var string
*/
protected $currentPasswordValue;
/**
* Title for the link that triggers the visibility of password fields.
*
@ -107,6 +122,7 @@ class ConfirmedPasswordField extends FormField {
// disable auto complete
foreach($this->children as $child) {
/** @var FormField $child */
$child->setAttribute('autocomplete', 'off');
}
@ -115,7 +131,7 @@ class ConfirmedPasswordField extends FormField {
// we have labels for the subfields
$title = false;
parent::__construct($name, $title, null, $form);
parent::__construct($name, $title);
$this->setValue($value);
}
@ -149,6 +165,7 @@ class ConfirmedPasswordField extends FormField {
}
foreach($this->children as $field) {
/** @var FormField $field */
$field->setDisabled($this->isDisabled());
$field->setReadonly($this->isReadonly());
@ -222,6 +239,7 @@ class ConfirmedPasswordField extends FormField {
*/
public function setRightTitle($title) {
foreach($this->children as $field) {
/** @var FormField $field */
$field->setRightTitle($title);
}
@ -229,15 +247,20 @@ class ConfirmedPasswordField extends FormField {
}
/**
* @param array $titles 2 entry array with the customized title for each
* of the 2 children.
* Set child field titles. Titles in order should be:
* - "Current Password" (if getRequireExistingPassword() is set)
* - "Password"
* - "Confirm Password"
*
* @return ConfirmedPasswordField
* @param array $titles List of child titles
* @return $this
*/
public function setChildrenTitles($titles) {
if(is_array($titles) && count($titles) == 2) {
$expectedChildren = $this->getRequireExistingPassword() ? 3 : 2;
if(is_array($titles) && count($titles) == $expectedChildren) {
foreach($this->children as $field) {
if(isset($titles[0])) {
/** @var FormField $field */
$field->setTitle($titles[0]);
array_shift($titles);
@ -253,8 +276,8 @@ class ConfirmedPasswordField extends FormField {
* to handle both cases.
*
* @param mixed $value
*
* @return ConfirmedPasswordField
* @param mixed $data
* @return $this
*/
public function setValue($value, $data = null) {
// If $data is a DataObject, don't use the value, since it's a hashed value
@ -266,6 +289,9 @@ class ConfirmedPasswordField extends FormField {
if(is_array($value)) {
$this->value = $value['_Password'];
$this->confirmValue = $value['_ConfirmPassword'];
$this->currentPasswordValue = ($this->getRequireExistingPassword() && isset($value['_CurrentPassword']))
? $value['_CurrentPassword']
: null;
if($this->showOnClick && isset($value['_PasswordFieldVisible'])) {
$this->children->fieldByName($this->getName() . '[_PasswordFieldVisible]')
@ -294,6 +320,7 @@ class ConfirmedPasswordField extends FormField {
* Update the names of the child fields when updating name of field.
*
* @param string $name new name to give to the field.
* @return $this
*/
public function setName($name) {
$this->children->fieldByName($this->getName() . '[_Password]')
@ -342,8 +369,7 @@ class ConfirmedPasswordField extends FormField {
$validator->validationError(
$name,
_t('Form.VALIDATIONPASSWORDSDONTMATCH',"Passwords don't match"),
"validation",
false
"validation"
);
return false;
@ -355,8 +381,7 @@ class ConfirmedPasswordField extends FormField {
$validator->validationError(
$name,
_t('Form.VALIDATIONPASSWORDSNOTEMPTY', "Passwords can't be empty"),
"validation",
false
"validation"
);
return false;
@ -365,6 +390,8 @@ class ConfirmedPasswordField extends FormField {
// lengths
if(($this->minLength || $this->maxLength)) {
$errorMsg = null;
$limit = null;
if($this->minLength && $this->maxLength) {
$limit = "{{$this->minLength},{$this->maxLength}}";
$errorMsg = _t(
@ -392,8 +419,7 @@ class ConfirmedPasswordField extends FormField {
$validator->validationError(
$name,
$errorMsg,
"validation",
false
"validation"
);
}
}
@ -404,14 +430,56 @@ class ConfirmedPasswordField extends FormField {
$name,
_t('Form.VALIDATIONSTRONGPASSWORD',
"Passwords must have at least one digit and one alphanumeric character"),
"validation",
false
"validation"
);
return false;
}
}
// Check if current password is valid
if(!empty($value) && $this->getRequireExistingPassword()) {
if(!$this->currentPasswordValue) {
$validator->validationError(
$name,
_t(
'ConfirmedPasswordField.CURRENT_PASSWORD_MISSING',
"You must enter your current password."
),
"validation"
);
return false;
}
// Check this password is valid for the current user
$member = Member::currentUser();
if(!$member) {
$validator->validationError(
$name,
_t(
'ConfirmedPasswordField.LOGGED_IN_ERROR',
"You must be logged in to change your password."
),
"validation"
);
return false;
}
// With a valid user and password, check the password is correct
$checkResult = $member->checkPassword($this->currentPasswordValue);
if(!$checkResult->valid()) {
$validator->validationError(
$name,
_t(
'ConfirmedPasswordField.CURRENT_PASSWORD_ERROR',
"The current password you have entered is not correct."
),
"validation"
);
return false;
}
}
return true;
}
@ -444,4 +512,36 @@ class ConfirmedPasswordField extends FormField {
return $field;
}
/**
* Check if existing password is required
*
* @return bool
*/
public function getRequireExistingPassword() {
return $this->requireExistingPassword;
}
/**
* Set if the existing password should be required
*
* @param bool $show Flag to show or hide this field
* @return $this
*/
public function setRequireExistingPassword($show) {
// Don't modify if already added / removed
if((bool)$show === $this->requireExistingPassword) {
return $this;
}
$this->requireExistingPassword = $show;
$name = $this->getName();
$currentName = "{$name}[_CurrentPassword]";
if ($show) {
$confirmField = PasswordField::create($currentName, _t('Member.CURRENT_PASSWORD', 'Current Password'));
$this->children->unshift($confirmField);
} else {
$this->children->removeByName($currentName, true);
}
return $this;
}
}

View File

@ -184,6 +184,11 @@ abstract class SelectField extends FormField {
return true;
}
// Safety check against casting arrays as strings in PHP>5.4
if(is_array($dataValue) || is_array($userValue)) {
return false;
}
// For non-falsey values do loose comparison
if($dataValue) {
return $dataValue == $userValue;

View File

@ -149,7 +149,7 @@ class GridFieldExportButton implements GridField_HTMLProvider, GridField_ActionP
} else {
$value = $gridField->getDataFieldValue($item, $columnSource);
if(!$value) {
if($value === null) {
$value = $gridField->getDataFieldValue($item, $columnHeader);
}
}

View File

@ -1038,7 +1038,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* Doesn't write to the database. Only sets fields as changed
* if they are not already marked as changed.
*
* @return DataObject $this
* @return $this
*/
public function forceChange() {
// Ensure lazy fields loaded
@ -1233,22 +1233,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @param bool $forceChanges If set to true, force all fields to be treated as changed
* @return bool True if any changes are detected
*/
protected function updateChanges($forceChanges = false) {
// Update the changed array with references to changed obj-fields
foreach($this->record as $field => $value) {
// Only mark ID as changed if $forceChanges
if($field === 'ID' && !$forceChanges) continue;
// Determine if this field should be forced, or can mark itself, changed
if($forceChanges
|| !$this->isInDB()
|| (is_object($value) && method_exists($value, 'isChanged') && $value->isChanged())
) {
$this->changed[$field] = self::CHANGE_VALUE;
protected function updateChanges($forceChanges = false)
{
if($forceChanges) {
// Force changes, but only for loaded fields
foreach($this->record as $field => $value) {
$this->changed[$field] = static::CHANGE_VALUE;
}
return true;
}
// Check changes exist, abort if there are no changes
return $this->changed && (bool)array_filter($this->changed);
return $this->isChanged();
}
/**
@ -1383,7 +1377,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$isNewRecord = !$this->isInDB() || $forceInsert;
// Check changes exist, abort if there are none
$hasChanges = $this->updateChanges($forceInsert);
$hasChanges = $this->updateChanges($isNewRecord);
if($hasChanges || $forceWrite || $isNewRecord) {
// New records have their insert into the base data table done first, so that they can pass the
// generated primary key on to the rest of the manipulation
@ -2476,9 +2470,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* Loads all the stub fields that an initial lazy load didn't load fully.
*
* @param string $tableClass Base table to load the values from. Others are joined as required.
* Not specifying a tableClass will load all lazy fields from all tables.
* Not specifying a tableClass will load all lazy fields from all tables.
* @return bool Flag if lazy loading succeeded
*/
protected function loadLazyFields($tableClass = null) {
if(!$this->isInDB() || !is_numeric($this->ID)) {
return false;
}
if (!$tableClass) {
$loaded = array();
@ -2489,7 +2488,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
}
return;
return false;
}
$dataQuery = new DataQuery($tableClass);
@ -2501,11 +2500,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
}
// TableField sets the record ID to "new" on new row data, so don't try doing anything in that case
if(!is_numeric($this->record['ID'])) {
return;
}
// Limit query to the current record, unless it has the Versioned extension,
// in which case it requires special handling through augmentLoadLazyFields()
$baseTable = ClassInfo::baseDataClass($this);
@ -2551,6 +2545,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
}
}
return true;
}
/**
@ -2624,7 +2619,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return boolean
*/
public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT) {
$fields = $fieldName ? array($fieldName) : false;
$fields = $fieldName ? array($fieldName) : true;
$changed = $this->getChangedFields($fields, $changeLevel);
if(!isset($fieldName)) {
return !empty($changed);

View File

@ -14,7 +14,7 @@ use i18n;
*/
class DBLocale extends DBVarchar {
public function __construct($name, $size = 16) {
public function __construct($name = null, $size = 16) {
parent::__construct($name, $size);
}

View File

@ -1,9 +1,14 @@
<?php
/**
* DataObjects that use the Hierarchy extension can be be organised as a hierarchy, with children and parents.
* The most obvious example of this is SiteTree.
* DataObjects that use the Hierarchy extension can be be organised as a hierarchy, with children and parents. The most
* obvious example of this is SiteTree.
*
* @package framework
* @subpackage model
*
* @property int ParentID
* @property DataObject owner
* @method DataObject Parent
*/
class Hierarchy extends DataExtension {
@ -11,30 +16,28 @@ class Hierarchy extends DataExtension {
protected $markingFilter;
/**
* @var Int
*/
/** @var int */
protected $_cache_numChildren;
/**
* The lower bounds for the amount of nodes to mark. If set, the logic will expand nodes until it reaches at least
* this number, and then stops. Root nodes will always show regardless of this settting. Further nodes can be
* lazy-loaded via ajax. This isn't a hard limit. Example: On a value of 10, with 20 root nodes, each having 30
* children, the actual node count will be 50 (all root nodes plus first expanded child).
*
* @config
* @var integer The lower bounds for the amount of nodes to mark. If set, the logic will expand
* nodes until it reaches at least this number, and then stops. Root nodes will always
* show regardless of this settting. Further nodes can be lazy-loaded via ajax.
* This isn't a hard limit. Example: On a value of 10, with 20 root nodes, each having
* 30 children, the actual node count will be 50 (all root nodes plus first expanded child).
* @var int
*/
private static $node_threshold_total = 50;
/**
* Limit on the maximum children a specific node can display. Serves as a hard limit to avoid exceeding available
* server resources in generating the tree, and browser resources in rendering it. Nodes with children exceeding
* this value typically won't display any children, although this is configurable through the $nodeCountCallback
* parameter in {@link getChildrenAsUL()}. "Root" nodes will always show all children, regardless of this setting.
*
* @config
* @var integer Limit on the maximum children a specific node can display.
* Serves as a hard limit to avoid exceeding available server resources
* in generating the tree, and browser resources in rendering it.
* Nodes with children exceeding this value typically won't display
* any children, although this is configurable through the $nodeCountCallback
* parameter in {@link getChildrenAsUL()}. "Root" nodes will always show
* all children, regardless of this setting.
* @var int
*/
private static $node_threshold_leaf = 250;
@ -46,6 +49,8 @@ class Hierarchy extends DataExtension {
/**
* Validate the owner object - check for existence of infinite loops.
*
* @param ValidationResult $validationResult
*/
public function validate(ValidationResult $validationResult) {
// The object is new, won't be looping.
@ -78,19 +83,21 @@ class Hierarchy extends DataExtension {
}
/**
* Returns the children of this DataObject as an XHTML UL. This will be called recursively on each child,
* so if they have children they will be displayed as a UL inside a LI.
* @param string $attributes Attributes to add to the UL.
* @param string|callable $titleEval PHP code to evaluate to start each child - this should include '<li>'
* @param string $extraArg Extra arguments that will be passed on to children, for if they overload this function.
* @param boolean $limitToMarked Display only marked children.
* @param string $childrenMethod The name of the method used to get children from each object
* @param boolean $rootCall Set to true for this first call, and then to false for calls inside the recursion. You
* should not change this.
* @param int $nodeCountThreshold See {@link self::$node_threshold_total}
* @param callable $nodeCountCallback Called with the node count, which gives the callback an opportunity
* to intercept the query. Useful e.g. to avoid excessive children listings
* (Arguments: $parent, $numChildren)
* Returns the children of this DataObject as an XHTML UL. This will be called recursively on each child, so if they
* have children they will be displayed as a UL inside a LI.
*
* @param string $attributes Attributes to add to the UL
* @param string|callable $titleEval PHP code to evaluate to start each child - this should include '<li>'
* @param string $extraArg Extra arguments that will be passed on to children, for if they
* overload this function
* @param bool $limitToMarked Display only marked children
* @param string $childrenMethod The name of the method used to get children from each object
* @param bool $rootCall Set to true for this first call, and then to false for calls inside
* the recursion. You should not change this.
* @param int $nodeCountThreshold See {@link self::$node_threshold_total}
* @param callable $nodeCountCallback Called with the node count, which gives the callback an opportunity to
* intercept the query. Useful e.g. to avoid excessive children listings
* (Arguments: $parent, $numChildren)
*
* @return string
*/
@ -175,11 +182,12 @@ class Hierarchy extends DataExtension {
/**
* Mark a segment of the tree, by calling mark().
* The method performs a breadth-first traversal until the number of nodes is more than minCount.
* This is used to get a limited number of tree nodes to show in the CMS initially.
*
* This method returns the number of nodes marked. After this method is called other methods
* can check isExpanded() and isMarked() on individual nodes.
* The method performs a breadth-first traversal until the number of nodes is more than minCount. This is used to
* get a limited number of tree nodes to show in the CMS initially.
*
* This method returns the number of nodes marked. After this method is called other methods can check
* {@link isExpanded()} and {@link isMarked()} on individual nodes.
*
* @param int $nodeCountThreshold See {@link getChildrenAsUL()}
* @return int The actual number of nodes marked.
@ -205,9 +213,10 @@ class Hierarchy extends DataExtension {
}
/**
* Filter the marking to only those object with $node->$parameterName = $parameterValue
* @param string $parameterName The parameter on each node to check when marking.
* @param mixed $parameterValue The value the parameter must be to be marked.
* Filter the marking to only those object with $node->$parameterName == $parameterValue
*
* @param string $parameterName The parameter on each node to check when marking.
* @param mixed $parameterValue The value the parameter must be to be marked.
*/
public function setMarkingFilter($parameterName, $parameterValue) {
$this->markingFilter = array(
@ -217,9 +226,10 @@ class Hierarchy extends DataExtension {
}
/**
* Filter the marking to only those where the function returns true.
* The node in question will be passed to the function.
* @param string $funcName The function name.
* Filter the marking to only those where the function returns true. The node in question will be passed to the
* function.
*
* @param string $funcName The name of the function to call
*/
public function setMarkingFilterFunction($funcName) {
$this->markingFilter = array(
@ -229,8 +239,9 @@ class Hierarchy extends DataExtension {
/**
* Returns true if the marking filter matches on the given node.
* @param DataObject $node Node to check.
* @return boolean
*
* @param DataObject $node Node to check
* @return bool
*/
public function markingFilterMatches($node) {
if(!$this->markingFilter) {
@ -257,7 +268,11 @@ class Hierarchy extends DataExtension {
/**
* Mark all children of the given node that match the marking filter.
* @param DataObject $node Parent node.
*
* @param DataObject $node Parent node
* @param mixed $context
* @param string $childrenMethod The name of the instance method to call to get the object's list of children
* @param string $numChildrenMethod The name of the instance method to call to count the object's children
* @return DataList
*/
public function markChildren($node, $context = null, $childrenMethod = "AllChildrenIncludingDeleted",
@ -288,8 +303,10 @@ class Hierarchy extends DataExtension {
}
/**
* Ensure marked nodes that have children are also marked expanded.
* Call this after marking but before iterating over the tree.
* Ensure marked nodes that have children are also marked expanded. Call this after marking but before iterating
* over the tree.
*
* @param string $numChildrenMethod The name of the instance method to call to count the object's children
*/
protected function markingFinished($numChildrenMethod = "numChildren") {
// Mark childless nodes as expanded.
@ -303,9 +320,10 @@ class Hierarchy extends DataExtension {
}
/**
* Return CSS classes of 'unexpanded', 'closed', both, or neither, as well as a
* 'jstree-*' state depending on the marking of this DataObject.
* Return CSS classes of 'unexpanded', 'closed', both, or neither, as well as a 'jstree-*' state depending on the
* marking of this DataObject.
*
* @param string $numChildrenMethod The name of the instance method to call to count the object's children
* @return string
*/
public function markingClasses($numChildrenMethod="numChildren") {
@ -327,8 +345,10 @@ class Hierarchy extends DataExtension {
/**
* Mark the children of the DataObject with the given ID.
* @param int $id ID of parent node.
* @param boolean $open If this is true, mark the parent node as opened.
*
* @param int $id ID of parent node
* @param bool $open If this is true, mark the parent node as opened
* @return bool
*/
public function markById($id, $open = false) {
if(isset($this->markedNodes[$id])) {
@ -344,6 +364,7 @@ class Hierarchy extends DataExtension {
/**
* Expose the given object in the tree, by marking this page and all it ancestors.
*
* @param DataObject $childObj
*/
public function markToExpose($childObj) {
@ -356,7 +377,9 @@ class Hierarchy extends DataExtension {
}
/**
* Return the IDs of all the marked nodes
* Return the IDs of all the marked nodes.
*
* @return array
*/
public function markedNodeIDs() {
return array_keys($this->markedNodes);
@ -364,7 +387,8 @@ class Hierarchy extends DataExtension {
/**
* Return an array of this page and its ancestors, ordered item -> root.
* @return array
*
* @return SiteTree[]
*/
public function parentStack() {
$p = $this->owner;
@ -378,20 +402,20 @@ class Hierarchy extends DataExtension {
}
/**
* True if this DataObject is marked.
* @var boolean
* Cache of DataObjects' marked statuses: [ClassName][ID] = bool
* @var array
*/
protected static $marked = array();
/**
* True if this DataObject is expanded.
* @var boolean
* Cache of DataObjects' expanded statuses: [ClassName][ID] = bool
* @var array
*/
protected static $expanded = array();
/**
* True if this DataObject is opened.
* @var boolean
* Cache of DataObjects' opened statuses: [ClassName][ID] = bool
* @var array
*/
protected static $treeOpened = array();
@ -430,7 +454,8 @@ class Hierarchy extends DataExtension {
/**
* Check if this DataObject is marked.
* @return boolean
*
* @return bool
*/
public function isMarked() {
$baseClass = ClassInfo::baseDataClass($this->owner->class);
@ -440,7 +465,8 @@ class Hierarchy extends DataExtension {
/**
* Check if this DataObject is expanded.
* @return boolean
*
* @return bool
*/
public function isExpanded() {
$baseClass = ClassInfo::baseDataClass($this->owner->class);
@ -450,6 +476,8 @@ class Hierarchy extends DataExtension {
/**
* Check if this DataObject's tree is opened.
*
* @return bool
*/
public function isTreeOpened() {
$baseClass = ClassInfo::baseDataClass($this->owner->class);
@ -459,7 +487,8 @@ class Hierarchy extends DataExtension {
/**
* Get a list of this DataObject's and all it's descendants IDs.
* @return int
*
* @return int[]
*/
public function getDescendantIDList() {
$idList = array();
@ -468,8 +497,9 @@ class Hierarchy extends DataExtension {
}
/**
* Get a list of this DataObject's and all it's descendants ID, and put it in $idList.
* @var array $idList Array to put results in.
* Get a list of this DataObject's and all it's descendants ID, and put them in $idList.
*
* @param array $idList Array to put results in.
*/
public function loadDescendantIDListInto(&$idList) {
if($children = $this->AllChildren()) {
@ -488,7 +518,8 @@ class Hierarchy extends DataExtension {
/**
* Get the children for this DataObject.
* @return ArrayList
*
* @return DataList
*/
public function Children() {
if(!(isset($this->_cache_children) && $this->_cache_children)) {
@ -506,7 +537,8 @@ class Hierarchy extends DataExtension {
/**
* Return all children, including those 'not in menus'.
* @return SS_List
*
* @return DataList
*/
public function AllChildren() {
return $this->owner->stageChildren(true);
@ -514,11 +546,13 @@ class Hierarchy extends DataExtension {
/**
* Return all children, including those that have been deleted but are still in live.
* Deleted children will be marked as "DeletedFromStage"
* Added children will be marked as "AddedToStage"
* Modified children will be marked as "ModifiedOnStage"
* Everything else has "SameOnStage" set, as an indicator that this information has been looked up.
* @return SS_List
* - Deleted children will be marked as "DeletedFromStage"
* - Added children will be marked as "AddedToStage"
* - Modified children will be marked as "ModifiedOnStage"
* - Everything else has "SameOnStage" set, as an indicator that this information has been looked up.
*
* @param mixed $context
* @return ArrayList
*/
public function AllChildrenIncludingDeleted($context = null) {
return $this->doAllChildrenIncludingDeleted($context);
@ -527,8 +561,8 @@ class Hierarchy extends DataExtension {
/**
* @see AllChildrenIncludingDeleted
*
* @param unknown_type $context
* @return SS_List
* @param mixed $context
* @return ArrayList
*/
public function doAllChildrenIncludingDeleted($context = null) {
if(!$this->owner) user_error('Hierarchy::doAllChildrenIncludingDeleted() called without $this->owner');
@ -560,8 +594,10 @@ class Hierarchy extends DataExtension {
}
/**
* Return all the children that this page had, including pages that were deleted
* from both stage & live.
* Return all the children that this page had, including pages that were deleted from both stage & live.
*
* @return DataList
* @throws Exception
*/
public function AllHistoricalChildren() {
if(!$this->owner->hasExtension('Versioned')) {
@ -574,7 +610,10 @@ class Hierarchy extends DataExtension {
}
/**
* Return the number of children that this page ever had, including pages that were deleted
* Return the number of children that this page ever had, including pages that were deleted.
*
* @return int
* @throws Exception
*/
public function numHistoricalChildren() {
if(!$this->owner->hasExtension('Versioned')) {
@ -586,11 +625,10 @@ class Hierarchy extends DataExtension {
}
/**
* Return the number of direct children.
* By default, values are cached after the first invocation.
* Can be augumented by {@link augmentNumChildrenCountQuery()}.
* Return the number of direct children. By default, values are cached after the first invocation. Can be
* augumented by {@link augmentNumChildrenCountQuery()}.
*
* @param Boolean $cache
* @param bool $cache Whether to retrieve values from cache
* @return int
*/
public function numChildren($cache = true) {
@ -606,10 +644,10 @@ class Hierarchy extends DataExtension {
}
/**
* Return children from the stage site
* Return children in the stage site.
*
* @param showAll Inlcude all of the elements, even those not shown in the menus.
* (only applicable when extension is applied to {@link SiteTree}).
* @param bool $showAll Include all of the elements, even those not shown in the menus. Only applicable when
* extension is applied to {@link SiteTree}.
* @return DataList
*/
public function stageChildren($showAll = false) {
@ -625,12 +663,13 @@ class Hierarchy extends DataExtension {
}
/**
* Return children from the live site, if it exists.
* Return children in the live site, if it exists.
*
* @param boolean $showAll Include all of the elements, even those not shown in the menus.
* (only applicable when extension is applied to {@link SiteTree}).
* @param boolean $onlyDeletedFromStage Only return items that have been deleted from stage
* @return SS_List
* @param bool $showAll Include all of the elements, even those not shown in the menus. Only
* applicable when extension is applied to {@link SiteTree}.
* @param bool $onlyDeletedFromStage Only return items that have been deleted from stage
* @return DataList
* @throws Exception
*/
public function liveChildren($showAll = false, $onlyDeletedFromStage = false) {
if(!$this->owner->hasExtension('Versioned')) {
@ -652,7 +691,10 @@ class Hierarchy extends DataExtension {
}
/**
* Get the parent of this class.
* Get this object's parent, optionally filtered by an SQL clause. If the clause doesn't match the parent, nothing
* is returned.
*
* @param string $filter
* @return DataObject
*/
public function getParent($filter = null) {
@ -669,7 +711,7 @@ class Hierarchy extends DataExtension {
/**
* Return all the parents of this class in a set ordered from the lowest to highest parent.
*
* @return SS_List
* @return ArrayList
*/
public function getAncestors() {
$ancestors = new ArrayList();
@ -683,11 +725,10 @@ class Hierarchy extends DataExtension {
}
/**
* Returns a human-readable, flattened representation of the path to the object,
* using its {@link Title()} attribute.
* Returns a human-readable, flattened representation of the path to the object, using its {@link Title} attribute.
*
* @param String
* @return String
* @param string $separator
* @return string
*/
public function getBreadcrumbs($separator = ' &raquo; ') {
$crumbs = array();
@ -702,22 +743,25 @@ class Hierarchy extends DataExtension {
* then search the parents.
*
* @todo Write!
*
* @param string $className Class name of the node to find
* @param DataObject $afterNode Used for recursive calls to this function
* @return DataObject
*/
public function naturalPrev( $className, $afterNode = null ) {
public function naturalPrev($className, $afterNode = null ) {
return null;
}
/**
* Get the next node in the tree of the type. If there is no instance of the className descended from this node,
* then search the parents.
* @param string $className Class name of the node to find.
* @param string|int $root ID/ClassName of the node to limit the search to
* @param DataObject afterNode Used for recursive calls to this function
* @param string $className Class name of the node to find.
* @param string|int $root ID/ClassName of the node to limit the search to
* @param DataObject $afterNode Used for recursive calls to this function
* @return DataObject
*/
public function naturalNext( $className = null, $root = 0, $afterNode = null ) {
// If this node is not the node we are searching from, then we can possibly return this
// node as a solution
public function naturalNext($className = null, $root = 0, $afterNode = null ) {
// If this node is not the node we are searching from, then we can possibly return this node as a solution
if($afterNode && $afterNode->ID != $this->owner->ID) {
if(!$className || ($className && $this->owner->class == $className)) {
return $this->owner;
@ -761,6 +805,14 @@ class Hierarchy extends DataExtension {
return null;
}
/**
* Flush all Hierarchy caches:
* - Children (instance)
* - NumChildren (instance)
* - Marked (global)
* - Expanded (global)
* - TreeOpened (global)
*/
public function flushCache() {
$this->_cache_children = null;
$this->_cache_numChildren = null;
@ -769,6 +821,12 @@ class Hierarchy extends DataExtension {
self::$treeOpened = array();
}
/**
* Reset global Hierarchy caches:
* - Marked
* - Expanded
* - TreeOpened
*/
public static function reset() {
self::$marked = array();
self::$expanded = array();

View File

@ -650,9 +650,8 @@ abstract class SQLConditionalExpression extends SQLExpression {
* @return boolean
*/
public function filtersOnID() {
$regexp = '/^(.*\.)?("|`)?ID("|`)?\s?=/';
$regexp = '/^(.*\.)?("|`)?ID("|`)?\s?(=|IN)/';
// @todo - Test this works with paramaterised queries
foreach($this->getWhereParameterised($parameters) as $predicate) {
if(preg_match($regexp, $predicate)) return true;
}
@ -668,7 +667,7 @@ abstract class SQLConditionalExpression extends SQLExpression {
* @return boolean
*/
public function filtersOnFK() {
$regexp = '/^(.*\.)?("|`)?[a-zA-Z]+ID("|`)?\s?=/';
$regexp = '/^(.*\.)?("|`)?[a-zA-Z]+ID("|`)?\s?(=|IN)/';
// @todo - Test this works with paramaterised queries
foreach($this->getWhereParameterised($parameters) as $predicate) {

View File

@ -290,15 +290,23 @@ class Member extends DataObject implements TemplateGlobalProvider {
/**
* Check if the passed password matches the stored one (if the member is not locked out).
*
* @param string $password
* @param string $password
* @return ValidationResult
*/
public function checkPassword($password) {
$result = $this->canLogIn();
// Short-circuit the result upon failure, no further checks needed.
if (!$result->valid()) return $result;
if (!$result->valid()) {
return $result;
}
// Allow default admin to login as self
if($this->isDefaultAdmin() && Security::check_default_admin($this->Email, $password)) {
return $result;
}
// Check a password is set on this member
if(empty($this->Password) && $this->exists()) {
$result->error(_t('Member.NoPassword','There is no password on this member.'));
return $result;
@ -315,6 +323,16 @@ class Member extends DataObject implements TemplateGlobalProvider {
return $result;
}
/**
* Check if this user is the currently configured default admin
*
* @return bool
*/
public function isDefaultAdmin() {
return Security::has_default_admin()
&& $this->Email === Security::default_admin_username();
}
/**
* Returns a valid {@link ValidationResult} if this member can currently log in, or an invalid
* one with error messages to display if the member is locked out.
@ -725,14 +743,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
public function getMemberFormFields() {
$fields = parent::getFrontendFields();
$fields->replaceField('Password', $password = new ConfirmedPasswordField (
'Password',
$this->fieldLabel('Password'),
null,
null,
(bool) $this->ID
));
$password->setCanBeEmpty(true);
$fields->replaceField('Password', $this->getMemberPasswordField());
$fields->replaceField('Locale', new DropdownField (
'Locale',
@ -748,6 +759,36 @@ class Member extends DataObject implements TemplateGlobalProvider {
return $fields;
}
/**
* Builds "Change / Create Password" field for this member
*
* @return ConfirmedPasswordField
*/
public function getMemberPasswordField() {
$editingPassword = $this->isInDB();
$label = $editingPassword
? _t('Member.EDIT_PASSWORD', 'New Password')
: $this->fieldLabel('Password');
/** @var ConfirmedPasswordField $password */
$password = ConfirmedPasswordField::create(
'Password',
$label,
null,
null,
$editingPassword
);
// If editing own password, require confirmation of existing
if($editingPassword && $this->ID == Member::currentUserID()) {
$password->setRequireExistingPassword(true);
}
$password->setCanBeEmpty(true);
$this->extend('updateMemberPasswordField', $password);
return $password;
}
/**
* Returns the {@link RequiredFields} instance for the Member object. This
* Validator is used when saving a {@link CMSProfileController} or added to
@ -1337,19 +1378,12 @@ class Member extends DataObject implements TemplateGlobalProvider {
require_once 'Zend/Date.php';
$self = $this;
$this->beforeUpdateCMSFields(function($fields) use ($self) {
$mainFields = $fields->fieldByName("Root")->fieldByName("Main")->Children;
$this->beforeUpdateCMSFields(function(FieldList $fields) use ($self) {
/** @var FieldList $mainFields */
$mainFields = $fields->fieldByName("Root")->fieldByName("Main")->getChildren();
$password = new ConfirmedPasswordField(
'Password',
null,
null,
null,
true // showOnClick
);
$password->setCanBeEmpty(true);
if( ! $self->ID) $password->showOnClick = false;
$mainFields->replaceField('Password', $password);
// Build change password field
$mainFields->replaceField('Password', $self->getMemberPasswordField());
$mainFields->replaceField('Locale', new DropdownField(
"Locale",

View File

@ -19,9 +19,18 @@ Feature: Manage my own settings
Then I should see "Jack"
And I should see "Johnson"
Scenario: I can't reset the password without the original
Given I follow "Change Password"
And I fill in "Current Password" with "idontknow"
And I fill in "New Password" with "newsecret"
And I fill in "Confirm Password" with "newsecret"
And I press the "Save" button
Then I should see "The current password you have entered is not correct."
Scenario: I can change my password
Given I follow "Change Password"
And I fill in "Password" with "newsecret"
And I fill in "Current Password" with "secret"
And I fill in "New Password" with "newsecret"
And I fill in "Confirm Password" with "newsecret"
And I press the "Save" button
And I am not logged in
@ -35,4 +44,4 @@ Feature: Manage my own settings
Then I should see "Sprache"
# TODO Date/time format - Difficult because its not exposed anywhere in the CMS?
# TODO Group modification as ADMIN user
# TODO Group modification as ADMIN user

View File

@ -8,6 +8,30 @@
*/
class ErrorControlChainTest_Chain extends ErrorControlChain {
protected $displayErrors = 'STDERR';
/**
* Modify method visibility to public for testing
*
* @return string
*/
public function getDisplayErrors()
{
// Protect manipulation of underlying php_ini values
return $this->displayErrors;
}
/**
* Modify method visibility to public for testing
*
* @param mixed $errors
*/
public function setDisplayErrors($errors)
{
// Protect manipulation of underlying php_ini values
$this->displayErrors = $errors;
}
// Change function visibility to be testable directly
public function translateMemstring($memstring) {
return parent::translateMemstring($memstring);
@ -63,10 +87,7 @@ require_once '$classpath';
class ErrorControlChainTest extends SapphireTest {
protected $displayErrors = null;
function setUp() {
$this->displayErrors = (bool)ini_get('display_errors');
// Check we can run PHP at all
$null = is_writeable('/dev/null') ? '/dev/null' : 'NUL';
@ -79,50 +100,55 @@ class ErrorControlChainTest extends SapphireTest {
parent::setUp();
}
public function tearDown() {
if($this->displayErrors !== null) {
ini_set('display_errors', $this->displayErrors);
$this->displayErrors = null;
}
parent::tearDown(); // TODO: Change the autogenerated stub
}
function testErrorSuppression() {
// Errors disabled by default
ini_set('display_errors', false);
$chain = new ErrorControlChain();
$chain = new ErrorControlChainTest_Chain();
$chain->setDisplayErrors('Off'); // mocks display_errors: Off
$initialValue = null;
$whenNotSuppressed = null;
$whenSuppressed = null;
$chain->then(function($chain) use(&$whenNotSuppressed, &$whenSuppressed) {
$chain->setSuppression(true);
$whenSuppressed = ini_get('display_errors');
$chain->setSuppression(false);
$whenNotSuppressed = ini_get('display_errors');
})->execute();
$chain->then(
function(ErrorControlChainTest_Chain $chain)
use(&$initialValue, &$whenNotSuppressed, &$whenSuppressed) {
$initialValue = $chain->getDisplayErrors();
$chain->setSuppression(false);
$whenNotSuppressed = $chain->getDisplayErrors();
$chain->setSuppression(true);
$whenSuppressed = $chain->getDisplayErrors();
}
)->execute();
// Disabled errors never un-disable
$this->assertFalse((bool)$whenNotSuppressed);
$this->assertFalse((bool)$whenSuppressed);
$this->assertEquals(0, $initialValue); // Chain starts suppressed
$this->assertEquals(0, $whenSuppressed); // false value used internally when suppressed
$this->assertEquals('Off', $whenNotSuppressed); // false value set by php ini when suppression lifted
$this->assertEquals('Off', $chain->getDisplayErrors()); // Correctly restored after run
// Errors enabled by default
ini_set('display_errors', true);
$chain = new ErrorControlChain();
$chain = new ErrorControlChainTest_Chain();
$chain->setDisplayErrors('Yes'); // non-falsey ini value
$initialValue = null;
$whenNotSuppressed = null;
$whenSuppressed = null;
$chain->then(function($chain) use(&$whenNotSuppressed, &$whenSuppressed) {
$chain->setSuppression(true);
$whenSuppressed = ini_get('display_errors');
$chain->setSuppression(false);
$whenNotSuppressed = ini_get('display_errors');
})->execute();
$chain->then(
function(ErrorControlChainTest_Chain $chain)
use(&$initialValue, &$whenNotSuppressed, &$whenSuppressed) {
$initialValue = $chain->getDisplayErrors();
$chain->setSuppression(true);
$whenSuppressed = $chain->getDisplayErrors();
$chain->setSuppression(false);
$whenNotSuppressed = $chain->getDisplayErrors();
}
)->execute();
// Errors can be suppressed an un-suppressed when initially enabled
$this->assertTrue((bool)$whenNotSuppressed);
$this->assertFalse((bool)$whenSuppressed);
$this->assertEquals(0, $initialValue); // Chain starts suppressed
$this->assertEquals(0, $whenSuppressed); // false value used internally when suppressed
$this->assertEquals('Yes', $whenNotSuppressed); // false value set by php ini when suppression lifted
$this->assertEquals('Yes', $chain->getDisplayErrors()); // Correctly restored after run
// Fatal error
$chain = new ErrorControlChainTest_Chain();
list($out, $code) = $chain

View File

@ -254,6 +254,32 @@ class DropdownFieldTest extends SapphireTest {
$this->assertEquals(count($disabledOptions), 0, 'There are no disabled options');
}
/**
* The Field() method should be able to handle arrays as values in an edge case. If it couldn't handle it then
* this test would trigger an array to string conversion PHP notice
*
* @dataProvider arrayValueProvider
*/
public function testDropdownWithArrayValues($value) {
$field = $this->createDropdownField();
$field->setValue($value);
$this->assertInstanceOf('SilverStripe\Model\FieldType\DBHTMLText', $field->Field());
$this->assertSame($value, $field->Value());
}
/**
* @return array
*/
public function arrayValueProvider() {
return array(
array(array()),
array(array(0)),
array(array(123)),
array(array('string')),
array('Regression-ish test.')
);
}
/**
* Create a test dropdown field, with the option to
* set what source and blank value it should contain

View File

@ -137,6 +137,18 @@ class GridFieldExportButtonTest extends SapphireTest {
$button->generateExportFileData($this->gridField)
);
}
public function testZeroValue() {
$button = new GridFieldExportButton();
$button->setExportColumns(array(
'RugbyTeamNumber' => 'Rugby Team Number'
));
$this->assertEquals(
"\"Rugby Team Number\"\n2\n0\n",
$button->generateExportFileData($this->gridField)
);
}
}
/**
@ -147,7 +159,8 @@ class GridFieldExportButtonTest_Team extends DataObject implements TestOnly {
private static $db = array(
'Name' => 'Varchar',
'City' => 'Varchar'
'City' => 'Varchar',
'RugbyTeamNumber' => 'Int'
);
public function canView($member = null) {
@ -164,7 +177,8 @@ class GridFieldExportButtonTest_NoView extends DataObject implements TestOnly {
private static $db = array(
'Name' => 'Varchar',
'City' => 'Varchar'
'City' => 'Varchar',
'RugbyTeamNumber' => 'Int'
);
public function canView($member = null) {

View File

@ -2,9 +2,11 @@ GridFieldExportButtonTest_Team:
test-team-1:
Name: Test
City: City
RugbyTeamNumber: 2
test-team-2:
Name: Test2
City: 'Quoted "City" 2'
RugbyTeamNumber: 0
GridFieldExportButtonTest_NoView:
item1:
Name: Foo

View File

@ -74,6 +74,35 @@ class DBMoneyTest extends SapphireTest {
$this->assertEquals(0.0000, $moneyTest->MyMoneyAmount);
}
public function testIsChanged() {
$obj1 = $this->objFromFixture('MoneyTest_DataObject', 'test1');
$this->assertFalse($obj1->isChanged());
$this->assertFalse($obj1->isChanged('MyMoney'));
// modify non-db field
$m1 = new DBMoney();
$m1->setAmount(500);
$m1->setCurrency('NZD');
$obj1->NonDBMoneyField = $m1;
$this->assertFalse($obj1->isChanged()); // Because only detects DB fields
$this->assertTrue($obj1->isChanged('NonDBMoneyField')); // Allow change detection to non-db fields explicitly named
// Modify db field
$obj2 = $this->objFromFixture('MoneyTest_DataObject', 'test2');
$m2 = new DBMoney();
$m2->setAmount(500);
$m2->setCurrency('NZD');
$obj2->MyMoney = $m2;
$this->assertTrue($obj2->isChanged()); // Detects change to DB field
$this->assertTrue($obj2->ischanged('MyMoney'));
// Modify sub-fields
$obj3 = $this->objFromFixture('MoneyTest_DataObject', 'test3');
$obj3->MyMoneyCurrency = 'USD';
$this->assertTrue($obj3->isChanged()); // Detects change to DB field
$this->assertTrue($obj3->ischanged('MyMoneyCurrency'));
}
/**
* Write a Money object to the database, then re-read it to ensure it
* is re-read properly.

View File

@ -565,29 +565,29 @@ class DataObjectTest extends SapphireTest {
$obj->IsRetired = true;
$this->assertEquals(
$obj->getChangedFields(false, 1),
$obj->getChangedFields(true, DataObject::CHANGE_STRICT),
array(
'FirstName' => array(
'before' => 'Captain',
'after' => 'Captain-changed',
'level' => 2
'level' => DataObject::CHANGE_VALUE
),
'IsRetired' => array(
'before' => 1,
'after' => true,
'level' => 1
'level' => DataObject::CHANGE_STRICT
)
),
'Changed fields are correctly detected with strict type changes (level=1)'
);
$this->assertEquals(
$obj->getChangedFields(false, 2),
$obj->getChangedFields(true, DataObject::CHANGE_VALUE),
array(
'FirstName' => array(
'before'=>'Captain',
'after'=>'Captain-changed',
'level' => 2
'level' => DataObject::CHANGE_VALUE
)
),
'Changed fields are correctly detected while ignoring type changes (level=2)'
@ -596,50 +596,58 @@ class DataObjectTest extends SapphireTest {
$newObj = new DataObjectTest_Player();
$newObj->FirstName = "New Player";
$this->assertEquals(
$newObj->getChangedFields(false, 2),
array(
'FirstName' => array(
'before' => null,
'after' => 'New Player',
'level' => 2
'level' => DataObject::CHANGE_VALUE
)
),
$newObj->getChangedFields(true, DataObject::CHANGE_VALUE),
'Initialised fields are correctly detected as full changes'
);
}
public function testIsChanged() {
$obj = $this->objFromFixture('DataObjectTest_Player', 'captain1');
$obj->NonDBField = 'bob';
$obj->FirstName = 'Captain-changed';
$obj->IsRetired = true; // type change only, database stores "1"
$this->assertTrue($obj->isChanged('FirstName', 1));
$this->assertTrue($obj->isChanged('FirstName', 2));
$this->assertTrue($obj->isChanged('IsRetired', 1));
$this->assertFalse($obj->isChanged('IsRetired', 2));
// Now that DB fields are changed, isChanged is true
$this->assertTrue($obj->isChanged('NonDBField'));
$this->assertFalse($obj->isChanged('NonField'));
$this->assertTrue($obj->isChanged('FirstName', DataObject::CHANGE_STRICT));
$this->assertTrue($obj->isChanged('FirstName', DataObject::CHANGE_VALUE));
$this->assertTrue($obj->isChanged('IsRetired', DataObject::CHANGE_STRICT));
$this->assertFalse($obj->isChanged('IsRetired', DataObject::CHANGE_VALUE));
$this->assertFalse($obj->isChanged('Email', 1), 'Doesnt change mark unchanged property');
$this->assertFalse($obj->isChanged('Email', 2), 'Doesnt change mark unchanged property');
$newObj = new DataObjectTest_Player();
$newObj->FirstName = "New Player";
$this->assertTrue($newObj->isChanged('FirstName', 1));
$this->assertTrue($newObj->isChanged('FirstName', 2));
$this->assertFalse($newObj->isChanged('Email', 1));
$this->assertFalse($newObj->isChanged('Email', 2));
$this->assertTrue($newObj->isChanged('FirstName', DataObject::CHANGE_STRICT));
$this->assertTrue($newObj->isChanged('FirstName', DataObject::CHANGE_VALUE));
$this->assertFalse($newObj->isChanged('Email', DataObject::CHANGE_STRICT));
$this->assertFalse($newObj->isChanged('Email', DataObject::CHANGE_VALUE));
$newObj->write();
$this->assertFalse($newObj->isChanged('FirstName', 1));
$this->assertFalse($newObj->isChanged('FirstName', 2));
$this->assertFalse($newObj->isChanged('Email', 1));
$this->assertFalse($newObj->isChanged('Email', 2));
$this->assertFalse($newObj->ischanged());
$this->assertFalse($newObj->isChanged('FirstName', DataObject::CHANGE_STRICT));
$this->assertFalse($newObj->isChanged('FirstName', DataObject::CHANGE_VALUE));
$this->assertFalse($newObj->isChanged('Email', DataObject::CHANGE_STRICT));
$this->assertFalse($newObj->isChanged('Email', DataObject::CHANGE_VALUE));
$obj = $this->objFromFixture('DataObjectTest_Player', 'captain1');
$obj->FirstName = null;
$this->assertTrue($obj->isChanged('FirstName', 1));
$this->assertTrue($obj->isChanged('FirstName', 2));
$this->assertTrue($obj->isChanged('FirstName', DataObject::CHANGE_STRICT));
$this->assertTrue($obj->isChanged('FirstName', DataObject::CHANGE_VALUE));
/* Test when there's not field provided */
$obj = $this->objFromFixture('DataObjectTest_Player', 'captain1');
$obj = $this->objFromFixture('DataObjectTest_Player', 'captain2');
$this->assertFalse($obj->isChanged());
$obj->NonDBField = 'new value';
$this->assertFalse($obj->isChanged());
$obj->FirstName = "New Player";
$this->assertTrue($obj->isChanged());

View File

@ -1,8 +1,14 @@
MoneyTest_DataObject:
test1:
MyMoneyCurrency: EUR
MyMoneyAmount: 1.23
test1:
MyMoneyCurrency: EUR
MyMoneyAmount: 1.23
test2:
MyMoneyCurrency: USD
MyMoneyAmount: 4.45
test3:
MyMoneyCurrency: NZD
MyMoneyAmount: 7.66
MoneyTest_SubClass:
test2:
MyOtherMoneyCurrency: GBP
MyOtherMoneyAmount: 2.46
test2:
MyOtherMoneyCurrency: GBP
MyOtherMoneyAmount: 2.46

View File

@ -315,6 +315,41 @@ class SQLSelectTest extends SapphireTest {
"filtersOnID() is true with simple unquoted column name"
);
$query = new SQLSelect();
$query->setWhere('"ID" = 5');
$this->assertTrue(
$query->filtersOnID(),
"filtersOnID() is true with simple quoted column name"
);
$query = new SQLSelect();
$query->setWhere(array('"ID"' => 4));
$this->assertTrue(
$query->filtersOnID(),
"filtersOnID() is true with parameterised quoted column name"
);
$query = new SQLSelect();
$query->setWhere(array('"ID" = ?' => 4));
$this->assertTrue(
$query->filtersOnID(),
"filtersOnID() is true with parameterised quoted column name"
);
$query = new SQLSelect();
$query->setWhere('"ID" IN (5,4)');
$this->assertTrue(
$query->filtersOnID(),
"filtersOnID() is true with WHERE ID IN"
);
$query = new SQLSelect();
$query->setWhere(array('"ID" IN ?' => array(1,2)));
$this->assertTrue(
$query->filtersOnID(),
"filtersOnID() is true with parameterised WHERE ID IN"
);
$query = new SQLSelect();
$query->setWhere("ID=5");
$this->assertTrue(