Merge pull request #159 from creative-commoners/pulls/1/arbitrarydata-gridfield

NEW Add fixtures for arbitrary data gridfield behat tests
This commit is contained in:
Maxime Rainville 2023-12-19 19:55:54 +13:00 committed by GitHub
commit 5f70444e06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 659 additions and 1 deletions

View File

@ -15,6 +15,18 @@ SilverStripe\FrameworkTest\Model\Employee:
extensions:
- SilverStripe\FrameworkTest\Extension\TestDataObjectExtension
SilverStripe\ORM\DatabaseAdmin:
extensions:
- SilverStripe\FrameworkTest\GridFieldArbitraryData\DatabaseBuildExtension
---
Only:
moduleexists: 'silverstripe/testsession'
---
SilverStripe\TestSession\TestSessionEnvironment:
extensions:
- SilverStripe\FrameworkTest\GridFieldArbitraryData\DatabaseBuildExtension
---
Only:
moduleexists: 'dnadesign/silverstripe-elemental'

View File

@ -0,0 +1,240 @@
<?php
namespace SilverStripe\FrameworkTest\GridFieldArbitraryData;
use RuntimeException;
use SilverStripe\Admin\LeftAndMain;
use SilverStripe\Control\Controller;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldConfig;
use SilverStripe\Forms\GridField\GridFieldConfig_Base;
use SilverStripe\Forms\GridField\GridFieldConfig_RecordEditor;
use SilverStripe\Forms\GridField\GridFieldDataColumns;
use SilverStripe\Forms\GridField\GridFieldDetailForm;
use SilverStripe\Forms\GridField\GridFieldExportButton;
use SilverStripe\Forms\GridField\GridFieldFilterHeader;
use SilverStripe\Forms\GridField\GridFieldPaginator;
use SilverStripe\Forms\GridField\GridFieldPrintButton;
use SilverStripe\Forms\GridField\GridFieldViewButton;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\Queries\SQLSelect;
use SilverStripe\ORM\Search\BasicSearchContext;
use SilverStripe\View\ArrayData;
class ArbitraryDataAdmin extends LeftAndMain
{
public const TAB_ARRAYDATA = 'arraydata';
public const TAB_CUSTOM_MODEL = 'custommodel';
private static $url_segment = 'arbitrary-data';
private static $menu_title = 'Arbitrary Data Gridfield';
private static $url_rule = '/$Tab/$Action';
private static $url_handlers = [
'$Tab/$Action' => 'handleAction'
];
private ?string $tab = null;
private static int $num_initial_items = 30;
/**
* Directly copied from ModelAdmin with minor tweaks
*/
protected function init()
{
parent::init();
$this->tab = $this->getRequest()->param('Tab');
// accessing the admin directly
if ($this->tab === null) {
$this->tab = self::TAB_ARRAYDATA;
}
if ($this->tab !== self::TAB_ARRAYDATA && $this->tab !== self::TAB_CUSTOM_MODEL) {
throw new RuntimeException("Unexpected url segment: {$this->tab}");
}
}
public function getList()
{
$list = ArrayList::create();
switch ($this->tab) {
case self::TAB_ARRAYDATA:
foreach (self::getInitialRecords() as $stub) {
$list->add(ArrayData::create($stub));
}
break;
case self::TAB_CUSTOM_MODEL:
$rawData = SQLSelect::create()->setFrom(ArbitraryDataModel::TABLE_NAME)->execute();
foreach ($rawData as $record) {
$list->add(ArbitraryDataModel::create($record));
}
$list->setDataClass(ArbitraryDataModel::class);
break;
default:
throw new RuntimeException("Unexpected tab: {$this->tab}");
}
$this->extend('updateList', $list);
return $list;
}
public static function getInitialRecords()
{
$numRecords = static::config()->get('num_initial_items');
$records = [];
for ($id = 1; $id <= $numRecords; $id++) {
$records[] = [
'ID' => $id,
'Title' => "item $id",
];
}
return $records;
}
protected function getGridFieldConfig(): GridFieldConfig
{
if ($this->tab === self::TAB_CUSTOM_MODEL) {
$config = GridFieldConfig_RecordEditor::create();
} else {
// This is effectively the same as a GridFieldConfig_RecordViewer, but without removing the GridFieldFilterHeader.
$config = GridFieldConfig_Base::create();
$config->addComponent(GridFieldViewButton::create());
$config->addComponent(GridFieldDetailForm::create());
$fieldNames = array_keys(self::getInitialRecords()[0]);
$config->getComponentByType(GridFieldDataColumns::class)->setDisplayFields(array_combine($fieldNames, $fieldNames));
$fields = array_map(fn ($name) => $name === 'ID' ? HiddenField::create($name) : TextField::create($name), $fieldNames);
$config->getComponentByType(GridFieldDetailForm::class)->setFields(FieldList::create($fields));
$searchContext = BasicSearchContext::create(ArrayData::class);
$searchFields = array_map(
fn ($name) => $name === 'ID'
? HiddenField::create(BasicSearchContext::config()->get('general_search_field_name'))
: TextField::create($name),
$fieldNames
);
$searchContext->setFields(FieldList::create($searchFields));
$config->getComponentByType(GridFieldFilterHeader::class)->setSearchContext($searchContext);
}
$config->getComponentByType(GridFieldPaginator::class)->setItemsPerPage(10);
$exportButton = GridFieldExportButton::create('buttons-before-left');
// $exportButton->setExportColumns($this->getExportFields());
$config->addComponents([
$exportButton,
GridFieldPrintButton::create('buttons-before-left')
]);
$this->extend('updateGridFieldConfig', $config);
return $config;
}
/**
* Directly copied from ModelAdmin with minor tweaks
*/
protected function getGridField(): GridField
{
$field = GridField::create(
$this->tab,
false,
$this->getList(),
$this->getGridFieldConfig()
);
$this->extend('updateGridField', $field);
return $field;
}
/**
* Directly copied from ModelAdmin with minor tweaks
*/
public function getEditForm($id = null, $fields = null)
{
$form = Form::create(
$this,
'EditForm',
FieldList::create($this->getGridField()),
FieldList::create()
)->setHTMLID('Form_EditForm');
$form->addExtraClass('cms-edit-form cms-panel-padded center flexbox-area-grow');
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
$editFormAction = Controller::join_links($this->Link($this->tab), 'EditForm');
$form->setFormAction($editFormAction);
$form->setAttribute('data-pjax-fragment', 'CurrentForm');
$this->extend('updateEditForm', $form);
return $form;
}
/**
* Directly copied from ModelAdmin with minor tweaks
*/
protected function getManagedTabs()
{
$tabs = [
self::TAB_ARRAYDATA => 'ArrayData',
self::TAB_CUSTOM_MODEL => 'Custom Model',
];
$forms = new ArrayList();
foreach ($tabs as $tab => $title) {
$forms->push(new ArrayData([
'Title' => $title,
'Tab' => $tab,
'Link' => $this->Link($tab),
'LinkOrCurrent' => ($tab === $this->tab) ? 'current' : 'link'
]));
}
return $forms;
}
/**
* Directly copied from ModelAdmin with minor tweaks
*/
public function Link($action = null)
{
if (!$action) {
$action = $this->tab;
}
return parent::Link($action);
}
/**
* Directly copied from ModelAdmin with minor tweaks
*/
public function Breadcrumbs($unlinked = false)
{
$items = parent::Breadcrumbs($unlinked);
// Show the class name rather than ModelAdmin title as root node
$params = $this->getRequest()->getVars();
if (isset($params['url'])) {
unset($params['url']);
}
$items[0]->Title = $this->tab;
$items[0]->Link = Controller::join_links(
$this->Link($this->tab),
'?' . http_build_query($params ?? [])
);
return $items;
}
}

View File

@ -0,0 +1,205 @@
<?php
namespace SilverStripe\FrameworkTest\GridFieldArbitraryData;
use LogicException;
use SilverStripe\Forms\DatetimeField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\Queries\SQLDelete;
use SilverStripe\ORM\Search\BasicSearchContext;
use SilverStripe\View\ArrayData;
/**
* A class of arbitrary data for testing GridField components.
*
* It stores its data in the database, but it doesn't use DataObject abstractions
* to do so. The database in this scenario could just as easily be an API or other
* way to fetch and send data.
*/
class ArbitraryDataModel extends ArrayData implements DataObjectInterface
{
/**
* In order to validate that writing/deleting works for arbitrary data, we'll be storing these
* records in the database - just not using the DataObject abstraction.
*
* For our purposes, the database is acting as an abitrary data storage layer. It could just as
* easily be sending/recieving data through an API, for example.
*
* Note that the database will not be used for filtering/sorting/etc - it is only used to store
* the data on save, delete the data on delete, and fetch the data when loading the admin.
*/
public const TABLE_NAME = 'frameworktest_ArbitraryDataModel';
/**
* We need to ensure there is an ID field for new records
*/
public function __construct($value = [])
{
if (!isset($value['ID'])) {
$value['ID'] = 0;
}
parent::__construct($value);
}
/**
* Stores the current data into the database - but could just as easily send it through an API
* endpoint for storing somewhere else, or save to a file, etc.
*/
public function write()
{
$isNew = !$this->ID;
$now = DBDatetime::now()->Rfc2822();
$record = $this->array;
$record['LastEdited'] = $now;
if ($isNew) {
$record['Created'] = $now;
}
// Remove anything that isn't storable in the DB, such as Security ID
$dbColumns = DB::field_list(self::TABLE_NAME);
foreach ($record as $fieldName => $value) {
if (!array_key_exists($fieldName, $dbColumns)) {
unset($record[$fieldName]);
}
}
// This is basically a fancy SQLInsert or SQLUpdate - I just copied DataObject so I didn't have to think.
$manipulation = [
'command' => $isNew ? 'insert' : 'update',
'fields' => $record,
];
if (!$isNew) {
$manipulation['id'] = $this->ID;
}
DB::manipulate([self::TABLE_NAME => $manipulation]);
if ($isNew) {
// Must save the ID in this object so GridField knows what URL to redirect to.
$this->ID = DB::get_generated_id(self::TABLE_NAME);
}
}
public function delete()
{
if (!$this->ID) {
throw new LogicException('DataObject::delete() called on a record without an ID');
}
SQLDelete::create()->setFrom(self::TABLE_NAME)->setWhere(['ID' => $this->ID])->execute();
$this->ID = 0;
}
/**
* Sets the value from the form
*/
public function setCastedField($fieldName, $val)
{
$this->$fieldName = $val;
}
/**
* Gives a localisable plural name for the class.
*
* Used in add button, breadcrumbs, and toasts
*/
public function i18n_singular_name()
{
return _t(__CLASS__ . '.SINGULAR_NAME', 'Arbitrary Datum');
}
/**
* Gives a localisable plural name for the class.
*
* Used in filter header as the placeholder text
*/
public function i18n_plural_name()
{
return _t(__CLASS__ . '.PLURAL_NAME', 'Arbitrary Data');
}
/**
* Used to auto-detect gridfield columns
*/
public function summaryFields()
{
$fieldNames = $this->getFieldNames();
$summaryFields = array_combine($fieldNames, $fieldNames);
unset($summaryFields['ID']);
return $summaryFields;
}
public function getDefaultSearchContext()
{
return BasicSearchContext::create(static::class);
}
public function scaffoldSearchFields()
{
$fieldNames = $this->getFieldNames();
$fields = [HiddenField::create(BasicSearchContext::config()->get('general_search_field_name'))];
foreach ($fieldNames as $fieldName) {
if ($fieldName === 'ID' || $fieldName === 'Created' || $fieldName === 'LastEdited') {
continue;
}
$fields[] = TextField::create($fieldName);
}
return FieldList::create($fields);
}
public function getCMSFields(): FieldList
{
$fieldNames = $this->getFieldNames();
$fields = [];
foreach ($fieldNames as $fieldName) {
switch ($fieldName) {
case 'ID':
$fields[] = HiddenField::create($fieldName);
break;
case 'Created':
case 'LastEdited':
$fields[] = DatetimeField::create($fieldName)->performReadonlyTransformation();
break;
default:
$fields[] = TextField::create($fieldName);
}
}
return FieldList::create($fields);
}
// Note that a FieldsValidator is used by default, but we can add additional validation if we want
// by implementing this method:
// public function getCMSCompositeValidator()
// {
// return CompositeValidator::create([
// FieldsValidator::create(),
// RequiredFields::create(['Title']),
// ]);
// }
public function canCreate()
{
return true;
}
public function canEdit()
{
return true;
}
public function canDelete()
{
return true;
}
private function getFieldNames()
{
$fieldNames = array_keys(ArbitraryDataAdmin::getInitialRecords()[0]);
$fieldNames[] = 'Created';
$fieldNames[] = 'LastEdited';
return $fieldNames;
}
}

View File

@ -0,0 +1,144 @@
<?php
namespace SilverStripe\FrameworkTest\GridFieldArbitraryData;
use SilverStripe\Control\Director;
use SilverStripe\Core\Extension;
use SilverStripe\ORM\DatabaseAdmin;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\Queries\SQLSelect;
/**
* Builds the table and adds default records for the ArbitraryDataModel.
*/
class DatabaseBuildExtension extends Extension
{
/**
* This extension hook is on TestSessionEnvironment, which is used by behat but not by phpunit.
* For whatever reason, behat doesn't use dev/build, so we can't rely on the below onAfterbuild
* being run in that scenario.
*/
protected function onAfterStartTestSession()
{
$this->buildTable(true);
$this->populateData();
}
/**
* This extension hook is on DatabaseAdmin, after dev/build has finished building the database.
*/
protected function onAfterBuild(bool $quiet, bool $populate, bool $testMode): void
{
if ($testMode) {
return;
}
if (!$quiet) {
if (Director::is_cli()) {
echo "\nCREATING TABLE FOR FRAMEWORKTEST ARBITRARY DATA\n\n";
} else {
echo "\n<p><b>Creating table for frameworktest arbitrary data</b></p><ul>\n\n";
}
}
$this->buildTable($quiet);
if (!$quiet && !Director::is_cli()) {
echo '</ul>';
}
if ($populate) {
if (!$quiet) {
if (Director::is_cli()) {
echo "\nCREATING DATABASE RECORDS FOR FRAMEWORKTEST ARBITRARY DATA\n\n";
} else {
echo "\n<p><b>Creating database records arbitrary data</b></p><ul>\n\n";
}
}
$this->populateData();
if (!$quiet && !Director::is_cli()) {
echo '</ul>';
}
}
if (!$quiet) {
echo (Director::is_cli()) ? "\n Frameworktest database build completed!\n\n" : '<p>Frameworktest database build completed!</p>';
}
}
private function buildTable(bool $quiet): void
{
$tableName = ArbitraryDataModel::TABLE_NAME;
// Log data
if (!$quiet) {
$showRecordCounts = DatabaseAdmin::config()->get('show_record_counts');
if ($showRecordCounts && DB::get_schema()->hasTable($tableName)) {
try {
$count = SQLSelect::create()->setFrom($tableName)->count();
$countSuffix = " ($count records)";
} catch (\Exception $e) {
$countSuffix = ' (error getting record count)';
}
} else {
$countSuffix = "";
}
if (Director::is_cli()) {
echo " * $tableName$countSuffix\n";
} else {
echo "<li>$tableName$countSuffix</li>\n";
}
}
// Get field schema
$fields = [
'ID' => 'PrimaryKey',
'LastEdited' => 'DBDatetime',
'Created' => 'DBDatetime',
];
$fieldNames = array_keys(ArbitraryDataAdmin::getInitialRecords()[0]);
foreach ($fieldNames as $fieldName) {
if ($fieldName === 'ID') {
continue;
}
$fields[$fieldName] = 'Varchar';
}
// Write the table to the database
DB::get_schema()->schemaUpdate(function () use ($tableName, $fields) {
DB::require_table(
$tableName,
$fields,
null,
true,
DataObject::config()->get('create_table_options')
);
});
}
private function populateData(): void
{
$tableName = ArbitraryDataModel::TABLE_NAME;
$count = SQLSelect::create()->setFrom($tableName)->count();
if ($count <= 0) {
$now = DBDatetime::now()->Rfc2822();
$data = ArbitraryDataAdmin::getInitialRecords();
foreach ($data as $record) {
unset($record['ID']);
$record['LastEdited'] = $now;
$record['Created'] = $now;
$item = ArbitraryDataModel::create($record);
$item->write();
}
DB::alteration_message('Added default records for frameworktest arbitrary data', 'created');
}
}
}

View File

@ -0,0 +1,35 @@
<div class="cms-content fill-height flexbox-area-grow cms-tabset center $BaseCSSClasses" data-layout-type="border" data-pjax-fragment="Content" id="ModelAdmin">
<div class="cms-content-header north">
<div class="cms-content-header-info vertical-align-items flexbox-area-grow">
<div class="breadcrumbs-wrapper">
<span class="cms-panel-link crumb last">
<% if $SectionTitle %>
$SectionTitle
<% else %>
<%t SilverStripe\Admin\ModelAdmin.Title 'Data Models' %>
<% end_if %>
</span>
</div>
</div>
<div class="cms-content-header-tabs cms-tabset-nav-primary ss-ui-tabs-nav">
<ul class="cms-tabset-nav-primary">
<% loop $ManagedTabs %>
<li class="tab-$Tab $LinkOrCurrent<% if $LinkOrCurrent == 'current' %> ui-tabs-active<% end_if %>">
<a href="$Link" class="cms-panel-link" title="$Title.ATT">$Title</a>
</li>
<% end_loop %>
</ul>
</div>
</div>
<div class="cms-content-fields center ui-widget-content cms-panel-padded fill-height flexbox-area-grow" data-layout-type="border">
$Tools
<div class="cms-content-view">
$EditForm
</div>
</div>
</div>

View File

@ -0,0 +1 @@
<% include SilverStripe/Forms/Form %>

View File

@ -0,0 +1,21 @@
<div class="importSpec" id="SpecFor{$ClassName}">
<a href="#SpecDetailsFor{$ClassName}" class="detailsLink"><%t SilverStripe\Admin\ModelAdmin.IMPORTSPECLINK 'Show Specification for {model}' model=$ModelName %></a>
<div class="details" id="SpecDetailsFor{$ClassName}">
<h4><%t SilverStripe\Admin\ModelAdmin.IMPORTSPECTITLE 'Specification for {model}' model=$ModelName %></h4>
<h5><%t SilverStripe\Admin\ModelAdmin.IMPORTSPECFIELDS 'Database columns' %></h5>
<% loop $Fields %>
<dl>
<dt><em>$Name</em></dt>
<dd>$Description</dd>
</dl>
<% end_loop %>
<h5><%t SilverStripe\Admin\ModelAdmin.IMPORTSPECRELATIONS 'Relations' %></h5>
<% loop $Relations %>
<dl>
<dt><em>$Name</em></dt>
<dd>$Description</dd>
</dl>
<% end_loop %>
</div>
</div>