Merge branch 'silverstripe:4' into 4

This commit is contained in:
Sergey Shevchenko 2021-11-15 22:07:11 +13:00 committed by GitHub
commit 5e26757a9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 517 additions and 138 deletions

View File

@ -20,6 +20,12 @@ jobs:
- PHPUNIT_TEST=1
- PHPUNIT_SUITE="framework"
- COMPOSER_INSTALL_ARG="--prefer-lowest"
- php: 7.4
env:
- DB=PGSQL
- REQUIRE_INSTALLER="$REQUIRE_RECIPE"
- PHPUNIT_TEST=1
- PHPUNIT_SUITE="framework"
- php: 7.4
env:
- DB=MYSQL

View File

@ -53,7 +53,6 @@
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"dms/phpunit-arraysubset-asserts": "^0.3.0",
"silverstripe/versioned": "^1",
"squizlabs/php_codesniffer": "^3.5"
},

View File

@ -10,9 +10,9 @@ Silverstripe CMS needs to be installed on a web server. Content authors and webs
to access a web-based GUI to do their day-to-day work. Website designers and developers require access to the files on
the server to update templates, website logic, and perform upgrades or maintenance.
## PHP
## PHP {php}
* PHP >=7.1
* PHP >=7.3
* PHP extensions: `ctype`, `dom`, `fileinfo`, `hash`, `intl`, `mbstring`, `session`, `simplexml`, `tokenizer`, `xml`
* PHP configuration: `memory_limit` with at least `48M`
* PHP extension for image manipulation: Either `gd` or `imagick`
@ -20,6 +20,8 @@ the server to update templates, website logic, and perform upgrades or maintenan
Use [phpinfo()](http://php.net/manual/en/function.phpinfo.php) to inspect your configuration.
Silverstripe CMS tracks the official [PHP release support timeline](https://www.php.net/supported-versions.php). When a PHP version reaches end-of-life, Silverstripe CMS drops support for it in the next minor release.
## Database
* MySQL >=5.6 (
@ -271,11 +273,13 @@ table may be of use:
| Silverstripe CMS Version | PHP Version | More information |
| -------------------- | ----------- | ---------------- |
| 3.0 - 3.5 | 5.3 - 5.6 | [requirements docs](https://docs.silverstripe.org/en/3.4/getting_started/server_requirements/)
| 3.0 - 3.5 | 5.3 - 5.6 | |
| 3.6 | 5.3 - 7.1 | |
| 3.7 | 5.3 - 7.4 | [changelog](https://docs.silverstripe.org/en/3/changelogs/3.7.4/) |
| 4.0 - 4.4 | 5.6+ | |
| 4.5+ | 7.1+ | [blog post](https://www.silverstripe.org/blog/our-plan-for-ending-php-5-6-support-in-silverstripe-4/) |
| 4.5 - 4.9 | 7.1+ | [blog post](https://www.silverstripe.org/blog/our-plan-for-ending-php-5-6-support-in-silverstripe-4/) |
| 4.10 | 7.3+ | [changelog](/Changelogs/4.10.0#phpeol/) |
| 4.11 + | 7.4+ | [changelog](/Changelogs/4.11.0#phpeol) |
## CMS browser requirements

View File

@ -14,20 +14,6 @@ Modules are [Composer packages](https://getcomposer.org/), and are placed in the
These packages need to contain either a toplevel `_config` directory or `_config.php` file,
as well as a special `type` in their `composer.json` file ([example](https://github.com/silverstripe/silverstripe-module/blob/4/composer.json)).
```
app/
|
+-- _config/
+-- src/
+-- ..
|
vendor/my_vendor/my_module/
|
+-- _config/
+-- composer.json
+-- ...
```
Like with any Composer package, we recommend declaring your PHP classes through
[PSR autoloading](https://getcomposer.org/doc/01-basic-usage.md#autoloading).
Silverstripe CMS will automatically discover templates and configuration settings
@ -77,9 +63,104 @@ or share your code with the community. Silverstripe CMS already
has certain modules included, for example the `cms` module and core functionality such as commenting and spam protection
are also abstracted into modules allowing developers the freedom to choose what they want.
### Create a new directory
The easiest way to get started is our [Module Skeleton](https://github.com/silverstripe/silverstripe-module).
In case you want to share your creation with the community,
read more about [publishing a module](how_tos/publish_a_module).
First, create a new directory named after your intended module in your main project. It should sit alongside the other modules
such as *silverstripe/framework* and *silverstripe/cms* and use it for the module development:
`mkdir /vendor/my_vendor/nice_feature`
Then clone the Module Skeleton to get a headstart with the module files:
```bash
cd /vendor/my_vendor/nice_feature
git clone git@github.com:silverstripe/silverstripe-module.git .
```
### Allow your module to be importable by composer
You need to set your module up to be importable via composer. For this, edit the new `composer.json` file in the root of
your module. Here is an example for a module that builds on the functionality provided by the `blog` main module (hence the
requirement):
```json
{
"name": "my_vendor/nice_feature",
"description": "Short module description",
"type": "silverstripe-vendormodule",
"require": {
"silverstripe/cms": "^4.0",
"silverstripe/framework": "^4.0",
"silverstripe/blog": "^4@dev"
}
}
```
After your module is running and tested, you can publish it. Since your module is a self-contained piece of software, it
will constitute a project in itself. The below assumes you are using GitHub and have already created a new GitHub repository for this module.
Push your module upstream to the empty repository just created:
```bash
git init
git add -A
git commit -m 'first commit'
git remote add origin git@github.com:my_vendor/nice_feature.git
git push -u origin master
```
Once the module is pushed to the repository you should see the code on GitHub. From now on it will be available for
others to clone, as long as they have access (see the note below though: private modules are not deployable).
### Including a private module in your project
Including public or private repositories that are not indexed on **Packagist** is different from simply using the `composer require silverstripe/blog` command. We will need to point *composer* to specific URLs. Background information can be found at
[Working with project forks and unreleased
modules](../../getting_started/composer/#working-with-project-forks-and-unreleased-modules).
For our *nice_module* example module we have just pushed upstream and can add the following lines to your `composer.json` file in the root directory of your main project.
```json
"repositories": [
{
"type": "vcs",
"url": "git@github.com:my_vendor/nice_feature.git",
}
]
```
This will add the repository to the list of URLs composer checks when updating the project dependencies. Hence you can
now include the following requirement in the same `composer.json`:
```
"require": {
...
"my_vendor.nice_feature": "*"
}
```
Add the module directory name (`nice_feature/`) to `.gitignore` - we will rely on *composer* to update the dependencies so
we don't need to version-control it through the master repository.
Run `composer update` to pull the module in and update all other dependencies as well. You can also update just this one
module by calling `composer update my_vendor/nice_feature`.
If you get cryptic composer errors it's worth checking that your module code is fully pushed. This is because composer
can only access the code you have actually pushed to the upstream repository and it may be trying to use the stale
versions of the files. Also, update composer regularly (`composer self-update`). You can also try deleting Composer
cache: `rm -fr ~/.composer/cache`.
Finally, commit the the modified `composer.json`, `composer.lock` and `.gitignore` files to the repository. The
`composer.lock` serves as a snapshot marker for the dependencies - other developers will be able to `composer install`
exactly the version of the modules you have used in your project, as well as the correct version will be used for the
deployment. Some additional information is available in the [Deploying projects with
composer](https://docs.silverstripe.org/en/4/getting_started/composer/#deploying-projects-with-composer).
### Open-sourcing your creation for the community to use
In case you want to share your creation with the community, read more about [publishing a module](how_tos/publish_a_module).
## Module Standard

View File

@ -0,0 +1,52 @@
---
title: Staying up to date with CMS releases
summary: Guidance on upgrading your website with new recipe releases
---
# Upgrading
Upgrading to new patch versions of the recipe shouldn't take a long time. See [recipes and supported modules](../00_Getting_Started/05_Recipes.md)) documentation to learn more about how recipe versioning is structured.
## Patch upgrades
To get the newest patch release of the recipe, just run:
`composer update`
This will update the recipe to the new version, and pull in all the new dependencies. A new `composer.lock` file will be generated. Once you are satisfied the site is running as expected, commit both files:
`git commit composer.* -m "Upgrade the recipe to latest patch release"`
After you have pushed this commit back to your remote repository you can deploy the change.
## Minor and major upgrades
Assuming your project is using one of the [supported recipes](../00_Getting_Started/05_Recipes.md), these will likely take more time as the APIs may change between minor and major releases. For small sites it's possible for minor upgrade to take a day of work, and major upgrades could take several days. Of course this can widely differ depending on each project.
To upgrade your code, open the root `composer.json` file. Find the lines that reference the recipes, like `silverstripe/recipe-cms` and change the referenced versions to what has been reference in the changelog (as well as any other modules that have a new version).
For example, assuming that you are currently on version `~4.8.0@stable`, if you wish to upgrade to 4.9.0 you will need to modify your `composer.json` file to explicitly specify the new release branch, here `~4.9.0`:
```json
"require": {
"silverstripe/recipe-cms": "~4.9.0"
},
...
```
You now need to pull in new dependencies and commit the lock file:
```bash
composer update
git commit composer.* -m "Upgrade to recipe 4.9.0"
```
Push this commit to your remote repository, and continue with your deployment workflow.
## Cherrypicking the upgrades
If you like to only upgrade the recipe modules, you can cherry pick what is upgraded using this syntax:
`composer update silverstripe/recipe-cms`
This will update only the two specified metapackage modules without touching anything else. You still need to commit resulting `composer.lock`.

View File

@ -3,6 +3,8 @@ title: Upgrading
summary: The following guides will help you upgrade your project or module to Silverstripe CMS 4.
---
The following guides will help you upgrade your project or module to Silverstripe CMS 4. Upgrading a module is very similar to upgrading a Project. The module upgrade guide assumes familiarity with the project upgrade guide.
The following guides will help you upgrade your project.
There are also key points to help you upgrade your project or module to Silverstripe CMS 4. Upgrading a module is very similar to upgrading a Project. The module upgrade guide assumes familiarity with the project upgrade guide.
[CHILDREN]

View File

@ -0,0 +1,35 @@
# 4.10.0 (unreleased)
## Overview
- [Regression test and Security audit](#audit)
- [Dropping support for PHP 7.1 and PHP 7.2](#phpeol)
- [Features and enhancements](#features-and-enhancements)
- [Bugfixes](#bugfixes)
## Regression test and Security audit{#audit}
This release has been comprehensively regression tested and passed to a third party for a security-focused audit.
While it is still advised that you perform your own due diligence when upgrading your project, this work is performed to ensure a safe and secure upgrade with each recipe release.
## Dropping support for PHP 7.1 and PHP 7.2{#phpeol}
We've recently updated our [PHP support policy](/Getting_Started/Server_Requirements#php). The immediate affects of this changes are:
- The Silverstripe CMS Recipe release 4.10.0 drops support for PHP 7.1 and PHP 7.2. Those two PHP releases have been end-of-life for several years now and continued support would detract effort from more valuable work.
- The 4.11 minor release will drop support for PHP 7.3 later this year.
- We expect to drop support for PHP 7 altogether around January 2023.
## Features and enhancements {#features-and-enhancements}
## Bugfixes {#bugfixes}
This release includes a number of bug fixes to improve a broad range of areas. Check the change logs for full details of these fixes split by module. Thank you to the community members that helped contribute these fixes as part of the release!
<!--- Changes below this line will be automatically regenerated -->
<!--- Changes above this line will be automatically regenerated -->

View File

@ -0,0 +1,31 @@
# 4.11.0 (unreleased)
## Overview
- [Regression test and Security audit](#audit)
- [Dropping support for PHP 7.3](#phpeol)
- [Features and enhancements](#features-and-enhancements)
- [Bugfixes](#bugfixes)
## Regression test and Security audit{#audit}
This release has been comprehensively regression tested and passed to a third party for a security-focused audit.
While it is still advised that you perform your own due diligence when upgrading your project, this work is performed to ensure a safe and secure upgrade with each recipe release.
## Dropping support for PHP 7.3{#phpeol}
In accordance with our [PHP support policy](/Getting_Started/Server_Requirements), Silverstripe CMS Recipe release 4.11.0 drops support for PHP 7.3. We expect to drop support for PHP 7 altogether around January 2023.
## Features and enhancements {#features-and-enhancements}
## Bugfixes {#bugfixes}
This release includes a number of bug fixes to improve a broad range of areas. Check the change logs for full details of these fixes split by module. Thank you to the community members that helped contribute these fixes as part of the release!
<!--- Changes below this line will be automatically regenerated -->
<!--- Changes above this line will be automatically regenerated -->

View File

@ -217,6 +217,7 @@ en:
GROUPNAME: 'Group name'
GroupReminder: 'If you choose a parent group, this group will take all it''s roles'
HierarchyPermsError: 'Can''t assign parent group "{group}" with privileged permissions (requires ADMIN access)'
ValidationIdentifierAlreadyExists: 'A Group ({group}) already exists with the same {identifier}'
Locked: 'Locked?'
MEMBERS: Members
NEWGROUP: 'New Group'

View File

@ -365,9 +365,9 @@ class HTTPRequest implements ArrayAccess
}
/**
* Remove an existing HTTP header
* Returns a HTTP Header by name if found in the request
*
* @param string $header
* @param string $header Name of the header (Insensitive to case as per <rfc2616 section 4.2 "Message Headers">)
* @return mixed
*/
public function getHeader($header)
@ -393,7 +393,7 @@ class HTTPRequest implements ArrayAccess
/**
* Returns the URL used to generate the page
*
* @param bool $includeGetVars whether or not to include the get parameters\
* @param bool $includeGetVars whether or not to include the get parameters
* @return string
*/
public function getURL($includeGetVars = false)

View File

@ -1108,9 +1108,15 @@ if (class_exists(IsEqualCanonicalizing::class)) {
$member = $this->cache_generatedMembers[$permCode];
} else {
// Generate group with these permissions
$group = Group::create();
$group->Title = "$permCode group";
$group->write();
$group = Group::get()->filterAny([
'Code' => "$permCode-group",
'Title' => "$permCode group",
])->first();
if (!$group || !$group->exists()) {
$group = Group::create();
$group->Title = "$permCode group";
$group->write();
}
// Create each individual permission
foreach ($permArray as $permArrayItem) {
@ -2434,9 +2440,15 @@ class SapphireTest extends PHPUnit_Framework_TestCase implements TestOnly
$member = $this->cache_generatedMembers[$permCode];
} else {
// Generate group with these permissions
$group = Group::create();
$group->Title = "$permCode group";
$group->write();
$group = Group::get()->filterAny([
'Code' => "$permCode-group",
'Title' => "$permCode group",
])->first();
if (!$group || !$group->exists()) {
$group = Group::create();
$group->Title = "$permCode group";
$group->write();
}
// Create each individual permission
foreach ($permArray as $permArrayItem) {

View File

@ -1779,6 +1779,25 @@ class Form extends ViewableData implements HasRequestHandler
return implode(' ', array_unique($this->extraClasses));
}
/**
* Check if a CSS-class has been added to the form container.
*
* @param string $class A string containing a classname or several class
* names delimited by a single space.
* @return boolean True if all of the classnames passed in have been added.
*/
public function hasExtraClass($class)
{
//split at white space
$classes = preg_split('/\s+/', $class);
foreach ($classes as $class) {
if (!isset($this->extraClasses[$class])) {
return false;
}
}
return true;
}
/**
* Add a CSS-class to the form-container. If needed, multiple classes can
* be added by delimiting a string with spaces.

View File

@ -602,6 +602,25 @@ class FormField extends RequestHandler
return implode(' ', $classes);
}
/**
* Check if a CSS-class has been added to the form container.
*
* @param string $class A string containing a classname or several class
* names delimited by a single space.
* @return boolean True if all of the classnames passed in have been added.
*/
public function hasExtraClass($class)
{
//split at white space
$classes = preg_split('/\s+/', $class);
foreach ($classes as $class) {
if (!isset($this->extraClasses[$class])) {
return false;
}
}
return true;
}
/**
* Add one or more CSS-classes to the FormField container.
*

View File

@ -394,12 +394,28 @@ class PaginatedList extends ListDecorator
return $this->TotalPages() > 1;
}
/**
* @return bool
*/
public function FirstPage()
{
return $this->CurrentPage() == 1;
}
/**
* @return bool
*/
public function NotFirstPage()
{
return $this->CurrentPage() != 1;
return !$this->FirstPage();
}
/**
* @return bool
*/
public function LastPage()
{
return $this->CurrentPage() == $this->TotalPages();
}
/**
@ -407,7 +423,7 @@ class PaginatedList extends ListDecorator
*/
public function NotLastPage()
{
return $this->CurrentPage() < $this->TotalPages();
return !$this->LastPage();
}
/**

View File

@ -4,6 +4,7 @@ namespace SilverStripe\Security;
use SilverStripe\Admin\SecurityAdmin;
use SilverStripe\Core\Convert;
use SilverStripe\Forms\CompositeValidator;
use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
@ -21,6 +22,7 @@ use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig;
use SilverStripe\Forms\ListboxField;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\Tab;
use SilverStripe\Forms\TabSet;
use SilverStripe\Forms\TextareaField;
@ -480,7 +482,16 @@ class Group extends DataObject
*/
public function setCode($val)
{
$this->setField("Code", Convert::raw2url($val));
$currentGroups = Group::get()
->map('Code', 'Title')
->toArray();
$code = Convert::raw2url($val);
$count = 2;
while (isset($currentGroups[$code])) {
$code = Convert::raw2url($val . '-' . $count);
$count++;
}
$this->setField("Code", $code);
}
public function validate()
@ -506,9 +517,45 @@ class Group extends DataObject
}
}
$currentGroups = Group::get()
->filter('ID:not', $this->ID)
->map('Code', 'Title')
->toArray();
if (isset($currentGroups[$this->Code])) {
$result->addError(
_t(
'SilverStripe\\Security\\Group.ValidationIdentifierAlreadyExists',
'A Group ({group}) already exists with the same {identifier}',
['group' => $this->Code, 'identifier' => 'Code']
)
);
}
if (in_array($this->Title, $currentGroups)) {
$result->addError(
_t(
'SilverStripe\\Security\\Group.ValidationIdentifierAlreadyExists',
'A Group ({group}) already exists with the same {identifier}',
['group' => $this->Title, 'identifier' => 'Title']
)
);
}
return $result;
}
public function getCMSCompositeValidator(): CompositeValidator
{
$validator = parent::getCMSCompositeValidator();
$validator->addValidator(RequiredFields::create([
'Title'
]));
return $validator;
}
public function onBeforeWrite()
{
parent::onBeforeWrite();

View File

@ -93,6 +93,19 @@ class FormFieldTest extends SapphireTest
$this->assertStringEndsWith('class1 class2', $field->extraClass());
}
public function testHasExtraClass()
{
$field = new FormField('MyField');
$field->addExtraClass('class1');
$field->addExtraClass('class2');
$this->assertTrue($field->hasExtraClass('class1'));
$this->assertTrue($field->hasExtraClass('class2'));
$this->assertTrue($field->hasExtraClass('class1 class2'));
$this->assertTrue($field->hasExtraClass('class2 class1'));
$this->assertFalse($field->hasExtraClass('class3'));
$this->assertFalse($field->hasExtraClass('class2 class3'));
}
public function testRemoveExtraClass()
{
$field = new FormField('MyField');

View File

@ -726,6 +726,19 @@ class FormTest extends FunctionalTest
$this->assertStringEndsWith('class1 class2', $form->extraClass());
}
public function testHasExtraClass()
{
$form = $this->getStubForm();
$form->addExtraClass('class1');
$form->addExtraClass('class2');
$this->assertTrue($form->hasExtraClass('class1'));
$this->assertTrue($form->hasExtraClass('class2'));
$this->assertTrue($form->hasExtraClass('class1 class2'));
$this->assertTrue($form->hasExtraClass('class2 class1'));
$this->assertFalse($form->hasExtraClass('class3'));
$this->assertFalse($form->hasExtraClass('class2 class3'));
}
public function testRemoveExtraClass()
{
$form = $this->getStubForm();

View File

@ -2,7 +2,6 @@
namespace SilverStripe\Forms\Tests;
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
use SilverStripe\Assets\File;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\Form;
@ -13,8 +12,6 @@ use SilverStripe\View\SSViewer;
class TreeMultiselectFieldTest extends SapphireTest
{
use ArraySubsetAsserts;
protected static $fixture_file = 'TreeDropdownFieldTest.yml';
protected static $extra_dataobjects = [
@ -136,38 +133,24 @@ class TreeMultiselectFieldTest extends SapphireTest
$this->assertEquals($fieldId, sprintf('%s_%s', $this->formId, $this->fieldName));
$schemaStateDefaults = $field->getSchemaStateDefaults();
$this->assertArraySubset(
[
'id' => $fieldId,
'name' => $this->fieldName,
'value' => 'unchanged'
],
$schemaStateDefaults,
true
);
$this->assertSame($fieldId, $schemaStateDefaults['id']);
$this->assertSame($this->fieldName, $schemaStateDefaults['name']);
$this->assertSame('unchanged', $schemaStateDefaults['value']);
$schemaDataDefaults = $field->getSchemaDataDefaults();
$this->assertArraySubset(
[
'id' => $fieldId,
'name' => $this->fieldName,
'type' => 'text',
'schemaType' => 'SingleSelect',
'component' => 'TreeDropdownField',
'holderId' => sprintf('%s_Holder', $fieldId),
'title' => 'Test tree',
'extraClass' => 'treemultiselect multiple searchable',
'data' => [
'urlTree' => 'field/TestTree/tree',
'showSearch' => true,
'emptyString' => '(Search or choose File)',
'hasEmptyDefault' => false,
'multiple' => true
]
],
$schemaDataDefaults,
true
);
$this->assertSame($fieldId, $schemaDataDefaults['id']);
$this->assertSame($this->fieldName, $schemaDataDefaults['name']);
$this->assertSame('text', $schemaDataDefaults['type']);
$this->assertSame('SingleSelect', $schemaDataDefaults['schemaType']);
$this->assertSame('TreeDropdownField', $schemaDataDefaults['component']);
$this->assertSame(sprintf('%s_Holder', $fieldId), $schemaDataDefaults['holderId']);
$this->assertSame('Test tree', $schemaDataDefaults['title']);
$this->assertSame('treemultiselect multiple searchable', $schemaDataDefaults['extraClass']);
$this->assertSame('field/TestTree/tree', $schemaDataDefaults['data']['urlTree']);
$this->assertSame(true, $schemaDataDefaults['data']['showSearch']);
$this->assertSame('(Search or choose File)', $schemaDataDefaults['data']['emptyString']);
$this->assertSame(false, $schemaDataDefaults['data']['hasEmptyDefault']);
$this->assertSame(true, $schemaDataDefaults['data']['multiple']);
$items = $field->getItems();
$this->assertCount(0, $items, 'there must be no items selected');
@ -188,15 +171,9 @@ class TreeMultiselectFieldTest extends SapphireTest
$field->setValue($this->fieldValue);
$schemaStateDefaults = $field->getSchemaStateDefaults();
$this->assertArraySubset(
[
'id' => $field->ID(),
'name' => 'TestTree',
'value' => $this->folderIds
],
$schemaStateDefaults,
true
);
$this->assertSame($field->ID(), $schemaStateDefaults['id']);
$this->assertSame('TestTree', $schemaStateDefaults['name']);
$this->assertSame($this->folderIds, $schemaStateDefaults['value']);
$items = $field->getItems();
$this->assertCount(2, $items, 'there must be exactly 2 items selected');
@ -214,38 +191,24 @@ class TreeMultiselectFieldTest extends SapphireTest
$field = $this->field->performReadonlyTransformation();
$schemaStateDefaults = $field->getSchemaStateDefaults();
$this->assertArraySubset(
[
'id' => $field->ID(),
'name' => 'TestTree',
'value' => 'unchanged'
],
$schemaStateDefaults,
true
);
$this->assertSame($field->ID(), $schemaStateDefaults['id']);
$this->assertSame('TestTree', $schemaStateDefaults['name']);
$this->assertSame('unchanged', $schemaStateDefaults['value']);
$schemaDataDefaults = $field->getSchemaDataDefaults();
$this->assertArraySubset(
[
'id' => $field->ID(),
'name' => $this->fieldName,
'type' => 'text',
'schemaType' => 'SingleSelect',
'component' => 'TreeDropdownField',
'holderId' => sprintf('%s_Holder', $field->ID()),
'title' => 'Test tree',
'extraClass' => 'treemultiselectfield_readonly multiple searchable',
'data' => [
'urlTree' => 'field/TestTree/tree',
'showSearch' => true,
'emptyString' => '(Search or choose File)',
'hasEmptyDefault' => false,
'multiple' => true
]
],
$schemaDataDefaults,
true
);
$this->assertSame($field->ID(), $schemaDataDefaults['id']);
$this->assertSame($this->fieldName, $schemaDataDefaults['name']);
$this->assertSame('text', $schemaDataDefaults['type']);
$this->assertSame('SingleSelect', $schemaDataDefaults['schemaType']);
$this->assertSame('TreeDropdownField', $schemaDataDefaults['component']);
$this->assertSame(sprintf('%s_Holder', $field->ID()), $schemaDataDefaults['holderId']);
$this->assertSame('Test tree', $schemaDataDefaults['title']);
$this->assertSame('treemultiselectfield_readonly multiple searchable', $schemaDataDefaults['extraClass']);
$this->assertSame('field/TestTree/tree', $schemaDataDefaults['data']['urlTree']);
$this->assertSame(true, $schemaDataDefaults['data']['showSearch']);
$this->assertSame('(Search or choose File)', $schemaDataDefaults['data']['emptyString']);
$this->assertSame(false, $schemaDataDefaults['data']['hasEmptyDefault']);
$this->assertSame(true, $schemaDataDefaults['data']['multiple']);
$items = $field->getItems();
$this->assertCount(0, $items, 'there must be 0 selected items');
@ -264,38 +227,24 @@ class TreeMultiselectFieldTest extends SapphireTest
$field = $field->performReadonlyTransformation();
$schemaStateDefaults = $field->getSchemaStateDefaults();
$this->assertArraySubset(
[
'id' => $field->ID(),
'name' => 'TestTree',
'value' => $this->folderIds
],
$schemaStateDefaults,
true
);
$this->assertSame($field->ID(), $schemaStateDefaults['id']);
$this->assertSame('TestTree', $schemaStateDefaults['name']);
$this->assertSame($this->folderIds, $schemaStateDefaults['value']);
$schemaDataDefaults = $field->getSchemaDataDefaults();
$this->assertArraySubset(
[
'id' => $field->ID(),
'name' => $this->fieldName,
'type' => 'text',
'schemaType' => 'SingleSelect',
'component' => 'TreeDropdownField',
'holderId' => sprintf('%s_Holder', $field->ID()),
'title' => 'Test tree',
'extraClass' => 'treemultiselectfield_readonly multiple searchable',
'data' => [
'urlTree' => 'field/TestTree/tree',
'showSearch' => true,
'emptyString' => '(Search or choose File)',
'hasEmptyDefault' => false,
'multiple' => true
]
],
$schemaDataDefaults,
true
);
$this->assertSame($field->ID(), $schemaDataDefaults['id']);
$this->assertSame($this->fieldName, $schemaDataDefaults['name']);
$this->assertSame('text', $schemaDataDefaults['type']);
$this->assertSame('SingleSelect', $schemaDataDefaults['schemaType']);
$this->assertSame('TreeDropdownField', $schemaDataDefaults['component']);
$this->assertSame(sprintf('%s_Holder', $field->ID()), $schemaDataDefaults['holderId']);
$this->assertSame('Test tree', $schemaDataDefaults['title']);
$this->assertSame('treemultiselectfield_readonly multiple searchable', $schemaDataDefaults['extraClass']);
$this->assertSame('field/TestTree/tree', $schemaDataDefaults['data']['urlTree']);
$this->assertSame(true, $schemaDataDefaults['data']['showSearch']);
$this->assertSame('(Search or choose File)', $schemaDataDefaults['data']['emptyString']);
$this->assertSame(false, $schemaDataDefaults['data']['hasEmptyDefault']);
$this->assertSame(true, $schemaDataDefaults['data']['multiple']);
$items = $field->getItems();
$this->assertCount(2, $items, 'there must be exactly 2 selected items');

View File

@ -282,6 +282,14 @@ class PaginatedListTest extends SapphireTest
$this->assertFalse($list->MoreThanOnePage());
}
public function testFirstPage()
{
$list = new PaginatedList(new ArrayList());
$this->assertTrue($list->FirstPage());
$list->setCurrentPage(2);
$this->assertFalse($list->FirstPage());
}
public function testNotFirstPage()
{
$list = new PaginatedList(new ArrayList());
@ -290,6 +298,16 @@ class PaginatedListTest extends SapphireTest
$this->assertTrue($list->NotFirstPage());
}
public function testLastPage()
{
$list = new PaginatedList(new ArrayList());
$list->setTotalItems(50);
$this->assertFalse($list->LastPage());
$list->setCurrentPage(5);
$this->assertTrue($list->LastPage());
}
public function testNotLastPage()
{
$list = new PaginatedList(new ArrayList());

View File

@ -38,7 +38,7 @@ class GroupCsvBulkLoaderTest extends SapphireTest
$updated = $results->Updated()->toArray();
$this->assertEquals(count($updated), 1);
$this->assertEquals($updated[0]->Code, 'newgroup1');
$this->assertEquals($updated[0]->Code, 'newgroup1-2');
$this->assertEquals($updated[0]->Title, 'New Group 1');
}

View File

@ -5,6 +5,7 @@ namespace SilverStripe\Security\Tests;
use InvalidArgumentException;
use SilverStripe\Control\Controller;
use SilverStripe\Dev\FunctionalTest;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataObject;
use SilverStripe\Security\Group;
@ -33,7 +34,7 @@ class GroupTest extends FunctionalTest
$this->assertEquals('my-title', $g1->Code, 'Custom title gets converted to code if none exists already');
$g2 = new Group();
$g2->Title = "My Title";
$g2->Title = "My Title and Code";
$g2->Code = "my-code";
$g2->write();
$this->assertEquals('my-code', $g2->Code, 'Custom attributes are not overwritten by Title field');
@ -101,6 +102,7 @@ class GroupTest extends FunctionalTest
{
$member = $this->objFromFixture(TestMember::class, 'admin');
$group = new Group();
$group->Title = 'Title';
// Can save user to unsaved group
$group->Members()->add($member);
@ -121,6 +123,7 @@ class GroupTest extends FunctionalTest
/** @var Group $childGroup */
$childGroup = $this->objFromFixture(Group::class, 'childgroup');
$orphanGroup = new Group();
$orphanGroup->Title = 'Title';
$orphanGroup->ParentID = 99999;
$orphanGroup->write();
@ -280,4 +283,59 @@ class GroupTest extends FunctionalTest
'Members with only APPLY_ROLES can\'t assign parent groups with inherited ADMIN permission'
);
}
public function testGroupTitleValidation()
{
$group1 = $this->objFromFixture(Group::class, 'group1');
$newGroup = new Group();
$validators = $newGroup->getCMSCompositeValidator()->getValidators();
$this->assertCount(1, $validators);
$this->assertInstanceOf(RequiredFields::class, $validators[0]);
$this->assertTrue(in_array('Title', $validators[0]->getRequired()));
$newGroup->Title = $group1->Title;
$result = $newGroup->validate();
$this->assertFalse(
$result->isValid(),
'Group names cannot be duplicated'
);
$newGroup->Title = 'Title';
$result = $newGroup->validate();
$this->assertTrue($result->isValid());
}
public function testGroupTitleDuplication()
{
$group = $this->objFromFixture(Group::class, 'group1');
$group->Title = 'Group title modified';
$group->write();
$this->assertEquals('group-1', $group->Code);
$group = new Group();
$group->Title = 'Group 1';
$group->write();
$this->assertEquals('group-1-2', $group->Code);
$group = new Group();
$group->Title = 'Duplicate';
$group->write();
$group->Title = 'Duplicate renamed';
$group->write();
$this->assertEquals('duplicate', $group->Code);
$group = new Group();
$group->Title = 'Duplicate';
$group->write();
$group->Title = 'More renaming';
$group->write();
$this->assertEquals('duplicate-2', $group->Code);
$group = new Group();
$group->Title = 'Duplicate';
$group->write();
$this->assertEquals('duplicate-3', $group->Code);
}
}

View File

@ -1,12 +1,16 @@
'SilverStripe\Security\Group':
admingroup:
Title: Admin Group
Code: admingroup
parentgroup:
Title: Parent Group
Code: parentgroup
childgroup:
Title: Child Group
Code: childgroup
Parent: '=>SilverStripe\Security\Group.parentgroup'
grandchildgroup:
Title: Grandchild Group
Code: grandchildgroup
Parent: '=>SilverStripe\Security\Group.childgroup'
group1: