Browse Source

Fix/batch upload children, with validation according to default widget (#896)

* Add ctools, prior to using it.

* Fix up all the dependency references.

... before the colon is the project name, so should only be "drupal" for
modules shipped in core.

* Some more together.

* Decent progress... getting things actually rendering...

... bit of refactoring stuff making a mess.

* More worky.

... as in, basically functional. Still needs coding standards pass, and
testing with more/all types of content.

* Coding standards, and warning of validation issues.

* Pull the batch out to a separate service.

* Something of namespacing the child-specific batch...

... 'cause need to slap together a media-specific batch similarly?

* All together, I think...

Both the child-uploading, and media-uploading forms.

* It is not necessary to explicitly mark the files as permanent.

* Further generalizing...

... no longer necessarily trying to load files, where files might not
be present (for non-file media... oEmbed things?).

* Adjust class comment.

* Get rid of the deprecation flags.

* Remove unused constant.

... is defined instead at the "FileSelectionForm" level, accidentally
left it here from intermediate implementation state.

* Pass the renderer along, with the version constraint.

* Add update hook to enable ctools in sites where it may not be.

... as it's now required.

* Cover ALL the exits.

* Refine message.

* Excessively long line in comment...

... whoops.

* Bump spec up to allow ctools 4.

Gave it a run through here, and seemed to work fine; however, ctools'
project page still seems to suggest the 3 major version should be
preferred... but let's allow 4, if people are using or want to test it
out?

* Fix undefined "count" index.
pull/904/merge 2.5.0
Adam 2 years ago committed by GitHub
parent
commit
3f7ca2ca10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      composer.json
  2. 25
      islandora.info.yml
  3. 38
      islandora.install
  4. 14
      islandora.routing.yml
  5. 16
      islandora.services.yml
  6. 2
      src/Form/AddChildrenForm.php
  7. 258
      src/Form/AddChildrenWizard/AbstractBatchProcessor.php
  8. 157
      src/Form/AddChildrenWizard/AbstractFileSelectionForm.php
  9. 125
      src/Form/AddChildrenWizard/AbstractForm.php
  10. 71
      src/Form/AddChildrenWizard/Access.php
  11. 57
      src/Form/AddChildrenWizard/ChildBatchProcessor.php
  12. 32
      src/Form/AddChildrenWizard/ChildFileSelectionForm.php
  13. 24
      src/Form/AddChildrenWizard/ChildForm.php
  14. 157
      src/Form/AddChildrenWizard/ChildTypeSelectionForm.php
  15. 66
      src/Form/AddChildrenWizard/FieldTrait.php
  16. 34
      src/Form/AddChildrenWizard/MediaBatchProcessor.php
  17. 32
      src/Form/AddChildrenWizard/MediaFileSelectionForm.php
  18. 24
      src/Form/AddChildrenWizard/MediaForm.php
  19. 227
      src/Form/AddChildrenWizard/MediaTypeSelectionForm.php
  20. 58
      src/Form/AddChildrenWizard/MediaTypeTrait.php
  21. 40
      src/Form/AddChildrenWizard/WizardTrait.php

5
composer.json

@ -28,7 +28,8 @@
"drupal/token" : "^1.3",
"drupal/flysystem" : "^2.0@alpha",
"islandora/crayfish-commons": "^2",
"drupal/file_replace": "^1.1"
"drupal/file_replace": "^1.1",
"drupal/ctools": "^3.8 || ^4"
},
"require-dev": {
"phpunit/phpunit": "^6",
@ -37,7 +38,7 @@
"sebastian/phpcpd": "*"
},
"suggest": {
"drupal/transliterate_filenames": "Sanitizes filenames when they are uploaded so they don't break your repository."
"drupal/transliterate_filenames": "Sanitizes filenames when they are uploaded so they don't break your repository."
},
"license": "GPL-2.0-or-later",
"authors": [

25
islandora.info.yml

@ -13,22 +13,23 @@ dependencies:
- drupal:text
- drupal:options
- drupal:link
- drupal:jsonld
- drupal:search_api
- drupal:jwt
- jsonld:jsonld
- search_api:search_api
- jwt:jwt
- drupal:rest
- drupal:filehash
- filehash:filehash
- drupal:basic_auth
- drupal:context_ui
- context:context_ui
- drupal:action
- drupal:eva
- eva:eva
- drupal:taxonomy
- drupal:views_ui
- drupal:media
- drupal:prepopulate
- drupal:features_ui
- drupal:migrate_source_csv
- prepopulate:prepopulate
- features:features_ui
- migrate_source_csv:migrate_source_csv
- drupal:content_translation
- drupal:flysystem
- drupal:token
- drupal:file_replace
- flysystem:flysystem
- token:token
- file_replace:file_replace
- ctools:ctools

38
islandora.install

@ -5,6 +5,10 @@
* Install/update hook implementations.
*/
use Drupal\Core\Extension\ExtensionNameLengthException;
use Drupal\Core\Extension\MissingDependencyException;
use Drupal\Core\Utility\UpdateException;
/**
* Adds common namespaces to jsonld.settings.
*/
@ -174,3 +178,37 @@ function update_jsonld_included_namespaces() {
->warning("Could not find required jsonld.settings to add default RDF namespaces.");
}
}
/**
* Ensure that ctools is enabled.
*/
function islandora_update_8007() {
$module_handler = \Drupal::moduleHandler();
if ($module_handler->moduleExists('ctools')) {
return t('The "@module_name" module is already enabled, no action necessary.', [
'@module_name' => 'ctools',
]);
}
/** @var \Drupal\Core\Extension\ModuleInstallerInterface $installer */
$installer = \Drupal::service('module_installer');
try {
if ($installer->install(['ctools'], TRUE)) {
return t('The "@module_name" module was enabled successfully.', [
'@module_name' => 'ctools',
]);
}
}
catch (ExtensionNameLengthException | MissingDependencyException $e) {
throw new UpdateException('Failed; ensure that the ctools module is available in the Drupal installation.', 0, $e);
}
catch (\Exception $e) {
throw new UpdateException('Failed; encountered an exception while trying to enable ctools.', 0, $e);
}
// Theoretically impossible to hit, as ModuleInstaller::install() only returns
// TRUE (or throws/propagates an exception), but... probably a good idea to
// have the here, just in case?
throw new UpdateException('Failed; hit the end of the update hook implementation, which is not expected.');
}

14
islandora.routing.yml

@ -37,14 +37,15 @@ islandora.add_member_to_node_page:
_entity_create_any_access: 'node'
islandora.upload_children:
path: '/node/{node}/members/upload'
path: '/node/{node}/members/upload/{step}'
defaults:
_form: '\Drupal\islandora\Form\AddChildrenForm'
_wizard: '\Drupal\islandora\Form\AddChildrenWizard\ChildForm'
_title: 'Upload Children'
step: 'type_selection'
options:
_admin_route: 'TRUE'
requirements:
_custom_access: '\Drupal\islandora\Form\AddChildrenForm::access'
_custom_access: '\Drupal\islandora\Form\AddChildrenWizard\Access::childAccess'
islandora.add_media_to_node_page:
path: '/node/{node}/media/add'
@ -58,14 +59,15 @@ islandora.add_media_to_node_page:
_entity_create_any_access: 'media'
islandora.upload_media:
path: '/node/{node}/media/upload'
path: '/node/{node}/media/upload/{step}'
defaults:
_form: '\Drupal\islandora\Form\AddMediaForm'
_wizard: '\Drupal\islandora\Form\AddChildrenWizard\MediaForm'
_title: 'Add media'
step: 'type_selection'
options:
_admin_route: 'TRUE'
requirements:
_custom_access: '\Drupal\islandora\Form\AddMediaForm::access'
_custom_access: '\Drupal\islandora\Form\AddChildrenWizard\Access::mediaAccess'
islandora.media_source_update:
path: '/media/{media}/source'

16
islandora.services.yml

@ -59,3 +59,19 @@ services:
arguments: ['@jwt.authentication.jwt']
tags:
- { name: event_subscriber }
islandora.upload_children.batch_processor:
class: Drupal\islandora\Form\AddChildrenWizard\ChildBatchProcessor
arguments:
- '@entity_type.manager'
- '@database'
- '@current_user'
- '@messenger'
- '@date.formatter'
islandora.upload_media.batch_processor:
class: Drupal\islandora\Form\AddChildrenWizard\MediaBatchProcessor
arguments:
- '@entity_type.manager'
- '@database'
- '@current_user'
- '@messenger'
- '@date.formatter'

2
src/Form/AddChildrenForm.php

@ -229,7 +229,7 @@ class AddChildrenForm extends AddMediaForm {
* @param \Drupal\Core\Routing\RouteMatch $route_match
* The current routing match.
*
* @return \Drupal\Core\Access\AccessResultAllowed|\Drupal\Core\Access\AccessResultForbidden
* @return \Drupal\Core\Access\AccessResultInterface
* Whether we can or can't show the "thing".
*/
public function access(RouteMatch $route_match) {

258
src/Form/AddChildrenWizard/AbstractBatchProcessor.php

@ -0,0 +1,258 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\Core\Database\Connection;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\file\FileInterface;
use Drupal\islandora\IslandoraUtils;
use Drupal\media\MediaInterface;
use Drupal\node\NodeInterface;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
/**
* Abstract addition batch processor.
*/
abstract class AbstractBatchProcessor {
use FieldTrait;
use DependencySerializationTrait;
use StringTranslationTrait;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface|null
*/
protected ?EntityTypeManagerInterface $entityTypeManager = NULL;
/**
* The database connection serivce.
*
* @var \Drupal\Core\Database\Connection|null
*/
protected ?Connection $database;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountProxyInterface|null
*/
protected ?AccountProxyInterface $currentUser;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected MessengerInterface $messenger;
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected DateFormatterInterface $dateFormatter;
/**
* Constructor.
*/
public function __construct(
EntityTypeManagerInterface $entity_type_manager,
Connection $database,
AccountProxyInterface $current_user,
MessengerInterface $messenger,
DateFormatterInterface $date_formatter
) {
$this->entityTypeManager = $entity_type_manager;
$this->database = $database;
$this->currentUser = $current_user;
$this->messenger = $messenger;
$this->dateFormatter = $date_formatter;
}
/**
* Implements callback_batch_operation() for our child addition batch.
*/
public function batchOperation($delta, $info, array $values, &$context) {
$transaction = $this->database->startTransaction();
try {
$entities[] = $node = $this->getNode($info, $values);
$entities[] = $this->createMedia($node, $info, $values);
$context['results'] = array_merge_recursive($context['results'], [
'validation_violations' => $this->validationClassification($entities),
]);
$context['results']['count'] = ($context['results']['count'] ?? 0) + 1;
}
catch (HttpExceptionInterface $e) {
$transaction->rollBack();
throw $e;
}
catch (\Exception $e) {
$transaction->rollBack();
throw new HttpException(500, $e->getMessage(), $e);
}
}
/**
* Loads the file indicated.
*
* @param mixed $info
* Widget values.
*
* @return \Drupal\file\FileInterface|null
* The loaded file.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function getFile($info) : ?FileInterface {
return (is_array($info) && isset($info['target_id'])) ?
$this->entityTypeManager->getStorage('file')->load($info['target_id']) :
NULL;
}
/**
* Get the node to which to attach our media.
*
* @param mixed $info
* Info from the widget used to create the request.
* @param array $values
* Additional form inputs.
*
* @return \Drupal\node\NodeInterface
* The node to which to attach the created media.
*/
abstract protected function getNode($info, array $values) : NodeInterface;
/**
* Get a name to use for bulk-created assets.
*
* @param mixed $info
* Widget values.
* @param array $values
* Form values.
*
* @return string
* An applicable name.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function getName($info, array $values) : string {
$file = $this->getFile($info);
return $file ? $file->getFilename() : strtr('Bulk ingest, {date}', [
'{date}' => $this->dateFormatter->format(time(), 'long'),
]);
}
/**
* Create a media referencing the given file, associated with the given node.
*
* @param \Drupal\node\NodeInterface $node
* The node to which the media should be associated.
* @param mixed $info
* The widget info for the media source field.
* @param array $values
* Values from the wizard, which should contain at least:
* - media_type: The machine name/ID of the media type as which to create
* the media
* - use: An array of the selected "media use" terms.
*
* @return \Drupal\media\MediaInterface
* The created media entity.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Entity\EntityStorageException
*/
protected function createMedia(NodeInterface $node, $info, array $values) : MediaInterface {
$taxonomy_term_storage = $this->entityTypeManager->getStorage('taxonomy_term');
// Create a media with the file attached and also pointing at the node.
$field = $this->getField($values);
$media_values = array_merge(
[
'bundle' => $values['media_type'],
'name' => $this->getName($info, $values),
IslandoraUtils::MEDIA_OF_FIELD => $node,
IslandoraUtils::MEDIA_USAGE_FIELD => ($values['use'] ?
$taxonomy_term_storage->loadMultiple($values['use']) :
NULL),
'uid' => $this->currentUser->id(),
// XXX: Published... no constant?
'status' => 1,
],
[
$field->getName() => [
$info,
],
]
);
$media = $this->entityTypeManager->getStorage('media')->create($media_values);
if ($media->save() !== SAVED_NEW) {
throw new \Exception("Failed to create media.");
}
return $media;
}
/**
* Helper to bulk process validatable entities.
*
* @param array $entities
* An array of entities to scan for validation violations.
*
* @return array
* An associative array mapping entity type IDs to entity IDs to a count
* of validation violations found on then given entity.
*/
protected function validationClassification(array $entities) {
$violations = [];
foreach ($entities as $entity) {
$entity_violations = $entity->validate();
if ($entity_violations->count() > 0) {
$violations[$entity->getEntityTypeId()][$entity->id()] = $entity_violations->count();
}
}
return $violations;
}
/**
* Implements callback_batch_finished() for our child addition batch.
*/
public function batchProcessFinished($success, $results, $operations): void {
if ($success) {
foreach ($results['validation_violations'] ?? [] as $entity_type => $info) {
foreach ($info as $id => $count) {
$this->messenger->addWarning($this->formatPlural(
$count,
'1 validation error present in <a target="_blank" href=":uri">bulk created entity of type %type, with ID %id</a>.',
'@count validation errors present in <a target="_blank" href=":uri">bulk created entity of type %type, with ID %id</a>.',
[
'%type' => $entity_type,
':uri' => Url::fromRoute("entity.{$entity_type}.canonical", [$entity_type => $id])->toString(),
'%id' => $id,
]
));
}
}
}
else {
$this->messenger->addError($this->t('Encountered an error when processing.'));
}
}
}

157
src/Form/AddChildrenWizard/AbstractFileSelectionForm.php

@ -0,0 +1,157 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\Core\Batch\BatchBuilder;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemList;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\WidgetInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\media\MediaTypeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Children addition wizard's second step.
*/
abstract class AbstractFileSelectionForm extends FormBase {
use WizardTrait;
const BATCH_PROCESSOR = 'abstract.abstract';
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountProxyInterface|null
*/
protected ?AccountProxyInterface $currentUser;
/**
* The batch processor service.
*
* @var \Drupal\islandora\Form\AddChildrenWizard\AbstractBatchProcessor|null
*/
protected ?AbstractBatchProcessor $batchProcessor;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
$instance = parent::create($container);
$instance->entityTypeManager = $container->get('entity_type.manager');
$instance->widgetPluginManager = $container->get('plugin.manager.field.widget');
$instance->entityFieldManager = $container->get('entity_field.manager');
$instance->currentUser = $container->get('current_user');
$instance->batchProcessor = $container->get(static::BATCH_PROCESSOR);
return $instance;
}
/**
* Helper; get the media type, based off discovering from form state.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return \Drupal\media\MediaTypeInterface
* The target media type.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function getMediaTypeFromFormState(FormStateInterface $form_state): MediaTypeInterface {
return $this->getMediaType($form_state->getTemporaryValue('wizard'));
}
/**
* Helper; get field instance, based off discovering from form state.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return \Drupal\Core\Field\FieldDefinitionInterface
* The field definition.
*/
protected function getFieldFromFormState(FormStateInterface $form_state): FieldDefinitionInterface {
$cached_values = $form_state->getTemporaryValue('wizard');
$field = $this->getField($cached_values);
$def = $field->getFieldStorageDefinition();
if ($def instanceof FieldStorageConfigInterface) {
$def->set('cardinality', FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
}
elseif ($def instanceof BaseFieldDefinition) {
$def->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
}
else {
throw new \Exception('Unable to remove cardinality limit.');
}
return $field;
}
/**
* Helper; get widget for the field, based on discovering from form state.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return \Drupal\Core\Field\WidgetInterface
* The widget.
*/
protected function getWidgetFromFormState(FormStateInterface $form_state): WidgetInterface {
return $this->getWidget($this->getFieldFromFormState($form_state));
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state): array {
// Using the media type selected in the previous step, grab the
// media bundle's "source" field, and create a multi-file upload widget
// for it, with the same kind of constraints.
$field = $this->getFieldFromFormState($form_state);
$items = FieldItemList::createInstance($field, $field->getName(), $this->getMediaTypeFromFormState($form_state)->getTypedData());
$form['#tree'] = TRUE;
$form['#parents'] = [];
$widget = $this->getWidgetFromFormState($form_state);
$form['files'] = $widget->form(
$items,
$form,
$form_state
);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
$widget = $this->getWidgetFromFormState($form_state);
$builder = (new BatchBuilder())
->setTitle($this->t('Bulk creating...'))
->setInitMessage($this->t('Initializing...'))
->setFinishCallback([$this->batchProcessor, 'batchProcessFinished']);
$values = $form_state->getValue($this->getField($cached_values)->getName());
$massaged_values = $widget->massageFormValues($values, $form, $form_state);
foreach ($massaged_values as $delta => $info) {
$builder->addOperation(
[$this->batchProcessor, 'batchOperation'],
[$delta, $info, $cached_values]
);
}
batch_set($builder->toArray());
}
}

125
src/Form/AddChildrenWizard/AbstractForm.php

@ -0,0 +1,125 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\TempStore\SharedTempStoreFactory;
use Drupal\ctools\Wizard\FormWizardBase;
use Drupal\islandora\IslandoraUtils;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Bulk children addition wizard base form.
*/
abstract class AbstractForm extends FormWizardBase {
const TEMPSTORE_ID = 'abstract.abstract';
const TYPE_SELECTION_FORM = MediaTypeSelectionForm::class;
const FILE_SELECTION_FORM = AbstractFileSelectionForm::class;
/**
* The Islandora Utils service.
*
* @var \Drupal\islandora\IslandoraUtils
*/
protected IslandoraUtils $utils;
/**
* The current node ID.
*
* @var mixed|null
*/
protected $nodeId;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected RouteMatchInterface $currentRoute;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected AccountProxyInterface $currentUser;
/**
* Constructor.
*/
public function __construct(
SharedTempStoreFactory $tempstore,
FormBuilderInterface $builder,
ClassResolverInterface $class_resolver,
EventDispatcherInterface $event_dispatcher,
RouteMatchInterface $route_match,
RendererInterface $renderer,
$tempstore_id,
AccountProxyInterface $current_user,
$machine_name = NULL,
$step = NULL
) {
parent::__construct($tempstore, $builder, $class_resolver, $event_dispatcher, $route_match, $renderer, $tempstore_id,
$machine_name, $step);
$this->nodeId = $this->routeMatch->getParameter('node');
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function getParameters() : array {
return array_merge(
parent::getParameters(),
[
'tempstore_id' => static::TEMPSTORE_ID,
'current_user' => \Drupal::service('current_user'),
]
);
}
/**
* {@inheritdoc}
*/
public function getOperations($cached_values) {
$ops = [];
$ops['type_selection'] = [
'title' => $this->t('Type Selection'),
'form' => static::TYPE_SELECTION_FORM,
'values' => [
'node' => $this->nodeId,
],
];
$ops['file_selection'] = [
'title' => $this->t('Widget Input for Selected Type'),
'form' => static::FILE_SELECTION_FORM,
'values' => [
'node' => $this->nodeId,
],
];
return $ops;
}
/**
* {@inheritdoc}
*/
public function getNextParameters($cached_values) {
return parent::getNextParameters($cached_values) + ['node' => $this->nodeId];
}
/**
* {@inheritdoc}
*/
public function getPreviousParameters($cached_values) {
return parent::getPreviousParameters($cached_values) + ['node' => $this->nodeId];
}
}

71
src/Form/AddChildrenWizard/Access.php

@ -0,0 +1,71 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Routing\RouteMatch;
use Drupal\islandora\IslandoraUtils;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Access checker.
*
* The _wizard/_form route enhancers do not really allow for access checking
* things, so let's roll it separately for now.
*/
class Access implements ContainerInjectionInterface {
/**
* The Islandora utils service.
*
* @var \Drupal\islandora\IslandoraUtils
*/
protected IslandoraUtils $utils;
/**
* Constructor.
*/
public function __construct(IslandoraUtils $utils) {
$this->utils = $utils;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) : self {
return new static(
$container->get('islandora.utils')
);
}
/**
* Check if the user can create any "Islandora" nodes and media.
*
* @param \Drupal\Core\Routing\RouteMatch $route_match
* The current routing match.
*
* @return \Drupal\Core\Access\AccessResultInterface
* Whether we can or cannot show the "thing".
*/
public function childAccess(RouteMatch $route_match) : AccessResultInterface {
return AccessResult::allowedIf($this->utils->canCreateIslandoraEntity('node', 'node_type'))
->andIf($this->mediaAccess($route_match));
}
/**
* Check if the user can create any "Islandora" media.
*
* @param \Drupal\Core\Routing\RouteMatch $route_match
* The current routing match.
*
* @return \Drupal\Core\Access\AccessResultInterface
* Whether we can or cannot show the "thing".
*/
public function mediaAccess(RouteMatch $route_match) : AccessResultInterface {
return AccessResult::allowedIf($this->utils->canCreateIslandoraEntity('media', 'media_type'));
}
}

57
src/Form/AddChildrenWizard/ChildBatchProcessor.php

@ -0,0 +1,57 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\islandora\IslandoraUtils;
use Drupal\node\NodeInterface;
/**
* Children addition batch processor.
*/
class ChildBatchProcessor extends AbstractBatchProcessor {
/**
* {@inheritdoc}
*/
protected function getNode($info, array $values) : NodeInterface {
$taxonomy_term_storage = $this->entityTypeManager->getStorage('taxonomy_term');
$node_storage = $this->entityTypeManager->getStorage('node');
$parent = $node_storage->load($values['node']);
// Create a node (with the filename?) (and also belonging to the target
// node).
/** @var \Drupal\node\NodeInterface $node */
$node = $node_storage->create([
'type' => $values['bundle'],
'title' => $this->getName($info, $values),
IslandoraUtils::MEMBER_OF_FIELD => $parent,
'uid' => $this->currentUser->id(),
'status' => NodeInterface::PUBLISHED,
IslandoraUtils::MODEL_FIELD => ($values['model'] ?
$taxonomy_term_storage->load($values['model']) :
NULL),
]);
if ($node->save() !== SAVED_NEW) {
throw new \Exception("Failed to create node.");
}
return $node;
}
/**
* {@inheritdoc}
*/
public function batchProcessFinished($success, $results, $operations): void {
if ($success) {
$this->messenger->addMessage($this->formatPlural(
$results['count'],
'Added 1 child node.',
'Added @count child nodes.'
));
}
parent::batchProcessFinished($success, $results, $operations);
}
}

32
src/Form/AddChildrenWizard/ChildFileSelectionForm.php

@ -0,0 +1,32 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Children addition wizard's second step.
*/
class ChildFileSelectionForm extends AbstractFileSelectionForm {
public const BATCH_PROCESSOR = 'islandora.upload_children.batch_processor';
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'islandora_add_children_wizard_file_selection';
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
$cached_values = $form_state->getTemporaryValue('wizard');
$form_state->setRedirectUrl(Url::fromUri("internal:/node/{$cached_values['node']}/members"));
}
}

24
src/Form/AddChildrenWizard/ChildForm.php

@ -0,0 +1,24 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
/**
* Bulk children addition wizard base form.
*/
class ChildForm extends AbstractForm {
const TEMPSTORE_ID = 'islandora.upload_children';
const TYPE_SELECTION_FORM = ChildTypeSelectionForm::class;
const FILE_SELECTION_FORM = ChildFileSelectionForm::class;
/**
* {@inheritdoc}
*/
public function getMachineName() {
return strtr("islandora_add_children_wizard__{userid}__{nodeid}", [
'{userid}' => $this->currentUser->id(),
'{nodeid}' => $this->nodeId,
]);
}
}

157
src/Form/AddChildrenWizard/ChildTypeSelectionForm.php

@ -0,0 +1,157 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Form\FormStateInterface;
use Drupal\islandora\IslandoraUtils;
/**
* Children addition wizard's first step.
*/
class ChildTypeSelectionForm extends MediaTypeSelectionForm {
/**
* {@inheritdoc}
*/
public function getFormId() : string {
return 'islandora_add_children_type_selection';
}
/**
* Memoization for ::getNodeBundleOptions().
*
* @var array|null
*/
protected ?array $nodeBundleOptions = NULL;
/**
* Indicate presence of model field on node bundles.
*
* Populated as a side effect of ::getNodeBundleOptions().
*
* @var array|null
*/
protected ?array $nodeBundleHasModelField = NULL;
/**
* Helper; get the node bundle options available to the current user.
*
* @return array
* An associative array mapping node bundle machine names to their human-
* readable labels.
*/
protected function getNodeBundleOptions() : array {
if ($this->nodeBundleOptions === NULL) {
$this->nodeBundleOptions = [];
$this->nodeBundleHasModelField = [];
$access_handler = $this->entityTypeManager->getAccessControlHandler('node');
foreach ($this->entityTypeBundleInfo->getBundleInfo('node') as $bundle => $info) {
$access = $access_handler->createAccess(
$bundle,
NULL,
[],
TRUE
);
$this->cacheableMetadata->addCacheableDependency($access);
if (!$access->isAllowed()) {
continue;
}
$this->nodeBundleOptions[$bundle] = $info['label'];
$fields = $this->entityFieldManager->getFieldDefinitions('node', $bundle);
$this->nodeBundleHasModelField[$bundle] = array_key_exists(IslandoraUtils::MODEL_FIELD, $fields);
}
}
return $this->nodeBundleOptions;
}
/**
* Generates a mapping of taxonomy term IDs to their names.
*
* @return \Generator
* The mapping of taxonomy term IDs to their names.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function getModelOptions() : \Generator {
$terms = $this->entityTypeManager->getStorage('taxonomy_term')
->loadTree('islandora_models', 0, NULL, TRUE);
foreach ($terms as $term) {
yield $term->id() => $term->getName();
}
}
/**
* Helper; map node bundles supporting the "has model" field, for #states.
*
* @return \Generator
* Yields associative array mapping the string 'value' to the bundles which
* have the given field.
*/
protected function mapModelStates() : \Generator {
$this->getNodeBundleOptions();
foreach (array_keys(array_filter($this->nodeBundleHasModelField)) as $bundle) {
yield ['value' => $bundle];
}
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$this->cacheableMetadata = CacheableMetadata::createFromRenderArray($form)
->addCacheContexts([
'url',
'url.query_args',
]);
$cached_values = $form_state->getTemporaryValue('wizard');
$form['bundle'] = [
'#type' => 'select',
'#title' => $this->t('Content Type'),
'#description' => $this->t('Each child created will have this content type.'),
'#empty_value' => '',
'#default_value' => $cached_values['bundle'] ?? '',
'#options' => $this->getNodeBundleOptions(),
'#required' => TRUE,
];
$model_states = iterator_to_array($this->mapModelStates());
$form['model'] = [
'#type' => 'select',
'#title' => $this->t('Model'),
'#description' => $this->t('Each child will be tagged with this model.'),
'#options' => iterator_to_array($this->getModelOptions()),
'#empty_value' => '',
'#default_value' => $cached_values['model'] ?? '',
'#states' => [
'visible' => [
':input[name="bundle"]' => $model_states,
],
'required' => [
':input[name="bundle"]' => $model_states,
],
],
];
$this->cacheableMetadata->applyTo($form);
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
protected static function keysToSave() : array {
return array_merge(
parent::keysToSave(),
[
'bundle',
'model',
]
);
}
}

66
src/Form/AddChildrenWizard/FieldTrait.php

@ -0,0 +1,66 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
/**
* Field lookup helper trait.
*/
trait FieldTrait {
use MediaTypeTrait;
/**
* The entity field manager service.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface|null
*/
protected ?EntityFieldManagerInterface $entityFieldManager = NULL;
/**
* Helper; get field instance, given our required values.
*
* @param array $values
* See ::getMediaType() for which values are required.
*
* @return \Drupal\Core\Field\FieldDefinitionInterface
* The target field.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function getField(array $values): FieldDefinitionInterface {
$media_type = $this->getMediaType($values);
$media_source = $media_type->getSource();
$source_field = $media_source->getSourceFieldDefinition($media_type);
$fields = $this->entityFieldManager()->getFieldDefinitions('media', $media_type->id());
return $fields[$source_field->getFieldStorageDefinition()->getName()] ??
$media_source->createSourceField($media_type);
}
/**
* Lazy-initialization of the entity field manager service.
*
* @return \Drupal\Core\Entity\EntityFieldManagerInterface
* The entity field manager service.
*/
protected function entityFieldManager() : EntityFieldManagerInterface {
if ($this->entityFieldManager === NULL) {
$this->setEntityFieldManager(\Drupal::service('entity_field.manager'));
}
return $this->entityFieldManager;
}
/**
* Setter for entity field manager.
*/
public function setEntityFieldManager(EntityFieldManagerInterface $entity_field_manager) : self {
$this->entityFieldManager = $entity_field_manager;
return $this;
}
}

34
src/Form/AddChildrenWizard/MediaBatchProcessor.php

@ -0,0 +1,34 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\node\NodeInterface;
/**
* Media addition batch processor.
*/
class MediaBatchProcessor extends AbstractBatchProcessor {
/**
* {@inheritdoc}
*/
protected function getNode($info, array $values) : NodeInterface {
return $this->entityTypeManager->getStorage('node')->load($values['node']);
}
/**
* {@inheritdoc}
*/
public function batchProcessFinished($success, $results, $operations): void {
if ($success) {
$this->messenger->addMessage($this->formatPlural(
$results['count'],
'Added 1 media.',
'Added @count media.'
));
}
parent::batchProcessFinished($success, $results, $operations);
}
}

32
src/Form/AddChildrenWizard/MediaFileSelectionForm.php

@ -0,0 +1,32 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Media addition wizard's second step.
*/
class MediaFileSelectionForm extends AbstractFileSelectionForm {
public const BATCH_PROCESSOR = 'islandora.upload_media.batch_processor';
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'islandora_add_media_wizard_file_selection';
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
$cached_values = $form_state->getTemporaryValue('wizard');
$form_state->setRedirectUrl(Url::fromUri("internal:/node/{$cached_values['node']}/media"));
}
}

24
src/Form/AddChildrenWizard/MediaForm.php

@ -0,0 +1,24 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
/**
* Bulk children addition wizard base form.
*/
class MediaForm extends AbstractForm {
const TEMPSTORE_ID = 'islandora.upload_media';
const TYPE_SELECTION_FORM = MediaTypeSelectionForm::class;
const FILE_SELECTION_FORM = MediaFileSelectionForm::class;
/**
* {@inheritdoc}
*/
public function getMachineName() {
return strtr("islandora_add_media_wizard__{userid}__{nodeid}", [
'{userid}' => $this->currentUser->id(),
'{nodeid}' => $this->nodeId,
]);
}
}

227
src/Form/AddChildrenWizard/MediaTypeSelectionForm.php

@ -0,0 +1,227 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\islandora\IslandoraUtils;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Children addition wizard's first step.
*/
class MediaTypeSelectionForm extends FormBase {
/**
* Cacheable metadata that is instantiated and used internally.
*
* @var \Drupal\Core\Cache\CacheableMetadata|null
*/
protected ?CacheableMetadata $cacheableMetadata = NULL;
/**
* The entity type bundle info service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface|null
*/
protected ?EntityTypeBundleInfoInterface $entityTypeBundleInfo;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface|null
*/
protected ?EntityTypeManagerInterface $entityTypeManager;
/**
* The entity field manager service.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface|null
*/
protected ?EntityFieldManagerInterface $entityFieldManager;
/**
* The Islandora Utils service.
*
* @var \Drupal\islandora\IslandoraUtils|null
*/
protected ?IslandoraUtils $utils;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) : self {
$instance = parent::create($container);
$instance->entityTypeBundleInfo = $container->get('entity_type.bundle.info');
$instance->entityTypeManager = $container->get('entity_type.manager');
$instance->entityFieldManager = $container->get('entity_field.manager');
$instance->utils = $container->get('islandora.utils');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId() : string {
return 'islandora_add_media_type_selection';
}
/**
* Memoization for ::getMediaBundleOptions().
*
* @var array|null
*/
protected ?array $mediaBundleOptions = NULL;
/**
* Indicate presence of usage field on media bundles.
*
* Populated as a side effect in ::getMediaBundleOptions().
*
* @var array|null
*/
protected ?array $mediaBundleUsageField = NULL;
/**
* Helper; get options for media types.
*
* @return array
* An associative array mapping the machine name of the media type to its
* human-readable label.
*/
protected function getMediaBundleOptions() : array {
if ($this->mediaBundleOptions === NULL) {
$this->mediaBundleOptions = [];
$this->mediaBundleUsageField = [];
$access_handler = $this->entityTypeManager->getAccessControlHandler('media');
foreach ($this->entityTypeBundleInfo->getBundleInfo('media') as $bundle => $info) {
if (!$this->utils->isIslandoraType('media', $bundle)) {
continue;
}
$access = $access_handler->createAccess(
$bundle,
NULL,
[],
TRUE
);
$this->cacheableMetadata->addCacheableDependency($access);
if (!$access->isAllowed()) {
continue;
}
$this->mediaBundleOptions[$bundle] = $info['label'];
$fields = $this->entityFieldManager->getFieldDefinitions('media', $bundle);
$this->mediaBundleUsageField[$bundle] = array_key_exists(IslandoraUtils::MEDIA_USAGE_FIELD, $fields);
}
}
return $this->mediaBundleOptions;
}
/**
* Helper; list the terms of the "islandora_media_use" vocabulary.
*
* @return \Generator
* Generates term IDs as keys mapping to term names.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function getMediaUseOptions() : \Generator {
/** @var \Drupal\taxonomy\TermInterface[] $terms */
$terms = $this->entityTypeManager->getStorage('taxonomy_term')
->loadTree('islandora_media_use', 0, NULL, TRUE);
foreach ($terms as $term) {
yield $term->id() => $term->getName();
}
}
/**
* Helper; map media types supporting the usage field for use with #states.
*
* @return \Generator
* Yields associative array mapping the string 'value' to the bundles which
* have the given field.
*/
protected function mapUseStates(): \Generator {
$this->getMediaBundleOptions();
foreach (array_keys(array_filter($this->mediaBundleUsageField)) as $bundle) {
yield ['value' => $bundle];
}
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$this->cacheableMetadata = CacheableMetadata::createFromRenderArray($form)
->addCacheContexts([
'url',
'url.query_args',
]);
$cached_values = $form_state->getTemporaryValue('wizard');
$form['media_type'] = [
'#type' => 'select',
'#title' => $this->t('Media Type'),
'#description' => $this->t('Each media created will have this type.'),
'#empty_value' => '',
'#default_value' => $cached_values['media_type'] ?? '',
'#options' => $this->getMediaBundleOptions(),
'#required' => TRUE,
];
$use_states = iterator_to_array($this->mapUseStates());
$form['use'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Usage'),
'#description' => $this->t('Defined by <a target="_blank" href=":url">Portland Common Data Model: Use Extension</a>. "Original File" will trigger creation of derivatives.', [
':url' => 'https://pcdm.org/2015/05/12/use',
]),
'#options' => iterator_to_array($this->getMediaUseOptions()),
'#default_value' => $cached_values['use'] ?? [],
'#states' => [
'visible' => [
':input[name="media_type"]' => $use_states,
],
'required' => [
':input[name="media_type"]' => $use_states,
],
],
];
$this->cacheableMetadata->applyTo($form);
return $form;
}
/**
* Helper; enumerate keys to persist in form state.
*
* @return string[]
* The keys to be persisted in our temp value in form state.
*/
protected static function keysToSave() : array {
return [
'media_type',
'use',
];
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
foreach (static::keysToSave() as $key) {
$cached_values[$key] = $form_state->getValue($key);
}
$form_state->setTemporaryValue('wizard', $cached_values);
}
}

58
src/Form/AddChildrenWizard/MediaTypeTrait.php

@ -0,0 +1,58 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\media\MediaTypeInterface;
/**
* Media type lookup helper trait.
*/
trait MediaTypeTrait {
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface|null
*/
protected ?EntityTypeManagerInterface $entityTypeManager = NULL;
/**
* Helper; get media type, given our required values.
*
* @param array $values
* An associative array which must contain at least:
* - media_type: The machine name of the media type to load.
*
* @return \Drupal\media\MediaTypeInterface
* The loaded media type.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function getMediaType(array $values): MediaTypeInterface {
return $this->entityTypeManager()->getStorage('media_type')->load($values['media_type']);
}
/**
* Lazy-initialization of the entity type manager service.
*
* @return \Drupal\Core\Entity\EntityTypeManagerInterface
* The entity type manager service.
*/
protected function entityTypeManager() : EntityTypeManagerInterface {
if ($this->entityTypeManager === NULL) {
$this->setEntityTypeManager(\Drupal::service('entity_type.manager'));
}
return $this->entityTypeManager;
}
/**
* Setter for the entity type manager service.
*/
public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) : self {
$this->entityTypeManager = $entity_type_manager;
return $this;
}
}

40
src/Form/AddChildrenWizard/WizardTrait.php

@ -0,0 +1,40 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\WidgetInterface;
/**
* Wizard/widget lookup helper trait.
*/
trait WizardTrait {
use FieldTrait;
/**
* The widget plugin manager service.
*
* @var \Drupal\Core\Field\WidgetPluginManager
*/
protected PluginManagerInterface $widgetPluginManager;
/**
* Helper; get the base widget for the given field.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field
* The field for which get obtain the widget.
*
* @return \Drupal\Core\Field\WidgetInterface
* The widget.
*/
protected function getWidget(FieldDefinitionInterface $field): WidgetInterface {
return $this->widgetPluginManager->getInstance([
'field_definition' => $field,
'form_mode' => 'default',
'prepare' => TRUE,
]);
}
}
Loading…
Cancel
Save