diff --git a/islandora.routing.yml b/islandora.routing.yml index e2cb7d35..86d13482 100644 --- a/islandora.routing.yml +++ b/islandora.routing.yml @@ -39,13 +39,13 @@ islandora.add_member_to_node_page: islandora.upload_children: path: '/node/{node}/members/upload/{step}' defaults: - _wizard: '\Drupal\islandora\Form\AddChildrenWizard\Form' + _wizard: '\Drupal\islandora\Form\AddChildrenWizard\ChildForm' _title: 'Upload Children' - step: 'child_type' + step: 'type_selection' options: _admin_route: 'TRUE' requirements: - _custom_access: '\Drupal\islandora\Form\AddChildrenWizard\Access::checkAccess' + _custom_access: '\Drupal\islandora\Form\AddChildrenWizard\Access::childAccess' islandora.add_media_to_node_page: path: '/node/{node}/media/add' @@ -59,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' diff --git a/islandora.services.yml b/islandora.services.yml index 1dbed028..6dfa158d 100644 --- a/islandora.services.yml +++ b/islandora.services.yml @@ -65,3 +65,11 @@ services: - '@entity_type.manager' - '@database' - '@current_user' + - '@messenger' + islandora.upload_media.batch_processor: + class: Drupal\islandora\Form\AddChildrenWizard\MediaBatchProcessor + arguments: + - '@entity_type.manager' + - '@database' + - '@current_user' + - '@messenger' diff --git a/src/Form/AddChildrenForm.php b/src/Form/AddChildrenForm.php index adad9dae..b349f570 100644 --- a/src/Form/AddChildrenForm.php +++ b/src/Form/AddChildrenForm.php @@ -13,7 +13,7 @@ use Symfony\Component\HttpKernel\Exception\HttpException; /** * Form that lets users upload one or more files as children to a resource node. * - * @deprecated Replaced with the "wizard" appraach. + * @deprecated Use the \Drupal\islandora\Form\AddChildrenWizard\ChildForm instead. */ class AddChildrenForm extends AddMediaForm { diff --git a/src/Form/AddChildrenWizard/AbstractBatchProcessor.php b/src/Form/AddChildrenWizard/AbstractBatchProcessor.php new file mode 100644 index 00000000..f5b63500 --- /dev/null +++ b/src/Form/AddChildrenWizard/AbstractBatchProcessor.php @@ -0,0 +1,254 @@ +<?php + +namespace Drupal\islandora\Form\AddChildrenWizard; + +use Drupal\Core\Database\Connection; +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; + +/** + * Children 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; + + /** + * Constructor. + */ + public function __construct( + EntityTypeManagerInterface $entity_type_manager, + Connection $database, + AccountProxyInterface $current_user, + MessengerInterface $messenger + ) { + $this->entityTypeManager = $entity_type_manager; + $this->database = $database; + $this->currentUser = $current_user; + $this->messenger = $messenger; + } + + /** + * Implements callback_batch_operation() for our child addition batch. + */ + public function batchOperation($delta, $info, array $values, &$context) { + $transaction = $this->database->startTransaction(); + + try { + $entities[] = $this->persistFile($info, $values); + $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'] += 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 array $info + * An associative array containing at least: + * - target_id: The id of the file to load. + * + * @return \Drupal\file\FileInterface + * The loaded file. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + protected function getFile(array $info) : FileInterface { + return $this->entityTypeManager->getStorage('file')->load($info['target_id']); + } + + /** + * Loads and marks the target file as permanent. + * + * @param array $info + * An associative array containing at least: + * - target_id: The id of the file to load. + * + * @return \Drupal\file\FileInterface + * The loaded file, after it has been marked as permanent. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + * @throws \Drupal\Core\Entity\EntityStorageException + */ + protected function persistFile(array $info) : FileInterface { + $file = $this->getFile($info); + $file->setPermanent(); + if ($file->save() !== SAVED_UPDATED) { + throw new \Exception("Failed to update file '{$file->id()}' to be permanent."); + } + + return $file; + } + + /** + * Get the node to which to attach our media. + * + * @param array $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(array $info, array $values) : NodeInterface; + + /** + * 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 array $info + * The widget info, which should have a 'target_id' identifying the target + * file. + * @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, array $info, array $values) : MediaInterface { + $taxonomy_term_storage = $this->entityTypeManager->getStorage('taxonomy_term'); + + $file = $this->getFile($info); + + // 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' => $file->getFilename(), + 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 for file '{$file->id()}."); + } + + 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.')); + } + } + +} diff --git a/src/Form/AddChildrenWizard/FileSelectionForm.php b/src/Form/AddChildrenWizard/AbstractFileSelectionForm.php similarity index 67% rename from src/Form/AddChildrenWizard/FileSelectionForm.php rename to src/Form/AddChildrenWizard/AbstractFileSelectionForm.php index 2e505c09..c8436a1a 100644 --- a/src/Form/AddChildrenWizard/FileSelectionForm.php +++ b/src/Form/AddChildrenWizard/AbstractFileSelectionForm.php @@ -3,7 +3,6 @@ namespace Drupal\islandora\Form\AddChildrenWizard; use Drupal\Core\Batch\BatchBuilder; -use Drupal\Core\Database\Connection; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemList; use Drupal\Core\Field\FieldStorageDefinitionInterface; @@ -11,27 +10,17 @@ use Drupal\Core\Field\WidgetInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountProxyInterface; -use Drupal\Core\Url; use Drupal\media\MediaTypeInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Children addition wizard's second step. */ -class FileSelectionForm extends FormBase { +abstract class AbstractFileSelectionForm extends FormBase { - use WizardTrait { - WizardTrait::getField as doGetField; - WizardTrait::getMediaType as doGetMediaType; - WizardTrait::getWidget as doGetWidget; - } + use WizardTrait; - /** - * The database connection serivce. - * - * @var \Drupal\Core\Database\Connection|null - */ - protected ?Connection $database; + const BATCH_PROCESSOR = 'abstract.abstract'; /** * The current user. @@ -43,9 +32,9 @@ class FileSelectionForm extends FormBase { /** * The batch processor service. * - * @var \Drupal\islandora\Form\AddChildrenWizard\ChildBatchProcessor|null + * @var \Drupal\islandora\Form\AddChildrenWizard\AbstractBatchProcessor|null */ - protected ?ChildBatchProcessor $batchProcessor; + protected ?AbstractBatchProcessor $batchProcessor; /** * {@inheritdoc} @@ -56,21 +45,13 @@ class FileSelectionForm extends FormBase { $instance->entityTypeManager = $container->get('entity_type.manager'); $instance->widgetPluginManager = $container->get('plugin.manager.field.widget'); $instance->entityFieldManager = $container->get('entity_field.manager'); - $instance->database = $container->get('database'); $instance->currentUser = $container->get('current_user'); - $instance->batchProcessor = $container->get('islandora.upload_children.batch_processor'); + $instance->batchProcessor = $container->get(static::BATCH_PROCESSOR); return $instance; } - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'islandora_add_children_wizard_file_selection'; - } - /** * Helper; get the media type, based off discovering from form state. * @@ -83,8 +64,8 @@ class FileSelectionForm extends FormBase { * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ - protected function getMediaType(FormStateInterface $form_state): MediaTypeInterface { - return $this->doGetMediaType($form_state->getTemporaryValue('wizard')); + protected function getMediaTypeFromFormState(FormStateInterface $form_state): MediaTypeInterface { + return $this->getMediaType($form_state->getTemporaryValue('wizard')); } /** @@ -96,10 +77,10 @@ class FileSelectionForm extends FormBase { * @return \Drupal\Core\Field\FieldDefinitionInterface * The field definition. */ - protected function getField(FormStateInterface $form_state): FieldDefinitionInterface { + protected function getFieldFromFormState(FormStateInterface $form_state): FieldDefinitionInterface { $cached_values = $form_state->getTemporaryValue('wizard'); - $field = $this->doGetField($cached_values); + $field = $this->getField($cached_values); $field->getFieldStorageDefinition()->set('cardinality', FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); return $field; @@ -114,8 +95,8 @@ class FileSelectionForm extends FormBase { * @return \Drupal\Core\Field\WidgetInterface * The widget. */ - protected function getWidget(FormStateInterface $form_state): WidgetInterface { - return $this->doGetWidget($this->getField($form_state)); + protected function getWidgetFromFormState(FormStateInterface $form_state): WidgetInterface { + return $this->getWidget($this->getFieldFromFormState($form_state)); } /** @@ -125,12 +106,12 @@ class FileSelectionForm extends FormBase { // 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->getField($form_state); - $items = FieldItemList::createInstance($field, $field->getName(), $this->getMediaType($form_state)->getTypedData()); + $field = $this->getFieldFromFormState($form_state); + $items = FieldItemList::createInstance($field, $field->getName(), $this->getMediaTypeFromFormState($form_state)->getTypedData()); $form['#tree'] = TRUE; $form['#parents'] = []; - $widget = $this->getWidget($form_state); + $widget = $this->getWidgetFromFormState($form_state); $form['files'] = $widget->form( $items, $form, @@ -146,21 +127,20 @@ class FileSelectionForm extends FormBase { public function submitForm(array &$form, FormStateInterface $form_state) { $cached_values = $form_state->getTemporaryValue('wizard'); - $widget = $this->getWidget($form_state); + $widget = $this->getWidgetFromFormState($form_state); $builder = (new BatchBuilder()) ->setTitle($this->t('Creating children...')) ->setInitMessage($this->t('Initializing...')) ->setFinishCallback([$this->batchProcessor, 'batchProcessFinished']); - $values = $form_state->getValue($this->doGetField($cached_values)->getName()); + $values = $form_state->getValue($this->getField($cached_values)->getName()); $massaged_values = $widget->massageFormValues($values, $form, $form_state); - foreach ($massaged_values as $delta => $file) { + foreach ($massaged_values as $delta => $info) { $builder->addOperation( [$this->batchProcessor, 'batchOperation'], - [$delta, $file, $cached_values] + [$delta, $info, $cached_values] ); } batch_set($builder->toArray()); - $form_state->setRedirectUrl(Url::fromUri("internal:/node/{$cached_values['node']}/members")); } } diff --git a/src/Form/AddChildrenWizard/Form.php b/src/Form/AddChildrenWizard/AbstractForm.php similarity index 82% rename from src/Form/AddChildrenWizard/Form.php rename to src/Form/AddChildrenWizard/AbstractForm.php index 97ac8b23..251953f6 100644 --- a/src/Form/AddChildrenWizard/Form.php +++ b/src/Form/AddChildrenWizard/AbstractForm.php @@ -14,7 +14,12 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * Bulk children addition wizard base form. */ -class Form extends FormWizardBase { +abstract class AbstractForm extends FormWizardBase { + + const TEMPSTORE_ID = 'abstract.abstract'; + const TYPE_SELECTION_FORM = MediaTypeSelectionForm::class; + const FILE_SELECTION_FORM = AbstractFileSelectionForm::class; + const BATCH_PROCESSOR_SERVICE_NAME = 'abstract.abstract'; /** * The Islandora Utils service. @@ -72,39 +77,28 @@ class Form extends FormWizardBase { return array_merge( parent::getParameters(), [ - 'tempstore_id' => 'islandora.upload_children', + 'tempstore_id' => static::TEMPSTORE_ID, 'current_user' => \Drupal::service('current_user'), - 'batch_processor' => \Drupal::service('islandora.upload_children.batch_processor'), ] ); } - /** - * {@inheritdoc} - */ - public function getMachineName() { - return strtr("islandora_add_children_wizard__{userid}__{nodeid}", [ - '{userid}' => $this->currentUser->id(), - '{nodeid}' => $this->nodeId, - ]); - } - /** * {@inheritdoc} */ public function getOperations($cached_values) { $ops = []; - $ops['child_type'] = [ + $ops['type_selection'] = [ 'title' => $this->t('Type of children'), - 'form' => TypeSelectionForm::class, + 'form' => static::TYPE_SELECTION_FORM, 'values' => [ 'node' => $this->nodeId, ], ]; - $ops['child_files'] = [ + $ops['file_selection'] = [ 'title' => $this->t('Files for children'), - 'form' => FileSelectionForm::class, + 'form' => static::FILE_SELECTION_FORM, 'values' => [ 'node' => $this->nodeId, ], diff --git a/src/Form/AddChildrenWizard/Access.php b/src/Form/AddChildrenWizard/Access.php index 2a5cdbe2..0adafde5 100644 --- a/src/Form/AddChildrenWizard/Access.php +++ b/src/Form/AddChildrenWizard/Access.php @@ -49,15 +49,23 @@ class Access implements ContainerInjectionInterface { * @return \Drupal\Core\Access\AccessResultInterface * Whether we can or cannot show the "thing". */ - public function checkAccess(RouteMatch $route_match) : AccessResultInterface { - $can_create_media = $this->utils->canCreateIslandoraEntity('media', 'media_type'); - $can_create_node = $this->utils->canCreateIslandoraEntity('node', 'node_type'); + public function childAccess(RouteMatch $route_match) : AccessResultInterface { + return AccessResult::allowedIf($this->utils->canCreateIslandoraEntity('node', 'node_type')) + ->andIf($this->mediaAccess($route_match)); - if ($can_create_media && $can_create_node) { - return AccessResult::allowed(); - } + } - return AccessResult::forbidden(); + /** + * 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')); } } diff --git a/src/Form/AddChildrenWizard/ChildBatchProcessor.php b/src/Form/AddChildrenWizard/ChildBatchProcessor.php index ccd07b99..9be9902a 100644 --- a/src/Form/AddChildrenWizard/ChildBatchProcessor.php +++ b/src/Form/AddChildrenWizard/ChildBatchProcessor.php @@ -2,189 +2,57 @@ namespace Drupal\islandora\Form\AddChildrenWizard; -use Drupal\Core\Database\Connection; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Session\AccountProxyInterface; -use Drupal\Core\Url; use Drupal\islandora\IslandoraUtils; use Drupal\node\NodeInterface; -use Symfony\Component\HttpKernel\Exception\HttpException; -use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; /** * Children addition batch processor. */ -class ChildBatchProcessor { - - use FieldTrait; - - /** - * The entity type manager service. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface|null - */ - protected ?EntityTypeManagerInterface $entityTypeManager; - - /** - * 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; - - /** - * Constructor. - */ - public function __construct( - EntityTypeManagerInterface $entity_type_manager, - Connection $database, - AccountProxyInterface $current_user - ) { - $this->entityTypeManager = $entity_type_manager; - $this->database = $database; - $this->currentUser = $current_user; - } - - /** - * Implements callback_batch_operation() for our child addition batch. - */ - public function batchOperation($delta, $info, array $values, &$context) { - $transaction = $this->database->startTransaction(); - - try { - $taxonomy_term_storage = $this->entityTypeManager->getStorage('taxonomy_term'); - - /** @var \Drupal\file\FileInterface $file */ - $file = $this->entityTypeManager->getStorage('file')->load($info['target_id']); - $file->setPermanent(); - if ($file->save() !== SAVED_UPDATED) { - throw new \Exception("Failed to update file '{$file->id()}' to be permanent."); - } - - $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' => $file->getFilename(), - 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 for file '{$file->id()}'."); - } - - // 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' => $file->getFilename(), - 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 for file '{$file->id()}."); - } - - $context['results'] = array_merge_recursive($context['results'], [ - 'validation_violations' => $this->validationClassification([ - $file, - $media, - $node, - ]), - ]); - $context['results']['count'] += 1; - } - catch (HttpExceptionInterface $e) { - $transaction->rollBack(); - throw $e; - } - catch (\Exception $e) { - $transaction->rollBack(); - throw new HttpException(500, $e->getMessage(), $e); - } - } +class ChildBatchProcessor extends AbstractBatchProcessor { /** - * 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. + * {@inheritdoc} */ - 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(); - } + protected function getNode(array $info, array $values) : NodeInterface { + $taxonomy_term_storage = $this->entityTypeManager->getStorage('taxonomy_term'); + $node_storage = $this->entityTypeManager->getStorage('node'); + $parent = $node_storage->load($values['node']); + $file = $this->getFile($info); + + // 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' => $file->getFilename(), + 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 for file '{$file->id()}'."); } - return $violations; + return $node; } /** - * Implements callback_batch_finished() for our child addition batch. + * {@inheritdoc} */ public function batchProcessFinished($success, $results, $operations): void { if ($success) { - $this->messenger()->addMessage($this->formatPlural( + $this->messenger->addMessage($this->formatPlural( $results['count'], 'Added 1 child node.', 'Added @count child nodes.' )); - 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 adding children.')); } + + parent::batchProcessFinished($success, $results, $operations); } } diff --git a/src/Form/AddChildrenWizard/ChildFileSelectionForm.php b/src/Form/AddChildrenWizard/ChildFileSelectionForm.php new file mode 100644 index 00000000..9783d082 --- /dev/null +++ b/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")); + } + +} diff --git a/src/Form/AddChildrenWizard/ChildForm.php b/src/Form/AddChildrenWizard/ChildForm.php new file mode 100644 index 00000000..0b9a197f --- /dev/null +++ b/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, + ]); + } + +} diff --git a/src/Form/AddChildrenWizard/ChildTypeSelectionForm.php b/src/Form/AddChildrenWizard/ChildTypeSelectionForm.php new file mode 100644 index 00000000..843cc85b --- /dev/null +++ b/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() { + 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() { + return array_merge( + parent::keysToSave(), + [ + 'bundle', + 'model', + ] + ); + } + +} diff --git a/src/Form/AddChildrenWizard/FieldTrait.php b/src/Form/AddChildrenWizard/FieldTrait.php index 156000e3..743fc619 100644 --- a/src/Form/AddChildrenWizard/FieldTrait.php +++ b/src/Form/AddChildrenWizard/FieldTrait.php @@ -17,7 +17,7 @@ trait FieldTrait { * * @var \Drupal\Core\Entity\EntityFieldManagerInterface|null */ - protected ?EntityFieldManagerInterface $entityFieldManager; + protected ?EntityFieldManagerInterface $entityFieldManager = NULL; /** * Helper; get field instance, given our required values. diff --git a/src/Form/AddChildrenWizard/MediaBatchProcessor.php b/src/Form/AddChildrenWizard/MediaBatchProcessor.php new file mode 100644 index 00000000..f069d0bf --- /dev/null +++ b/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(array $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); + } + +} diff --git a/src/Form/AddChildrenWizard/MediaFileSelectionForm.php b/src/Form/AddChildrenWizard/MediaFileSelectionForm.php new file mode 100644 index 00000000..534c7309 --- /dev/null +++ b/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")); + } + +} diff --git a/src/Form/AddChildrenWizard/MediaForm.php b/src/Form/AddChildrenWizard/MediaForm.php new file mode 100644 index 00000000..2e6fa217 --- /dev/null +++ b/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, + ]); + } + +} diff --git a/src/Form/AddChildrenWizard/TypeSelectionForm.php b/src/Form/AddChildrenWizard/MediaTypeSelectionForm.php similarity index 62% rename from src/Form/AddChildrenWizard/TypeSelectionForm.php rename to src/Form/AddChildrenWizard/MediaTypeSelectionForm.php index 68237711..0e687d58 100644 --- a/src/Form/AddChildrenWizard/TypeSelectionForm.php +++ b/src/Form/AddChildrenWizard/MediaTypeSelectionForm.php @@ -14,7 +14,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** * Children addition wizard's first step. */ -class TypeSelectionForm extends FormBase { +class MediaTypeSelectionForm extends FormBase { /** * Cacheable metadata that is instantiated and used internally. @@ -61,87 +61,7 @@ class TypeSelectionForm extends FormBase { * {@inheritdoc} */ public function getFormId() { - 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]; - } + return 'islandora_add_media_type_selection'; } /** @@ -237,33 +157,6 @@ class TypeSelectionForm extends FormBase { ]); $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, - ], - ], - ]; $form['media_type'] = [ '#type' => 'select', '#title' => $this->t('Media Type'), @@ -297,17 +190,24 @@ class TypeSelectionForm extends FormBase { } /** - * {@inheritdoc} + * Helper; enumerate keys to persist in form state. + * + * @return string[] + * The keys to be persisted in our temp value in form state. */ - public function submitForm(array &$form, FormStateInterface $form_state) { - $keys = [ - 'bundle', - 'model', + protected static function keysToSave() { + return [ 'media_type', 'use', ]; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { $cached_values = $form_state->getTemporaryValue('wizard'); - foreach ($keys as $key) { + foreach (static::keysToSave() as $key) { $cached_values[$key] = $form_state->getValue($key); } $form_state->setTemporaryValue('wizard', $cached_values); diff --git a/src/Form/AddChildrenWizard/MediaTypeTrait.php b/src/Form/AddChildrenWizard/MediaTypeTrait.php index e2caf2c2..5600210b 100644 --- a/src/Form/AddChildrenWizard/MediaTypeTrait.php +++ b/src/Form/AddChildrenWizard/MediaTypeTrait.php @@ -15,7 +15,7 @@ trait MediaTypeTrait { * * @var \Drupal\Core\Entity\EntityTypeManagerInterface|null */ - protected ?EntityTypeManagerInterface $entityTypeManager; + protected ?EntityTypeManagerInterface $entityTypeManager = NULL; /** * Helper; get media type, given our required values. diff --git a/src/Form/AddMediaForm.php b/src/Form/AddMediaForm.php index 281abd9a..5078d1a8 100644 --- a/src/Form/AddMediaForm.php +++ b/src/Form/AddMediaForm.php @@ -23,6 +23,8 @@ use Symfony\Component\HttpKernel\Exception\HttpException; /** * Form that lets users upload one or more files as children to a resource node. + * + * @deprecated Use the \Drupal\islandora\Form\AddChildrenWizard\MediaForm instead. */ class AddMediaForm extends FormBase {