mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
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:
commit
574bc6038b
@ -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)
|
@ -77,12 +77,34 @@ 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
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
protected function updateChanges($forceChanges = false)
|
||||
{
|
||||
if($forceChanges) {
|
||||
// Force changes, but only for loaded 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;
|
||||
$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
|
||||
@ -2477,8 +2471,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
*
|
||||
* @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.
|
||||
* @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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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,18 +83,20 @@ 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.
|
||||
* 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 $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 boolean $rootCall Set to true for this first call, and then to false for calls inside the recursion. You
|
||||
* should not change this.
|
||||
* @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
|
||||
* @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,7 +213,8 @@ class Hierarchy extends DataExtension {
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the marking to only those object with $node->$parameterName = $parameterValue
|
||||
* 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.
|
||||
*/
|
||||
@ -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 = ' » ') {
|
||||
$crumbs = array();
|
||||
@ -702,6 +743,10 @@ 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 ) {
|
||||
return null;
|
||||
@ -712,12 +757,11 @@ class Hierarchy extends DataExtension {
|
||||
* 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 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
|
||||
// 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();
|
||||
|
@ -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) {
|
||||
|
@ -297,8 +297,16 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
$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",
|
||||
|
@ -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
|
||||
|
@ -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->then(
|
||||
function(ErrorControlChainTest_Chain $chain)
|
||||
use(&$initialValue, &$whenNotSuppressed, &$whenSuppressed) {
|
||||
$initialValue = $chain->getDisplayErrors();
|
||||
$chain->setSuppression(false);
|
||||
$whenNotSuppressed = ini_get('display_errors');
|
||||
})->execute();
|
||||
$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->then(
|
||||
function(ErrorControlChainTest_Chain $chain)
|
||||
use(&$initialValue, &$whenNotSuppressed, &$whenSuppressed) {
|
||||
$initialValue = $chain->getDisplayErrors();
|
||||
$chain->setSuppression(true);
|
||||
$whenSuppressed = ini_get('display_errors');
|
||||
$whenSuppressed = $chain->getDisplayErrors();
|
||||
$chain->setSuppression(false);
|
||||
$whenNotSuppressed = ini_get('display_errors');
|
||||
})->execute();
|
||||
$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
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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());
|
||||
|
||||
|
@ -2,6 +2,12 @@ MoneyTest_DataObject:
|
||||
test1:
|
||||
MyMoneyCurrency: EUR
|
||||
MyMoneyAmount: 1.23
|
||||
test2:
|
||||
MyMoneyCurrency: USD
|
||||
MyMoneyAmount: 4.45
|
||||
test3:
|
||||
MyMoneyCurrency: NZD
|
||||
MyMoneyAmount: 7.66
|
||||
MoneyTest_SubClass:
|
||||
test2:
|
||||
MyOtherMoneyCurrency: GBP
|
||||
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user