From e69d918a690a975a1feaa178585a66c04882511e Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Thu, 28 Jul 2022 11:33:51 -0300 Subject: [PATCH 01/21] Add ctools, prior to using it. --- composer.json | 3 ++- islandora.info.yml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 24eb9e5ba..a4db6d035 100644 --- a/composer.json +++ b/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" }, "require-dev": { "phpunit/phpunit": "^6", diff --git a/islandora.info.yml b/islandora.info.yml index 9cd89cddc..7a6e4d0ae 100644 --- a/islandora.info.yml +++ b/islandora.info.yml @@ -32,3 +32,4 @@ dependencies: - drupal:flysystem - drupal:token - drupal:file_replace + - ctools:ctools From 47920445f18dee78ded134820756d0805b2e99c1 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Thu, 28 Jul 2022 11:38:40 -0300 Subject: [PATCH 02/21] Fix up all the dependency references. ... before the colon is the project name, so should only be "drupal" for modules shipped in core. --- islandora.info.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/islandora.info.yml b/islandora.info.yml index 7a6e4d0ae..0336d89d3 100644 --- a/islandora.info.yml +++ b/islandora.info.yml @@ -13,23 +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 From 71f48ff67543170b880dc1084f31a00f7171a996 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Wed, 3 Aug 2022 17:13:24 -0300 Subject: [PATCH 03/21] Some more together. --- .../AddChildrenWizard/FileSelectionForm.php | 25 +++ src/Form/AddChildrenWizard/Form.php | 25 +++ .../AddChildrenWizard/TypeSelectionForm.php | 196 ++++++++++++++++++ 3 files changed, 246 insertions(+) create mode 100644 src/Form/AddChildrenWizard/FileSelectionForm.php create mode 100644 src/Form/AddChildrenWizard/Form.php create mode 100644 src/Form/AddChildrenWizard/TypeSelectionForm.php diff --git a/src/Form/AddChildrenWizard/FileSelectionForm.php b/src/Form/AddChildrenWizard/FileSelectionForm.php new file mode 100644 index 000000000..5ea6581d3 --- /dev/null +++ b/src/Form/AddChildrenWizard/FileSelectionForm.php @@ -0,0 +1,25 @@ +setError($form, 'Oh no!'); + } +} diff --git a/src/Form/AddChildrenWizard/Form.php b/src/Form/AddChildrenWizard/Form.php new file mode 100644 index 000000000..2be973bec --- /dev/null +++ b/src/Form/AddChildrenWizard/Form.php @@ -0,0 +1,25 @@ + [ + 'title' => $this->t('Type of children'), + 'form' => TypeSelectionForm::class, + ], + 'child_files' => [ + 'title' => $this->t('Files for children'), + 'form' => FileSelectionForm::class, + ] + ]; + } +} diff --git a/src/Form/AddChildrenWizard/TypeSelectionForm.php b/src/Form/AddChildrenWizard/TypeSelectionForm.php new file mode 100644 index 000000000..41882e8ec --- /dev/null +++ b/src/Form/AddChildrenWizard/TypeSelectionForm.php @@ -0,0 +1,196 @@ +entityTypeBundleInfo = $container->get('entity_type.bundle.info'); + $instance->entityTypeManager = $container->get('entity_type.manager'); + $instance->entityFieldManager = $container->get('entity_field.manager'); + + return $instance; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'islandora_add_children_type_selection'; + } + + protected ?array $nodeBundleOptions = NULL; + protected ?array $nodeBundleHasModelField = NULL; + //protected ?array $nodeBundleHasMemberOfField = NULL; + protected function getNodeBundleOptions() : array { + if ($this->nodeBundleOptions === NULL) { + $this->nodeBundleOptions = []; + $this->nodeBundleHasModelField = []; + //$this->nodeBundleHasMemberOfField = []; + + $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->nodeBundleHasMemberOfField[$bundle] = array_key_exists(IslandoraUtils::MEMBER_OF_FIELD, $fields); + $this->nodeBundleHasModelField[$bundle] = array_key_exists(IslandoraUtils::MODEL_FIELD, $fields); + } + } + + return $this->nodeBundleOptions; + } + + protected function getModelOptions() : \Traversable { + $terms = $this->entityTypeManager->getStorage('taxonomy_term') + ->loadTree('islandora_models', 0, NULL, TRUE); + foreach ($terms as $term) { + yield $term->id() => $term->getName(); + } + } + + protected function mapModelStates() : \Traversable { + $this->getNodeBundleOptions(); + foreach (array_keys(array_filter($this->nodeBundleHasModelField)) as $bundle) { + yield ['value' => $bundle]; + } + } + + protected ?array $mediaBundleOptions = NULL; + protected ?array $mediaBundleUsageField = NULL; + 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) { + $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; + } + + protected function getMediaUseOptions() { + /** @var TermInterface[] $terms */ + $terms = $this->entityTypeManager->getStorage('taxonomy_term') + ->loadTree('islandora_media_use', 0, NULL, TRUE); + + foreach ($terms as $term) { + yield $term->id() => $term->getName(); + } + } + protected function mapUseStates() { + $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); + + $form['bundle'] = [ + '#type' => 'select', + '#title' => $this->t('Content Type'), + '#description' => $this->t('Each child created will have this content type.'), + '#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()), + '#states' => [ + 'visible' => [ + ':input[name="bundle"]' => $model_states, + ], + 'required' => [ + ':input[name="bundle"]' => $model_states, + ], + ] + ]; + $form['media_type'] = [ + '#type' => 'select', + '#title' => $this->t('Media Type'), + '#description' => $this->t('Each media created will have this 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 Portland Common Data Model: Use Extension. "Original File" will trigger creation of derivatives.', [ + ':url' => 'https://pcdm.org/2015/05/12/use', + ]), + '#options' => iterator_to_array($this->getMediaUseOptions()), + '#states' => [ + 'visible' => [ + ':input[name="media_type"]' => $use_states, + ], + 'required' => [ + ':input[name="media_type"]' => $use_states, + ], + ], + ]; + + $this->cacheableMetadata->applyTo($form); + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // TODO: Implement submitForm() method. + } +} From 593f175ac319b4803888b57bce989852e3e68f72 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Fri, 5 Aug 2022 14:48:39 -0300 Subject: [PATCH 04/21] Decent progress... getting things actually rendering... ... bit of refactoring stuff making a mess. --- islandora.routing.yml | 14 +- src/Form/AddChildrenForm.php | 2 +- .../AddChildrenWizard/FileSelectionForm.php | 180 +++++++++++++++++- src/Form/AddChildrenWizard/Form.php | 84 +++++++- .../AddChildrenWizard/TypeSelectionForm.php | 27 ++- 5 files changed, 299 insertions(+), 8 deletions(-) diff --git a/islandora.routing.yml b/islandora.routing.yml index 5387e9a47..9215a3609 100644 --- a/islandora.routing.yml +++ b/islandora.routing.yml @@ -37,7 +37,7 @@ islandora.add_member_to_node_page: _entity_create_any_access: 'node' islandora.upload_children: - path: '/node/{node}/members/upload' + path: '/node/{node}/members/upload_old' defaults: _form: '\Drupal\islandora\Form\AddChildrenForm' _title: 'Upload Children' @@ -46,6 +46,18 @@ islandora.upload_children: requirements: _custom_access: '\Drupal\islandora\Form\AddChildrenForm::access' +islandora.upload_children_wizard: + path: '/node/{node}/members/upload/{step}' + defaults: + _wizard: '\Drupal\islandora\Form\AddChildrenWizard\Form' + _title: 'Upload Children' + step: 'child_type' + options: + _admin_route: 'TRUE' + requirements: + #_custom_access: '\Drupal\islandora\Form\AddChildrenWizard\Form::access' + _custom_access: '\Drupal\islandora\Form\AddChildrenForm::access' + islandora.add_media_to_node_page: path: '/node/{node}/media/add' defaults: diff --git a/src/Form/AddChildrenForm.php b/src/Form/AddChildrenForm.php index 0ff724962..528b42832 100644 --- a/src/Form/AddChildrenForm.php +++ b/src/Form/AddChildrenForm.php @@ -229,7 +229,7 @@ public function buildNodeFinished($success, $results, $operations) { * @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) { diff --git a/src/Form/AddChildrenWizard/FileSelectionForm.php b/src/Form/AddChildrenWizard/FileSelectionForm.php index 5ea6581d3..f056d7d6a 100644 --- a/src/Form/AddChildrenWizard/FileSelectionForm.php +++ b/src/Form/AddChildrenWizard/FileSelectionForm.php @@ -2,24 +2,198 @@ namespace Drupal\islandora\Form\AddChildrenWizard; +use Drupal\Component\Plugin\PluginManagerInterface; +use Drupal\Core\Batch\BatchBuilder; +use Drupal\Core\Database\Connection; +use Drupal\Core\Entity\EntityFieldManagerInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Field\Entity\BaseFieldOverride; +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Field\FieldItemList; +use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\Core\Field\WidgetInterface; +use Drupal\Core\Field\WidgetPluginManager; use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormState; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Queue\QueueFactory; +use Drupal\Core\Queue\QueueInterface; +use Drupal\Core\Session\AccountProxyInterface; +use Drupal\file\FileInterface; +use Drupal\islandora\IslandoraUtils; +use Drupal\media\MediaSourceInterface; +use Drupal\media\MediaTypeInterface; +use Drupal\node\NodeInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; class FileSelectionForm extends FormBase { + protected ?EntityTypeManagerInterface $entityTypeManager; + + /** + * The widget plugin manager service. + * + * @var WidgetPluginManager + */ + protected ?PluginManagerInterface $widgetPluginManager; + + protected ?EntityFieldManagerInterface $entityFieldManager; + + protected ?Connection $database; + + protected ?AccountProxyInterface $currentUser; + + public static function create(ContainerInterface $container) { + $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->database = $container->get('database'); + $instance->currentUser = $container->get('current_user'); + + return $instance; + } + public function getFormId() { return 'islandora_add_children_wizard_file_selection'; } - public function buildForm(array $form, FormStateInterface $form_state) { + protected function getMediaType(FormStateInterface $form_state) : MediaTypeInterface { + return $this->doGetMediaType($form_state->getTemporaryValue('wizard')); + } + + protected function doGetMediaType(array $values) : MediaTypeInterface { + /** @var MediaTypeInterface $media_type */ + return $this->entityTypeManager->getStorage('media_type')->load($values['media_type']); + } + + protected function getField(FormStateInterface $form_state) : FieldDefinitionInterface { + $cached_values = $form_state->getTemporaryValue('wizard'); + + $field = $this->doGetField($cached_values); + $field->getFieldStorageDefinition()->set('cardinality', FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); + + return $field; + } + + protected function doGetField(array $values) : FieldDefinitionInterface { + $media_type = $this->doGetMediaType($values); + $media_source = $media_type->getSource(); + $source_field = $media_source->getSourceFieldDefinition($media_type); + + $fields = $this->entityFieldManager->getFieldDefinitions('media', $media_type->id()); + + return isset($fields[$source_field->getFieldStorageDefinition()->getName()]) ? + $fields[$source_field->getFieldStorageDefinition()->getName()] : + $media_source->createSourceField(); + } + + protected function getWidget(FormStateInterface $form_state) : WidgetInterface { + return $this->widgetPluginManager->getInstance([ + 'field_definition' => $this->getField($form_state), + 'form_mode' => 'default', + 'prepare' => TRUE, + ]); + } + + public function buildForm(array $form, FormStateInterface $form_state) : array { // TODO: 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()); + + $form['#tree'] = TRUE; + $form['#parents'] = []; + $widget = $this->getWidget($form_state); + $form['files'] = $widget->form( + $items, + $form, + $form_state + ); + return $form; } + /** + * {@inheritdoc} + */ public function submitForm(array &$form, FormStateInterface $form_state) { - // TODO: Implement submitForm() method. - $form_state->setError($form, 'Oh no!'); + $cached_values = $form_state->getTemporaryValue('wizard'); + + dsm($form_state); + + $builder = (new BatchBuilder()) + ->setTitle($this->t('Creating children...')) + ->setInitMessage($this->t('Initializing...')) + ->setFinishCallback([$this, 'batchProcessFinished']); + foreach ($form_state->getValue($this->doGetField($cached_values)->getName()) as $file) { + $builder->addOperation([$this, 'batchProcess'], [$file, $cached_values]); + } + batch_set($builder->toArray()); } + + public function batchProcess($fid, array $values, &$context) { + $transaction = \Drupal::database()->startTransaction(); + + try { + $taxonomy_term_storage = $this->entityTypeManager->getStorage('taxonomy_term'); + + /** @var FileInterface $file */ + $file = $this->entityTypeManager->getStorage('file')->load($fid); + $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). + $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. + $media = $this->entityTypeManager->getStorage('media')->create([ + '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, + $this->doGetField($values)->getName() => $file->id(), + ]); + if ($media->save() !== SAVED_NEW) { + throw new \Exception("Failed to create media for file '{$file->id()}."); + } + } + catch (HttpExceptionInterface $e) { + $transaction->rollBack(); + throw $e; + } + catch (\Exception $e) { + $transaction->rollBack(); + throw new HttpException(500, $e->getMessage(), $e); + } + } + + public function batchProcessFinished() { + // TODO: Dump out status message of some sort? + } + } diff --git a/src/Form/AddChildrenWizard/Form.php b/src/Form/AddChildrenWizard/Form.php index 2be973bec..d689fd37a 100644 --- a/src/Form/AddChildrenWizard/Form.php +++ b/src/Form/AddChildrenWizard/Form.php @@ -2,15 +2,83 @@ namespace Drupal\islandora\Form\AddChildrenWizard; +use Drupal\Core\Access\AccessResult; +use Drupal\Core\DependencyInjection\ClassResolverInterface; +use Drupal\Core\Form\FormBuilderInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\RendererInterface; +use Drupal\Core\Routing\RouteMatch; +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; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; class Form extends FormWizardBase { + protected IslandoraUtils $utils; + protected string $nodeId; + protected RouteMatchInterface $currentRoute; + protected AccountProxyInterface $currentUser; + + /** + * Constructor. + */ + public function __construct( + SharedTempStoreFactory $tempstore, + FormBuilderInterface $builder, + ClassResolverInterface $class_resolver, + EventDispatcherInterface $event_dispatcher, + RouteMatchInterface $route_match, + $tempstore_id, + IslandoraUtils $utils, + RouteMatchInterface $current_route_match, + AccountProxyInterface $current_user, + $machine_name = NULL, + $step = NULL + ) { + parent::__construct($tempstore, $builder, $class_resolver, $event_dispatcher, $route_match, $tempstore_id, + $machine_name, $step); + + $this->utils = $utils; + $this->currentRoute = $current_route_match; + $this->nodeId = $this->currentRoute->getParameter('node'); + $this->currentUser = $current_user; + } + + /** + * {@inheritdoc} + */ + public static function getParameters() : array { + return array_merge( + parent::getParameters(), + [ + 'utils' => \Drupal::service('islandora.utils'), + 'tempstore_id' => 'islandora.upload_children', + //'machine_name' => 'islandora_add_children_wizard', + 'current_route_match' => \Drupal::service('current_route_match'), + 'current_user' => \Drupal::service('current_user'), + ] + ); + } + + /** + * {@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) { - // TODO: Implement getOperations() method. return [ 'child_type' => [ 'title' => $this->t('Type of children'), @@ -22,4 +90,18 @@ public function getOperations($cached_values) { ] ]; } + + public function getNextParameters($cached_values) { + return parent::getNextParameters($cached_values) + ['node' => $this->nodeId]; + } + + public function getPreviousParameters($cached_values) { + return parent::getPreviousParameters($cached_values) + ['node' => $this->nodeId]; + } + + public function finish(array &$form, FormStateInterface $form_state) { + parent::finish($form, $form_state); // TODO: Change the autogenerated stub + dsm($form_state->getTemporaryValue('wizard')); + } + } diff --git a/src/Form/AddChildrenWizard/TypeSelectionForm.php b/src/Form/AddChildrenWizard/TypeSelectionForm.php index 41882e8ec..fffb2ae97 100644 --- a/src/Form/AddChildrenWizard/TypeSelectionForm.php +++ b/src/Form/AddChildrenWizard/TypeSelectionForm.php @@ -133,12 +133,19 @@ protected function mapUseStates() { * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { - $this->cacheableMetadata = CacheableMetadata::createFromRenderArray($form); + $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, ]; @@ -149,6 +156,8 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#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, @@ -162,6 +171,8 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#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, ]; @@ -173,6 +184,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { ':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, @@ -191,6 +203,17 @@ public function buildForm(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { - // TODO: Implement submitForm() method. + $keys = [ + 'bundle', + 'model', + 'media_type', + 'use', + ]; + $cached_values = $form_state->getTemporaryValue('wizard'); + foreach ($keys as $key) { + $cached_values[$key] = $form_state->getValue($key); + } + $form_state->setTemporaryValue('wizard', $cached_values); } + } From 648a5985945b8ee151188a3c8b7df16596d556ce Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 8 Aug 2022 11:53:08 -0300 Subject: [PATCH 05/21] More worky. ... as in, basically functional. Still needs coding standards pass, and testing with more/all types of content. --- .../AddChildrenWizard/FileSelectionForm.php | 64 ++++++++++++++----- src/Form/AddChildrenWizard/Form.php | 6 ++ 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/src/Form/AddChildrenWizard/FileSelectionForm.php b/src/Form/AddChildrenWizard/FileSelectionForm.php index f056d7d6a..5fb29c434 100644 --- a/src/Form/AddChildrenWizard/FileSelectionForm.php +++ b/src/Form/AddChildrenWizard/FileSelectionForm.php @@ -19,8 +19,10 @@ use Drupal\Core\Queue\QueueFactory; use Drupal\Core\Queue\QueueInterface; use Drupal\Core\Session\AccountProxyInterface; +use Drupal\Core\Url; use Drupal\file\FileInterface; use Drupal\islandora\IslandoraUtils; +use Drupal\media\MediaInterface; use Drupal\media\MediaSourceInterface; use Drupal\media\MediaTypeInterface; use Drupal\node\NodeInterface; @@ -45,7 +47,7 @@ class FileSelectionForm extends FormBase { protected ?AccountProxyInterface $currentUser; - public static function create(ContainerInterface $container) { + public static function create(ContainerInterface $container): self { $instance = parent::create($container); $instance->entityTypeManager = $container->get('entity_type.manager'); @@ -92,8 +94,12 @@ protected function doGetField(array $values) : FieldDefinitionInterface { } protected function getWidget(FormStateInterface $form_state) : WidgetInterface { + return $this->doGetWidget($this->getField($form_state)); + } + + protected function doGetWidget(FieldDefinitionInterface $field) : WidgetInterface { return $this->widgetPluginManager->getInstance([ - 'field_definition' => $this->getField($form_state), + 'field_definition' => $field, 'form_mode' => 'default', 'prepare' => TRUE, ]); @@ -125,26 +131,37 @@ public function buildForm(array $form, FormStateInterface $form_state) : array { public function submitForm(array &$form, FormStateInterface $form_state) { $cached_values = $form_state->getTemporaryValue('wizard'); + dsm($form); dsm($form_state); + dsm($this->doGetField($cached_values)); + dsm($this->doGetField($cached_values)->getName()); + $widget = $this->getWidget($form_state); $builder = (new BatchBuilder()) ->setTitle($this->t('Creating children...')) ->setInitMessage($this->t('Initializing...')) ->setFinishCallback([$this, 'batchProcessFinished']); - foreach ($form_state->getValue($this->doGetField($cached_values)->getName()) as $file) { - $builder->addOperation([$this, 'batchProcess'], [$file, $cached_values]); + $values = $form_state->getValue($this->doGetField($cached_values)->getName()); + $massaged_values = $widget->massageFormValues($values, $form, $form_state); + dsm($values); + dsm($massaged_values); + foreach ($massaged_values as $delta => $file) { + dsm($file); + $builder->addOperation([$this, 'batchProcess'], [$delta, $file, $form, $form_state, $cached_values]); } batch_set($builder->toArray()); + $form_state->setRedirectUrl(Url::fromUri("internal:/node/{$cached_values['node']}/members")); } - public function batchProcess($fid, array $values, &$context) { + public function batchProcess($delta, $info, array $form, FormStateInterface $form_state, array $values, &$context) { $transaction = \Drupal::database()->startTransaction(); try { $taxonomy_term_storage = $this->entityTypeManager->getStorage('taxonomy_term'); + dsm(func_get_args()); /** @var FileInterface $file */ - $file = $this->entityTypeManager->getStorage('file')->load($fid); + $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."); @@ -169,15 +186,32 @@ public function batchProcess($fid, array $values, &$context) { } // Create a media with the file attached and also pointing at the node. - $media = $this->entityTypeManager->getStorage('media')->create([ - '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, - $this->doGetField($values)->getName() => $file->id(), - ]); + $field = $this->doGetField($values); + $widget = $this->doGetWidget($field); + $items = FieldItemList::createInstance($field, $field->getName(), $this->getMediaType($form_state)->getTypedData()); + $items->setValue([0 => $info]); + //$items->setValue([$delta => $info]); + + $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, + ], + ] + ); + dsm($media_values); + $media = $this->entityTypeManager->getStorage('media')->create($media_values); if ($media->save() !== SAVED_NEW) { throw new \Exception("Failed to create media for file '{$file->id()}."); } diff --git a/src/Form/AddChildrenWizard/Form.php b/src/Form/AddChildrenWizard/Form.php index d689fd37a..bd2b92aeb 100644 --- a/src/Form/AddChildrenWizard/Form.php +++ b/src/Form/AddChildrenWizard/Form.php @@ -83,10 +83,16 @@ public function getOperations($cached_values) { 'child_type' => [ 'title' => $this->t('Type of children'), 'form' => TypeSelectionForm::class, + 'values' => [ + 'node' => $this->nodeId, + ] ], 'child_files' => [ 'title' => $this->t('Files for children'), 'form' => FileSelectionForm::class, + 'values' => [ + 'node' => $this->nodeId, + ] ] ]; } From bbb9fc8726010e0208dd99a1384de0c1313b079c Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 8 Aug 2022 16:21:20 -0300 Subject: [PATCH 06/21] Coding standards, and warning of validation issues. --- islandora.routing.yml | 13 +- src/Form/AddChildrenForm.php | 2 + src/Form/AddChildrenWizard/Access.php | 63 +++++ .../AddChildrenWizard/FileSelectionForm.php | 227 ++++++++++++++---- src/Form/AddChildrenWizard/Form.php | 72 ++++-- .../AddChildrenWizard/TypeSelectionForm.php | 123 +++++++++- 6 files changed, 403 insertions(+), 97 deletions(-) create mode 100644 src/Form/AddChildrenWizard/Access.php diff --git a/islandora.routing.yml b/islandora.routing.yml index 9215a3609..e2cb7d353 100644 --- a/islandora.routing.yml +++ b/islandora.routing.yml @@ -37,16 +37,6 @@ islandora.add_member_to_node_page: _entity_create_any_access: 'node' islandora.upload_children: - path: '/node/{node}/members/upload_old' - defaults: - _form: '\Drupal\islandora\Form\AddChildrenForm' - _title: 'Upload Children' - options: - _admin_route: 'TRUE' - requirements: - _custom_access: '\Drupal\islandora\Form\AddChildrenForm::access' - -islandora.upload_children_wizard: path: '/node/{node}/members/upload/{step}' defaults: _wizard: '\Drupal\islandora\Form\AddChildrenWizard\Form' @@ -55,8 +45,7 @@ islandora.upload_children_wizard: options: _admin_route: 'TRUE' requirements: - #_custom_access: '\Drupal\islandora\Form\AddChildrenWizard\Form::access' - _custom_access: '\Drupal\islandora\Form\AddChildrenForm::access' + _custom_access: '\Drupal\islandora\Form\AddChildrenWizard\Access::checkAccess' islandora.add_media_to_node_page: path: '/node/{node}/media/add' diff --git a/src/Form/AddChildrenForm.php b/src/Form/AddChildrenForm.php index 528b42832..adad9dae9 100644 --- a/src/Form/AddChildrenForm.php +++ b/src/Form/AddChildrenForm.php @@ -12,6 +12,8 @@ /** * Form that lets users upload one or more files as children to a resource node. + * + * @deprecated Replaced with the "wizard" appraach. */ class AddChildrenForm extends AddMediaForm { diff --git a/src/Form/AddChildrenWizard/Access.php b/src/Form/AddChildrenWizard/Access.php new file mode 100644 index 000000000..2a5cdbe21 --- /dev/null +++ b/src/Form/AddChildrenWizard/Access.php @@ -0,0 +1,63 @@ +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 checkAccess(RouteMatch $route_match) : AccessResultInterface { + $can_create_media = $this->utils->canCreateIslandoraEntity('media', 'media_type'); + $can_create_node = $this->utils->canCreateIslandoraEntity('node', 'node_type'); + + if ($can_create_media && $can_create_node) { + return AccessResult::allowed(); + } + + return AccessResult::forbidden(); + } + +} diff --git a/src/Form/AddChildrenWizard/FileSelectionForm.php b/src/Form/AddChildrenWizard/FileSelectionForm.php index 5fb29c434..8d29bab35 100644 --- a/src/Form/AddChildrenWizard/FileSelectionForm.php +++ b/src/Form/AddChildrenWizard/FileSelectionForm.php @@ -7,48 +7,66 @@ use Drupal\Core\Database\Connection; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Field\Entity\BaseFieldOverride; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemList; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Field\WidgetInterface; -use Drupal\Core\Field\WidgetPluginManager; use Drupal\Core\Form\FormBase; -use Drupal\Core\Form\FormState; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Queue\QueueFactory; -use Drupal\Core\Queue\QueueInterface; use Drupal\Core\Session\AccountProxyInterface; use Drupal\Core\Url; -use Drupal\file\FileInterface; use Drupal\islandora\IslandoraUtils; -use Drupal\media\MediaInterface; -use Drupal\media\MediaSourceInterface; use Drupal\media\MediaTypeInterface; use Drupal\node\NodeInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; +/** + * Children addition wizard's second step. + */ class FileSelectionForm extends FormBase { + /** + * The entity type manager service. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface|null + */ protected ?EntityTypeManagerInterface $entityTypeManager; /** * The widget plugin manager service. * - * @var WidgetPluginManager + * @var \Drupal\Core\Field\WidgetPluginManager|null */ protected ?PluginManagerInterface $widgetPluginManager; + /** + * The entity field manager service. + * + * @var \Drupal\Core\Entity\EntityFieldManagerInterface|null + */ protected ?EntityFieldManagerInterface $entityFieldManager; + /** + * 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; + /** + * {@inheritdoc} + */ public static function create(ContainerInterface $container): self { - $instance = parent::create($container); + $instance = parent::create($container); $instance->entityTypeManager = $container->get('entity_type.manager'); $instance->widgetPluginManager = $container->get('plugin.manager.field.widget'); @@ -59,20 +77,57 @@ public static function create(ContainerInterface $container): self { return $instance; } + /** + * {@inheritdoc} + */ public function getFormId() { return 'islandora_add_children_wizard_file_selection'; } - protected function getMediaType(FormStateInterface $form_state) : MediaTypeInterface { + /** + * 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 getMediaType(FormStateInterface $form_state): MediaTypeInterface { return $this->doGetMediaType($form_state->getTemporaryValue('wizard')); } - protected function doGetMediaType(array $values) : MediaTypeInterface { - /** @var MediaTypeInterface $media_type */ - return $this->entityTypeManager->getStorage('media_type')->load($values['media_type']); + /** + * 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 doGetMediaType(array $values): MediaTypeInterface { + /** @var \Drupal\media\MediaTypeInterface $media_type */ + return $this->entityTypeManager->getStorage('media_type')->load($values['media_type']); } - protected function getField(FormStateInterface $form_state) : FieldDefinitionInterface { + /** + * 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 getField(FormStateInterface $form_state): FieldDefinitionInterface { $cached_values = $form_state->getTemporaryValue('wizard'); $field = $this->doGetField($cached_values); @@ -81,23 +136,49 @@ protected function getField(FormStateInterface $form_state) : FieldDefinitionInt return $field; } - protected function doGetField(array $values) : FieldDefinitionInterface { + /** + * Helper; get field instance, given our required values. + * + * @param array $values + * See ::doGetMediaType() for which values are required. + * + * @return \Drupal\Core\Field\FieldDefinitionInterface + * The target field. + */ + protected function doGetField(array $values): FieldDefinitionInterface { $media_type = $this->doGetMediaType($values); $media_source = $media_type->getSource(); $source_field = $media_source->getSourceFieldDefinition($media_type); $fields = $this->entityFieldManager->getFieldDefinitions('media', $media_type->id()); - return isset($fields[$source_field->getFieldStorageDefinition()->getName()]) ? - $fields[$source_field->getFieldStorageDefinition()->getName()] : + return $fields[$source_field->getFieldStorageDefinition()->getName()] ?? $media_source->createSourceField(); } - protected function getWidget(FormStateInterface $form_state) : WidgetInterface { + /** + * 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 getWidget(FormStateInterface $form_state): WidgetInterface { return $this->doGetWidget($this->getField($form_state)); } - protected function doGetWidget(FieldDefinitionInterface $field) : WidgetInterface { + /** + * 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 doGetWidget(FieldDefinitionInterface $field): WidgetInterface { return $this->widgetPluginManager->getInstance([ 'field_definition' => $field, 'form_mode' => 'default', @@ -105,11 +186,13 @@ protected function doGetWidget(FieldDefinitionInterface $field) : WidgetInterfac ]); } - public function buildForm(array $form, FormStateInterface $form_state) : array { - // TODO: Using the media type selected in the previous step, grab the + /** + * {@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->getField($form_state); $items = FieldItemList::createInstance($field, $field->getName(), $this->getMediaType($form_state)->getTypedData()); @@ -131,11 +214,6 @@ public function buildForm(array $form, FormStateInterface $form_state) : array { public function submitForm(array &$form, FormStateInterface $form_state) { $cached_values = $form_state->getTemporaryValue('wizard'); - dsm($form); - dsm($form_state); - dsm($this->doGetField($cached_values)); - dsm($this->doGetField($cached_values)->getName()); - $widget = $this->getWidget($form_state); $builder = (new BatchBuilder()) ->setTitle($this->t('Creating children...')) @@ -143,24 +221,26 @@ public function submitForm(array &$form, FormStateInterface $form_state) { ->setFinishCallback([$this, 'batchProcessFinished']); $values = $form_state->getValue($this->doGetField($cached_values)->getName()); $massaged_values = $widget->massageFormValues($values, $form, $form_state); - dsm($values); - dsm($massaged_values); foreach ($massaged_values as $delta => $file) { - dsm($file); - $builder->addOperation([$this, 'batchProcess'], [$delta, $file, $form, $form_state, $cached_values]); + $builder->addOperation( + [$this, 'batchProcess'], + [$delta, $file, $cached_values] + ); } batch_set($builder->toArray()); $form_state->setRedirectUrl(Url::fromUri("internal:/node/{$cached_values['node']}/members")); } - public function batchProcess($delta, $info, array $form, FormStateInterface $form_state, array $values, &$context) { - $transaction = \Drupal::database()->startTransaction(); + /** + * Implements callback_batch_operation() for our child addition batch. + */ + public function batchProcess($delta, $info, array $values, &$context) { + $transaction = $this->database->startTransaction(); try { $taxonomy_term_storage = $this->entityTypeManager->getStorage('taxonomy_term'); - dsm(func_get_args()); - /** @var FileInterface $file */ + /** @var \Drupal\file\FileInterface $file */ $file = $this->entityTypeManager->getStorage('file')->load($info['target_id']); $file->setPermanent(); if ($file->save() !== SAVED_UPDATED) { @@ -170,36 +250,35 @@ public function batchProcess($delta, $info, array $form, FormStateInterface $for $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). + // 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'] ? + IslandoraUtils::MODEL_FIELD => ($values['model'] ? $taxonomy_term_storage->load($values['model']) : - NULL, + 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->doGetField($values); - $widget = $this->doGetWidget($field); - $items = FieldItemList::createInstance($field, $field->getName(), $this->getMediaType($form_state)->getTypedData()); - $items->setValue([0 => $info]); - //$items->setValue([$delta => $info]); $media_values = array_merge( [ 'bundle' => $values['media_type'], 'name' => $file->getFilename(), IslandoraUtils::MEDIA_OF_FIELD => $node, - IslandoraUtils::MEDIA_USAGE_FIELD => $values['use'] ? + IslandoraUtils::MEDIA_USAGE_FIELD => ($values['use'] ? $taxonomy_term_storage->loadMultiple($values['use']) : - NULL, + NULL), 'uid' => $this->currentUser->id(), // XXX: Published... no constant? 'status' => 1, @@ -210,11 +289,19 @@ public function batchProcess($delta, $info, array $form, FormStateInterface $for ], ] ); - dsm($media_values); $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(); @@ -226,8 +313,52 @@ public function batchProcess($delta, $info, array $form, FormStateInterface $for } } - public function batchProcessFinished() { - // TODO: Dump out status message of some sort? + /** + * @param array $entities + * + * @return array + */ + 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) { + $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 bulk created entity of type %type, with ID %id.', + '@count validation errors present in bulk created entity of type %type, with ID %id.', + [ + '%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.')); + } } } diff --git a/src/Form/AddChildrenWizard/Form.php b/src/Form/AddChildrenWizard/Form.php index bd2b92aeb..68489f189 100644 --- a/src/Form/AddChildrenWizard/Form.php +++ b/src/Form/AddChildrenWizard/Form.php @@ -5,8 +5,6 @@ use Drupal\Core\Access\AccessResult; use Drupal\Core\DependencyInjection\ClassResolverInterface; use Drupal\Core\Form\FormBuilderInterface; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Render\RendererInterface; use Drupal\Core\Routing\RouteMatch; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountProxyInterface; @@ -14,14 +12,38 @@ use Drupal\ctools\Wizard\FormWizardBase; use Drupal\islandora\IslandoraUtils; use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\RequestStack; +/** + * Bulk children addition wizard base form. + */ class Form extends FormWizardBase { + /** + * The Islandora Utils service. + * + * @var \Drupal\islandora\IslandoraUtils + */ protected IslandoraUtils $utils; + + /** + * The current node ID. + * + * @var string|mixed|null + */ protected string $nodeId; + + /** + * The current route match. + * + * @var \Drupal\Core\Routing\RouteMatchInterface + */ protected RouteMatchInterface $currentRoute; + + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountProxyInterface + */ protected AccountProxyInterface $currentUser; /** @@ -58,7 +80,6 @@ public static function getParameters() : array { [ 'utils' => \Drupal::service('islandora.utils'), 'tempstore_id' => 'islandora.upload_children', - //'machine_name' => 'islandora_add_children_wizard', 'current_route_match' => \Drupal::service('current_route_match'), 'current_user' => \Drupal::service('current_user'), ] @@ -79,35 +100,38 @@ public function getMachineName() { * {@inheritdoc} */ public function getOperations($cached_values) { - return [ - 'child_type' => [ - 'title' => $this->t('Type of children'), - 'form' => TypeSelectionForm::class, - 'values' => [ - 'node' => $this->nodeId, - ] + $ops = []; + + $ops['child_type'] = [ + 'title' => $this->t('Type of children'), + 'form' => TypeSelectionForm::class, + 'values' => [ + 'node' => $this->nodeId, + ], + ]; + $ops['child_files'] = [ + 'title' => $this->t('Files for children'), + 'form' => FileSelectionForm::class, + 'values' => [ + 'node' => $this->nodeId, ], - 'child_files' => [ - 'title' => $this->t('Files for children'), - 'form' => FileSelectionForm::class, - '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]; } - public function finish(array &$form, FormStateInterface $form_state) { - parent::finish($form, $form_state); // TODO: Change the autogenerated stub - dsm($form_state->getTemporaryValue('wizard')); - } - } diff --git a/src/Form/AddChildrenWizard/TypeSelectionForm.php b/src/Form/AddChildrenWizard/TypeSelectionForm.php index fffb2ae97..68237711a 100644 --- a/src/Form/AddChildrenWizard/TypeSelectionForm.php +++ b/src/Form/AddChildrenWizard/TypeSelectionForm.php @@ -3,25 +3,50 @@ namespace Drupal\islandora\Form\AddChildrenWizard; use Drupal\Core\Cache\CacheableMetadata; -use Drupal\Core\DependencyInjection\ContainerInjectionInterface; 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 Drupal\taxonomy\TermInterface; use Symfony\Component\DependencyInjection\ContainerInterface; -use function Symfony\Component\DependencyInjection\Loader\Configurator\iterator; +/** + * Children addition wizard's first step. + */ class TypeSelectionForm 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; - protected ?EntityFieldManagerInterface $entityFieldManager; + /** + * The entity field manager service. + * + * @var \Drupal\Core\Entity\EntityFieldManagerInterface|null + */ + protected ?EntityFieldManagerInterface $entityFieldManager; + /** + * {@inheritdoc} + */ public static function create(ContainerInterface $container) { $instance = parent::create($container); @@ -39,14 +64,33 @@ 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; - //protected ?array $nodeBundleHasMemberOfField = 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 = []; - //$this->nodeBundleHasMemberOfField = []; $access_handler = $this->entityTypeManager->getAccessControlHandler('node'); foreach ($this->entityTypeBundleInfo->getBundleInfo('node') as $bundle => $info) { @@ -62,7 +106,6 @@ protected function getNodeBundleOptions() : array { } $this->nodeBundleOptions[$bundle] = $info['label']; $fields = $this->entityFieldManager->getFieldDefinitions('node', $bundle); - //$this->nodeBundleHasMemberOfField[$bundle] = array_key_exists(IslandoraUtils::MEMBER_OF_FIELD, $fields); $this->nodeBundleHasModelField[$bundle] = array_key_exists(IslandoraUtils::MODEL_FIELD, $fields); } } @@ -70,7 +113,16 @@ protected function getNodeBundleOptions() : array { return $this->nodeBundleOptions; } - protected function getModelOptions() : \Traversable { + /** + * 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) { @@ -78,15 +130,43 @@ protected function getModelOptions() : \Traversable { } } - protected function mapModelStates() : \Traversable { + /** + * 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]; } } + /** + * 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 = []; @@ -113,16 +193,33 @@ protected function getMediaBundleOptions() : array { 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() { - /** @var TermInterface[] $terms */ - $terms = $this->entityTypeManager->getStorage('taxonomy_term') + /** @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(); } } - protected function mapUseStates() { + + /** + * 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]; @@ -165,7 +262,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { 'required' => [ ':input[name="bundle"]' => $model_states, ], - ] + ], ]; $form['media_type'] = [ '#type' => 'select', From dacec854a3d7c8f39937edf56cbaa20e4c7e21c5 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 8 Aug 2022 17:16:29 -0300 Subject: [PATCH 07/21] Pull the batch out to a separate service. --- islandora.services.yml | 6 + src/Form/AddChildrenWizard/BatchProcessor.php | 190 +++++++++++++++ src/Form/AddChildrenWizard/FieldTrait.php | 66 +++++ .../AddChildrenWizard/FileSelectionForm.php | 230 ++---------------- src/Form/AddChildrenWizard/Form.php | 11 +- src/Form/AddChildrenWizard/MediaTypeTrait.php | 59 +++++ src/Form/AddChildrenWizard/WizardTrait.php | 40 +++ 7 files changed, 379 insertions(+), 223 deletions(-) create mode 100644 src/Form/AddChildrenWizard/BatchProcessor.php create mode 100644 src/Form/AddChildrenWizard/FieldTrait.php create mode 100644 src/Form/AddChildrenWizard/MediaTypeTrait.php create mode 100644 src/Form/AddChildrenWizard/WizardTrait.php diff --git a/islandora.services.yml b/islandora.services.yml index 4b3a9d16e..e48818b96 100644 --- a/islandora.services.yml +++ b/islandora.services.yml @@ -59,3 +59,9 @@ services: arguments: ['@jwt.authentication.jwt'] tags: - { name: event_subscriber } + islandora.upload_children.batch_processor: + class: Drupal\islandora\Form\AddChildrenWizard\BatchProcessor + arguments: + - '@entity_type.manager' + - '@database' + - '@current_user' diff --git a/src/Form/AddChildrenWizard/BatchProcessor.php b/src/Form/AddChildrenWizard/BatchProcessor.php new file mode 100644 index 000000000..2e4a84bf9 --- /dev/null +++ b/src/Form/AddChildrenWizard/BatchProcessor.php @@ -0,0 +1,190 @@ +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); + } + } + + /** + * 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) { + $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 bulk created entity of type %type, with ID %id.', + '@count validation errors present in bulk created entity of type %type, with ID %id.', + [ + '%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.')); + } + } + +} diff --git a/src/Form/AddChildrenWizard/FieldTrait.php b/src/Form/AddChildrenWizard/FieldTrait.php new file mode 100644 index 000000000..156000e32 --- /dev/null +++ b/src/Form/AddChildrenWizard/FieldTrait.php @@ -0,0 +1,66 @@ +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(); + } + + /** + * 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; + } + +} diff --git a/src/Form/AddChildrenWizard/FileSelectionForm.php b/src/Form/AddChildrenWizard/FileSelectionForm.php index 8d29bab35..084eb5e50 100644 --- a/src/Form/AddChildrenWizard/FileSelectionForm.php +++ b/src/Form/AddChildrenWizard/FileSelectionForm.php @@ -2,11 +2,8 @@ namespace Drupal\islandora\Form\AddChildrenWizard; -use Drupal\Component\Plugin\PluginManagerInterface; use Drupal\Core\Batch\BatchBuilder; use Drupal\Core\Database\Connection; -use Drupal\Core\Entity\EntityFieldManagerInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemList; use Drupal\Core\Field\FieldStorageDefinitionInterface; @@ -15,38 +12,19 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountProxyInterface; use Drupal\Core\Url; -use Drupal\islandora\IslandoraUtils; use Drupal\media\MediaTypeInterface; -use Drupal\node\NodeInterface; use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpKernel\Exception\HttpException; -use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; /** * Children addition wizard's second step. */ class FileSelectionForm extends FormBase { - /** - * The entity type manager service. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface|null - */ - protected ?EntityTypeManagerInterface $entityTypeManager; - - /** - * The widget plugin manager service. - * - * @var \Drupal\Core\Field\WidgetPluginManager|null - */ - protected ?PluginManagerInterface $widgetPluginManager; - - /** - * The entity field manager service. - * - * @var \Drupal\Core\Entity\EntityFieldManagerInterface|null - */ - protected ?EntityFieldManagerInterface $entityFieldManager; + use WizardTrait { + WizardTrait::getField as doGetField; + WizardTrait::getMediaType as doGetMediaType; + WizardTrait::getWidget as doGetWidget; + } /** * The database connection serivce. @@ -62,6 +40,13 @@ class FileSelectionForm extends FormBase { */ protected ?AccountProxyInterface $currentUser; + /** + * The batch processor service. + * + * @var \Drupal\islandora\Form\AddChildrenWizard\BatchProcessor|null + */ + protected ?BatchProcessor $batchProcessor; + /** * {@inheritdoc} */ @@ -74,6 +59,8 @@ public static function create(ContainerInterface $container): self { $instance->database = $container->get('database'); $instance->currentUser = $container->get('current_user'); + $instance->batchProcessor = $container->get('islandora.upload_children.batch_processor'); + return $instance; } @@ -100,24 +87,6 @@ protected function getMediaType(FormStateInterface $form_state): MediaTypeInterf return $this->doGetMediaType($form_state->getTemporaryValue('wizard')); } - /** - * 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 doGetMediaType(array $values): MediaTypeInterface { - /** @var \Drupal\media\MediaTypeInterface $media_type */ - return $this->entityTypeManager->getStorage('media_type')->load($values['media_type']); - } - /** * Helper; get field instance, based off discovering from form state. * @@ -136,26 +105,6 @@ protected function getField(FormStateInterface $form_state): FieldDefinitionInte return $field; } - /** - * Helper; get field instance, given our required values. - * - * @param array $values - * See ::doGetMediaType() for which values are required. - * - * @return \Drupal\Core\Field\FieldDefinitionInterface - * The target field. - */ - protected function doGetField(array $values): FieldDefinitionInterface { - $media_type = $this->doGetMediaType($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(); - } - /** * Helper; get widget for the field, based on discovering from form state. * @@ -169,23 +118,6 @@ protected function getWidget(FormStateInterface $form_state): WidgetInterface { return $this->doGetWidget($this->getField($form_state)); } - /** - * 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 doGetWidget(FieldDefinitionInterface $field): WidgetInterface { - return $this->widgetPluginManager->getInstance([ - 'field_definition' => $field, - 'form_mode' => 'default', - 'prepare' => TRUE, - ]); - } - /** * {@inheritdoc} */ @@ -218,12 +150,12 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $builder = (new BatchBuilder()) ->setTitle($this->t('Creating children...')) ->setInitMessage($this->t('Initializing...')) - ->setFinishCallback([$this, 'batchProcessFinished']); + ->setFinishCallback([$this->batchProcessor, 'batchProcessFinished']); $values = $form_state->getValue($this->doGetField($cached_values)->getName()); $massaged_values = $widget->massageFormValues($values, $form, $form_state); foreach ($massaged_values as $delta => $file) { $builder->addOperation( - [$this, 'batchProcess'], + [$this->batchProcessor, 'batchOperation'], [$delta, $file, $cached_values] ); } @@ -231,134 +163,4 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $form_state->setRedirectUrl(Url::fromUri("internal:/node/{$cached_values['node']}/members")); } - /** - * Implements callback_batch_operation() for our child addition batch. - */ - public function batchProcess($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->doGetField($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); - } - } - - /** - * @param array $entities - * - * @return array - */ - 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) { - $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 bulk created entity of type %type, with ID %id.', - '@count validation errors present in bulk created entity of type %type, with ID %id.', - [ - '%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.')); - } - } - } diff --git a/src/Form/AddChildrenWizard/Form.php b/src/Form/AddChildrenWizard/Form.php index 68489f189..97ac8b239 100644 --- a/src/Form/AddChildrenWizard/Form.php +++ b/src/Form/AddChildrenWizard/Form.php @@ -2,10 +2,8 @@ namespace Drupal\islandora\Form\AddChildrenWizard; -use Drupal\Core\Access\AccessResult; use Drupal\Core\DependencyInjection\ClassResolverInterface; use Drupal\Core\Form\FormBuilderInterface; -use Drupal\Core\Routing\RouteMatch; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountProxyInterface; use Drupal\Core\TempStore\SharedTempStoreFactory; @@ -56,8 +54,6 @@ public function __construct( EventDispatcherInterface $event_dispatcher, RouteMatchInterface $route_match, $tempstore_id, - IslandoraUtils $utils, - RouteMatchInterface $current_route_match, AccountProxyInterface $current_user, $machine_name = NULL, $step = NULL @@ -65,9 +61,7 @@ public function __construct( parent::__construct($tempstore, $builder, $class_resolver, $event_dispatcher, $route_match, $tempstore_id, $machine_name, $step); - $this->utils = $utils; - $this->currentRoute = $current_route_match; - $this->nodeId = $this->currentRoute->getParameter('node'); + $this->nodeId = $this->routeMatch->getParameter('node'); $this->currentUser = $current_user; } @@ -78,10 +72,9 @@ public static function getParameters() : array { return array_merge( parent::getParameters(), [ - 'utils' => \Drupal::service('islandora.utils'), 'tempstore_id' => 'islandora.upload_children', - 'current_route_match' => \Drupal::service('current_route_match'), 'current_user' => \Drupal::service('current_user'), + 'batch_processor' => \Drupal::service('islandora.upload_children.batch_processor'), ] ); } diff --git a/src/Form/AddChildrenWizard/MediaTypeTrait.php b/src/Form/AddChildrenWizard/MediaTypeTrait.php new file mode 100644 index 000000000..e2caf2c2c --- /dev/null +++ b/src/Form/AddChildrenWizard/MediaTypeTrait.php @@ -0,0 +1,59 @@ +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; + } + +} diff --git a/src/Form/AddChildrenWizard/WizardTrait.php b/src/Form/AddChildrenWizard/WizardTrait.php new file mode 100644 index 000000000..dd56450fa --- /dev/null +++ b/src/Form/AddChildrenWizard/WizardTrait.php @@ -0,0 +1,40 @@ +widgetPluginManager->getInstance([ + 'field_definition' => $field, + 'form_mode' => 'default', + 'prepare' => TRUE, + ]); + } + +} From 65bccf019c2da3344860edb585a239c7a425220e Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 15 Aug 2022 10:36:06 -0300 Subject: [PATCH 08/21] Something of namespacing the child-specific batch... ... 'cause need to slap together a media-specific batch similarly? --- islandora.services.yml | 2 +- .../{BatchProcessor.php => ChildBatchProcessor.php} | 6 +++--- src/Form/AddChildrenWizard/FileSelectionForm.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename src/Form/AddChildrenWizard/{BatchProcessor.php => ChildBatchProcessor.php} (95%) diff --git a/islandora.services.yml b/islandora.services.yml index e48818b96..1dbed028a 100644 --- a/islandora.services.yml +++ b/islandora.services.yml @@ -60,7 +60,7 @@ services: tags: - { name: event_subscriber } islandora.upload_children.batch_processor: - class: Drupal\islandora\Form\AddChildrenWizard\BatchProcessor + class: Drupal\islandora\Form\AddChildrenWizard\ChildBatchProcessor arguments: - '@entity_type.manager' - '@database' diff --git a/src/Form/AddChildrenWizard/BatchProcessor.php b/src/Form/AddChildrenWizard/ChildBatchProcessor.php similarity index 95% rename from src/Form/AddChildrenWizard/BatchProcessor.php rename to src/Form/AddChildrenWizard/ChildBatchProcessor.php index 2e4a84bf9..ccd07b997 100644 --- a/src/Form/AddChildrenWizard/BatchProcessor.php +++ b/src/Form/AddChildrenWizard/ChildBatchProcessor.php @@ -14,7 +14,7 @@ /** * Children addition batch processor. */ -class BatchProcessor { +class ChildBatchProcessor { use FieldTrait; @@ -171,8 +171,8 @@ public function batchProcessFinished($success, $results, $operations): void { foreach ($info as $id => $count) { $this->messenger()->addWarning($this->formatPlural( $count, - '1 validation error present in bulk created entity of type %type, with ID %id.', - '@count validation errors present in bulk created entity of type %type, with ID %id.', + '1 validation error present in bulk created entity of type %type, with ID %id.', + '@count validation errors present in bulk created entity of type %type, with ID %id.', [ '%type' => $entity_type, ':uri' => Url::fromRoute("entity.{$entity_type}.canonical", [$entity_type => $id])->toString(), diff --git a/src/Form/AddChildrenWizard/FileSelectionForm.php b/src/Form/AddChildrenWizard/FileSelectionForm.php index 084eb5e50..2e505c099 100644 --- a/src/Form/AddChildrenWizard/FileSelectionForm.php +++ b/src/Form/AddChildrenWizard/FileSelectionForm.php @@ -43,9 +43,9 @@ class FileSelectionForm extends FormBase { /** * The batch processor service. * - * @var \Drupal\islandora\Form\AddChildrenWizard\BatchProcessor|null + * @var \Drupal\islandora\Form\AddChildrenWizard\ChildBatchProcessor|null */ - protected ?BatchProcessor $batchProcessor; + protected ?ChildBatchProcessor $batchProcessor; /** * {@inheritdoc} From ad1b6ba013c930e751187505a984d94b0a3f9883 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 16 Aug 2022 12:20:41 -0300 Subject: [PATCH 09/21] All together, I think... Both the child-uploading, and media-uploading forms. --- islandora.routing.yml | 13 +- islandora.services.yml | 8 + src/Form/AddChildrenForm.php | 2 +- .../AbstractBatchProcessor.php | 254 ++++++++++++++++++ ...Form.php => AbstractFileSelectionForm.php} | 58 ++-- .../{Form.php => AbstractForm.php} | 28 +- src/Form/AddChildrenWizard/Access.php | 22 +- .../AddChildrenWizard/ChildBatchProcessor.php | 190 ++----------- .../ChildFileSelectionForm.php | 32 +++ src/Form/AddChildrenWizard/ChildForm.php | 24 ++ .../ChildTypeSelectionForm.php | 157 +++++++++++ src/Form/AddChildrenWizard/FieldTrait.php | 2 +- .../AddChildrenWizard/MediaBatchProcessor.php | 34 +++ .../MediaFileSelectionForm.php | 32 +++ src/Form/AddChildrenWizard/MediaForm.php | 24 ++ ...ionForm.php => MediaTypeSelectionForm.php} | 130 ++------- src/Form/AddChildrenWizard/MediaTypeTrait.php | 2 +- src/Form/AddMediaForm.php | 2 + 18 files changed, 666 insertions(+), 348 deletions(-) create mode 100644 src/Form/AddChildrenWizard/AbstractBatchProcessor.php rename src/Form/AddChildrenWizard/{FileSelectionForm.php => AbstractFileSelectionForm.php} (67%) rename src/Form/AddChildrenWizard/{Form.php => AbstractForm.php} (82%) create mode 100644 src/Form/AddChildrenWizard/ChildFileSelectionForm.php create mode 100644 src/Form/AddChildrenWizard/ChildForm.php create mode 100644 src/Form/AddChildrenWizard/ChildTypeSelectionForm.php create mode 100644 src/Form/AddChildrenWizard/MediaBatchProcessor.php create mode 100644 src/Form/AddChildrenWizard/MediaFileSelectionForm.php create mode 100644 src/Form/AddChildrenWizard/MediaForm.php rename src/Form/AddChildrenWizard/{TypeSelectionForm.php => MediaTypeSelectionForm.php} (62%) diff --git a/islandora.routing.yml b/islandora.routing.yml index e2cb7d353..86d134828 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 1dbed028a..6dfa158dd 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 adad9dae9..b349f5708 100644 --- a/src/Form/AddChildrenForm.php +++ b/src/Form/AddChildrenForm.php @@ -13,7 +13,7 @@ /** * 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 000000000..f5b635002 --- /dev/null +++ b/src/Form/AddChildrenWizard/AbstractBatchProcessor.php @@ -0,0 +1,254 @@ +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 bulk created entity of type %type, with ID %id.', + '@count validation errors present in bulk created entity of type %type, with ID %id.', + [ + '%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 2e505c099..c8436a1a5 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\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 @@ public static function create(ContainerInterface $container): self { $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 @@ public function getFormId() { * @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 @@ protected function getMediaType(FormStateInterface $form_state): MediaTypeInterf * @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 @@ protected function getField(FormStateInterface $form_state): FieldDefinitionInte * @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 @@ 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->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 @@ public function buildForm(array $form, FormStateInterface $form_state): array { 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 97ac8b239..251953f6a 100644 --- a/src/Form/AddChildrenWizard/Form.php +++ b/src/Form/AddChildrenWizard/AbstractForm.php @@ -14,7 +14,12 @@ /** * 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 @@ public static function getParameters() : array { 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 2a5cdbe21..0adafde51 100644 --- a/src/Form/AddChildrenWizard/Access.php +++ b/src/Form/AddChildrenWizard/Access.php @@ -49,15 +49,23 @@ public static function create(ContainerInterface $container) : self { * @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 ccd07b997..9be9902ae 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 bulk created entity of type %type, with ID %id.', - '@count validation errors present in bulk created entity of type %type, with ID %id.', - [ - '%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 000000000..9783d0823 --- /dev/null +++ b/src/Form/AddChildrenWizard/ChildFileSelectionForm.php @@ -0,0 +1,32 @@ +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 000000000..0b9a197f4 --- /dev/null +++ b/src/Form/AddChildrenWizard/ChildForm.php @@ -0,0 +1,24 @@ + $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 000000000..843cc85b0 --- /dev/null +++ b/src/Form/AddChildrenWizard/ChildTypeSelectionForm.php @@ -0,0 +1,157 @@ +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 156000e32..743fc619d 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 000000000..f069d0bf3 --- /dev/null +++ b/src/Form/AddChildrenWizard/MediaBatchProcessor.php @@ -0,0 +1,34 @@ +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 000000000..534c73093 --- /dev/null +++ b/src/Form/AddChildrenWizard/MediaFileSelectionForm.php @@ -0,0 +1,32 @@ +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 000000000..2e6fa2177 --- /dev/null +++ b/src/Form/AddChildrenWizard/MediaForm.php @@ -0,0 +1,24 @@ + $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 68237711a..0e687d586 100644 --- a/src/Form/AddChildrenWizard/TypeSelectionForm.php +++ b/src/Form/AddChildrenWizard/MediaTypeSelectionForm.php @@ -14,7 +14,7 @@ /** * 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 @@ public static function create(ContainerInterface $container) { * {@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 @@ public function buildForm(array $form, FormStateInterface $form_state) { ]); $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 @@ public function buildForm(array $form, FormStateInterface $form_state) { } /** - * {@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 e2caf2c2c..5600210b4 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 281abd9a9..5078d1a86 100644 --- a/src/Form/AddMediaForm.php +++ b/src/Form/AddMediaForm.php @@ -23,6 +23,8 @@ /** * 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 { From e4f1b2055c77611b086c3beeea124f5f91356ad7 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 16 Aug 2022 14:06:04 -0300 Subject: [PATCH 10/21] It is not necessary to explicitly mark the files as permanent. --- .../AbstractBatchProcessor.php | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/src/Form/AddChildrenWizard/AbstractBatchProcessor.php b/src/Form/AddChildrenWizard/AbstractBatchProcessor.php index f5b635002..6ea426e38 100644 --- a/src/Form/AddChildrenWizard/AbstractBatchProcessor.php +++ b/src/Form/AddChildrenWizard/AbstractBatchProcessor.php @@ -75,7 +75,6 @@ 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); @@ -111,30 +110,6 @@ 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. * From a8f4c037bb8cd028735ef5fa197c64a7cb369f3b Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 16 Aug 2022 16:04:44 -0300 Subject: [PATCH 11/21] Further generalizing... ... no longer necessarily trying to load files, where files might not be present (for non-file media... oEmbed things?). --- islandora.services.yml | 2 + .../AbstractBatchProcessor.php | 63 ++++++++++++++----- .../AbstractFileSelectionForm.php | 15 ++++- src/Form/AddChildrenWizard/AbstractForm.php | 8 +-- .../AddChildrenWizard/ChildBatchProcessor.php | 7 +-- .../ChildTypeSelectionForm.php | 4 +- src/Form/AddChildrenWizard/FieldTrait.php | 2 +- .../AddChildrenWizard/MediaBatchProcessor.php | 2 +- .../MediaTypeSelectionForm.php | 19 ++++-- src/Form/AddChildrenWizard/MediaTypeTrait.php | 1 - 10 files changed, 87 insertions(+), 36 deletions(-) diff --git a/islandora.services.yml b/islandora.services.yml index 6dfa158dd..4108e2446 100644 --- a/islandora.services.yml +++ b/islandora.services.yml @@ -66,6 +66,7 @@ services: - '@database' - '@current_user' - '@messenger' + - '@date.formatter' islandora.upload_media.batch_processor: class: Drupal\islandora\Form\AddChildrenWizard\MediaBatchProcessor arguments: @@ -73,3 +74,4 @@ services: - '@database' - '@current_user' - '@messenger' + - '@date.formatter' diff --git a/src/Form/AddChildrenWizard/AbstractBatchProcessor.php b/src/Form/AddChildrenWizard/AbstractBatchProcessor.php index 6ea426e38..359db27e9 100644 --- a/src/Form/AddChildrenWizard/AbstractBatchProcessor.php +++ b/src/Form/AddChildrenWizard/AbstractBatchProcessor.php @@ -3,6 +3,7 @@ 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; @@ -53,6 +54,13 @@ abstract class AbstractBatchProcessor { */ protected MessengerInterface $messenger; + /** + * The date formatter service. + * + * @var \Drupal\Core\Datetime\DateFormatterInterface + */ + protected DateFormatterInterface $dateFormatter; + /** * Constructor. */ @@ -60,12 +68,14 @@ public function __construct( EntityTypeManagerInterface $entity_type_manager, Connection $database, AccountProxyInterface $current_user, - MessengerInterface $messenger + MessengerInterface $messenger, + DateFormatterInterface $date_formatter ) { $this->entityTypeManager = $entity_type_manager; $this->database = $database; $this->currentUser = $current_user; $this->messenger = $messenger; + $this->dateFormatter = $date_formatter; } /** @@ -96,24 +106,25 @@ public function batchOperation($delta, $info, array $values, &$context) { /** * Loads the file indicated. * - * @param array $info - * An associative array containing at least: - * - target_id: The id of the file to load. + * @param mixed $info + * Widget values. * - * @return \Drupal\file\FileInterface + * @return \Drupal\file\FileInterface|null * 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']); + 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 array $info + * @param mixed $info * Info from the widget used to create the request. * @param array $values * Additional form inputs. @@ -121,16 +132,36 @@ protected function getFile(array $info) : FileInterface { * @return \Drupal\node\NodeInterface * The node to which to attach the created media. */ - abstract protected function getNode(array $info, array $values) : NodeInterface; + 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 array $info - * The widget info, which should have a 'target_id' identifying the target - * file. + * @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 @@ -144,18 +175,16 @@ abstract protected function getNode(array $info, array $values) : NodeInterface; * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException * @throws \Drupal\Core\Entity\EntityStorageException */ - protected function createMedia(NodeInterface $node, array $info, array $values) : MediaInterface { + protected function createMedia(NodeInterface $node, $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(), + 'name' => $this->getName($info, $values), IslandoraUtils::MEDIA_OF_FIELD => $node, IslandoraUtils::MEDIA_USAGE_FIELD => ($values['use'] ? $taxonomy_term_storage->loadMultiple($values['use']) : @@ -172,7 +201,7 @@ protected function createMedia(NodeInterface $node, array $info, array $values) ); $media = $this->entityTypeManager->getStorage('media')->create($media_values); if ($media->save() !== SAVED_NEW) { - throw new \Exception("Failed to create media for file '{$file->id()}."); + throw new \Exception("Failed to create media."); } return $media; diff --git a/src/Form/AddChildrenWizard/AbstractFileSelectionForm.php b/src/Form/AddChildrenWizard/AbstractFileSelectionForm.php index c8436a1a5..6aeed8795 100644 --- a/src/Form/AddChildrenWizard/AbstractFileSelectionForm.php +++ b/src/Form/AddChildrenWizard/AbstractFileSelectionForm.php @@ -3,6 +3,7 @@ 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; @@ -10,6 +11,7 @@ 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; @@ -81,7 +83,16 @@ protected function getFieldFromFormState(FormStateInterface $form_state): FieldD $cached_values = $form_state->getTemporaryValue('wizard'); $field = $this->getField($cached_values); - $field->getFieldStorageDefinition()->set('cardinality', FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); + $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; } @@ -129,7 +140,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $widget = $this->getWidgetFromFormState($form_state); $builder = (new BatchBuilder()) - ->setTitle($this->t('Creating children...')) + ->setTitle($this->t('Bulk creating...')) ->setInitMessage($this->t('Initializing...')) ->setFinishCallback([$this->batchProcessor, 'batchProcessFinished']); $values = $form_state->getValue($this->getField($cached_values)->getName()); diff --git a/src/Form/AddChildrenWizard/AbstractForm.php b/src/Form/AddChildrenWizard/AbstractForm.php index 251953f6a..23f511e01 100644 --- a/src/Form/AddChildrenWizard/AbstractForm.php +++ b/src/Form/AddChildrenWizard/AbstractForm.php @@ -31,9 +31,9 @@ abstract class AbstractForm extends FormWizardBase { /** * The current node ID. * - * @var string|mixed|null + * @var mixed|null */ - protected string $nodeId; + protected $nodeId; /** * The current route match. @@ -90,14 +90,14 @@ public function getOperations($cached_values) { $ops = []; $ops['type_selection'] = [ - 'title' => $this->t('Type of children'), + 'title' => $this->t('Type Selection'), 'form' => static::TYPE_SELECTION_FORM, 'values' => [ 'node' => $this->nodeId, ], ]; $ops['file_selection'] = [ - 'title' => $this->t('Files for children'), + 'title' => $this->t('Widget Input for Selected Type'), 'form' => static::FILE_SELECTION_FORM, 'values' => [ 'node' => $this->nodeId, diff --git a/src/Form/AddChildrenWizard/ChildBatchProcessor.php b/src/Form/AddChildrenWizard/ChildBatchProcessor.php index 9be9902ae..084e7816e 100644 --- a/src/Form/AddChildrenWizard/ChildBatchProcessor.php +++ b/src/Form/AddChildrenWizard/ChildBatchProcessor.php @@ -13,18 +13,17 @@ class ChildBatchProcessor extends AbstractBatchProcessor { /** * {@inheritdoc} */ - protected function getNode(array $info, array $values) : NodeInterface { + 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']); - $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(), + 'title' => $this->getName($info, $values), IslandoraUtils::MEMBER_OF_FIELD => $parent, 'uid' => $this->currentUser->id(), 'status' => NodeInterface::PUBLISHED, @@ -34,7 +33,7 @@ protected function getNode(array $info, array $values) : NodeInterface { ]); if ($node->save() !== SAVED_NEW) { - throw new \Exception("Failed to create node for file '{$file->id()}'."); + throw new \Exception("Failed to create node."); } return $node; diff --git a/src/Form/AddChildrenWizard/ChildTypeSelectionForm.php b/src/Form/AddChildrenWizard/ChildTypeSelectionForm.php index 843cc85b0..f57959971 100644 --- a/src/Form/AddChildrenWizard/ChildTypeSelectionForm.php +++ b/src/Form/AddChildrenWizard/ChildTypeSelectionForm.php @@ -14,7 +14,7 @@ class ChildTypeSelectionForm extends MediaTypeSelectionForm { /** * {@inheritdoc} */ - public function getFormId() { + public function getFormId() : string { return 'islandora_add_children_type_selection'; } @@ -144,7 +144,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { /** * {@inheritdoc} */ - protected static function keysToSave() { + protected static function keysToSave() : array { return array_merge( parent::keysToSave(), [ diff --git a/src/Form/AddChildrenWizard/FieldTrait.php b/src/Form/AddChildrenWizard/FieldTrait.php index 743fc619d..830f95cda 100644 --- a/src/Form/AddChildrenWizard/FieldTrait.php +++ b/src/Form/AddChildrenWizard/FieldTrait.php @@ -39,7 +39,7 @@ protected function getField(array $values): FieldDefinitionInterface { $fields = $this->entityFieldManager()->getFieldDefinitions('media', $media_type->id()); return $fields[$source_field->getFieldStorageDefinition()->getName()] ?? - $media_source->createSourceField(); + $media_source->createSourceField($media_type); } /** diff --git a/src/Form/AddChildrenWizard/MediaBatchProcessor.php b/src/Form/AddChildrenWizard/MediaBatchProcessor.php index f069d0bf3..9a54f03b6 100644 --- a/src/Form/AddChildrenWizard/MediaBatchProcessor.php +++ b/src/Form/AddChildrenWizard/MediaBatchProcessor.php @@ -12,7 +12,7 @@ class MediaBatchProcessor extends AbstractBatchProcessor { /** * {@inheritdoc} */ - protected function getNode(array $info, array $values) : NodeInterface { + protected function getNode($info, array $values) : NodeInterface { return $this->entityTypeManager->getStorage('node')->load($values['node']); } diff --git a/src/Form/AddChildrenWizard/MediaTypeSelectionForm.php b/src/Form/AddChildrenWizard/MediaTypeSelectionForm.php index 0e687d586..b06d004dc 100644 --- a/src/Form/AddChildrenWizard/MediaTypeSelectionForm.php +++ b/src/Form/AddChildrenWizard/MediaTypeSelectionForm.php @@ -44,15 +44,23 @@ class MediaTypeSelectionForm extends FormBase { */ protected ?EntityFieldManagerInterface $entityFieldManager; + /** + * The Islandora Utils service. + * + * @var \Drupal\islandora\IslandoraUtils|null + */ + protected ?IslandoraUtils $utils; + /** * {@inheritdoc} */ - public static function create(ContainerInterface $container) { + 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; } @@ -60,7 +68,7 @@ public static function create(ContainerInterface $container) { /** * {@inheritdoc} */ - public function getFormId() { + public function getFormId() : string { return 'islandora_add_media_type_selection'; } @@ -94,6 +102,9 @@ protected function getMediaBundleOptions() : array { $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, @@ -122,7 +133,7 @@ protected function getMediaBundleOptions() : array { * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ - protected function getMediaUseOptions() { + protected function getMediaUseOptions() : \Generator { /** @var \Drupal\taxonomy\TermInterface[] $terms */ $terms = $this->entityTypeManager->getStorage('taxonomy_term') ->loadTree('islandora_media_use', 0, NULL, TRUE); @@ -195,7 +206,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { * @return string[] * The keys to be persisted in our temp value in form state. */ - protected static function keysToSave() { + protected static function keysToSave() : array { return [ 'media_type', 'use', diff --git a/src/Form/AddChildrenWizard/MediaTypeTrait.php b/src/Form/AddChildrenWizard/MediaTypeTrait.php index 5600210b4..36cf6ff2a 100644 --- a/src/Form/AddChildrenWizard/MediaTypeTrait.php +++ b/src/Form/AddChildrenWizard/MediaTypeTrait.php @@ -31,7 +31,6 @@ trait MediaTypeTrait { * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ protected function getMediaType(array $values): MediaTypeInterface { - /** @var \Drupal\media\MediaTypeInterface $media_type */ return $this->entityTypeManager()->getStorage('media_type')->load($values['media_type']); } From 519c3529275ef8f2ace3cc4d0b9061027c4a5816 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 16 Aug 2022 16:12:48 -0300 Subject: [PATCH 12/21] Adjust class comment. --- src/Form/AddChildrenWizard/AbstractBatchProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Form/AddChildrenWizard/AbstractBatchProcessor.php b/src/Form/AddChildrenWizard/AbstractBatchProcessor.php index 359db27e9..56aba1753 100644 --- a/src/Form/AddChildrenWizard/AbstractBatchProcessor.php +++ b/src/Form/AddChildrenWizard/AbstractBatchProcessor.php @@ -18,7 +18,7 @@ use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; /** - * Children addition batch processor. + * Abstract addition batch processor. */ abstract class AbstractBatchProcessor { From 5e6a816a3746cdb328bd041f51aff47996a5984d Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 16 Aug 2022 17:04:43 -0300 Subject: [PATCH 13/21] Get rid of the deprecation flags. --- src/Form/AddChildrenForm.php | 2 -- src/Form/AddMediaForm.php | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/Form/AddChildrenForm.php b/src/Form/AddChildrenForm.php index b349f5708..528b42832 100644 --- a/src/Form/AddChildrenForm.php +++ b/src/Form/AddChildrenForm.php @@ -12,8 +12,6 @@ /** * Form that lets users upload one or more files as children to a resource node. - * - * @deprecated Use the \Drupal\islandora\Form\AddChildrenWizard\ChildForm instead. */ class AddChildrenForm extends AddMediaForm { diff --git a/src/Form/AddMediaForm.php b/src/Form/AddMediaForm.php index 5078d1a86..281abd9a9 100644 --- a/src/Form/AddMediaForm.php +++ b/src/Form/AddMediaForm.php @@ -23,8 +23,6 @@ /** * 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 { From c333c97213f3c2b6c98199b231bc4af8e5c6349f Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 23 Aug 2022 10:36:31 -0300 Subject: [PATCH 14/21] Remove unused constant. ... is defined instead at the "FileSelectionForm" level, accidentally left it here from intermediate implementation state. --- src/Form/AddChildrenWizard/AbstractForm.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Form/AddChildrenWizard/AbstractForm.php b/src/Form/AddChildrenWizard/AbstractForm.php index 23f511e01..038d59f16 100644 --- a/src/Form/AddChildrenWizard/AbstractForm.php +++ b/src/Form/AddChildrenWizard/AbstractForm.php @@ -19,7 +19,6 @@ 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. From 6b65cd4239ca68c0851a23c855e3523e9cbaf65d Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 3 Oct 2022 15:47:40 -0300 Subject: [PATCH 15/21] Pass the renderer along, with the version constraint. --- composer.json | 4 ++-- src/Form/AddChildrenWizard/AbstractForm.php | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index a4db6d035..29b7b5c33 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "drupal/flysystem" : "^2.0@alpha", "islandora/crayfish-commons": "^2", "drupal/file_replace": "^1.1", - "drupal/ctools": "^3" + "drupal/ctools": "^3.8" }, "require-dev": { "phpunit/phpunit": "^6", @@ -38,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": [ diff --git a/src/Form/AddChildrenWizard/AbstractForm.php b/src/Form/AddChildrenWizard/AbstractForm.php index 038d59f16..e9fac3875 100644 --- a/src/Form/AddChildrenWizard/AbstractForm.php +++ b/src/Form/AddChildrenWizard/AbstractForm.php @@ -4,6 +4,7 @@ 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; @@ -57,12 +58,13 @@ public function __construct( 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, $tempstore_id, + parent::__construct($tempstore, $builder, $class_resolver, $event_dispatcher, $route_match, $renderer, $tempstore_id, $machine_name, $step); $this->nodeId = $this->routeMatch->getParameter('node'); From 2c42a355568039ddc0210895c5b8ee3268720050 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 3 Oct 2022 16:07:01 -0300 Subject: [PATCH 16/21] Add update hook to enable ctools in sites where it may not be. ... as it's now required. --- islandora.install | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/islandora.install b/islandora.install index f9eb1225f..5e1e3022a 100644 --- a/islandora.install +++ b/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,29 @@ 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.', [ + '@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.'); + } +} From a5148f5d187b2a70514d368a89ce85470e52d5cc Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 3 Oct 2022 16:21:08 -0300 Subject: [PATCH 17/21] Cover ALL the exits. --- islandora.install | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/islandora.install b/islandora.install index 5e1e3022a..ab80af9c7 100644 --- a/islandora.install +++ b/islandora.install @@ -201,6 +201,13 @@ function islandora_update_8007() { } } catch (ExtensionNameLengthException | MissingDependencyException $e) { - throw new UpdateException('Failed; ensure that the ctools module is available in the Drupal installation.'); + 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.'); } From 636261a4313d1d67d0571f18e13def3dfed7f5ab Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 3 Oct 2022 16:23:29 -0300 Subject: [PATCH 18/21] Refine message. --- islandora.install | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/islandora.install b/islandora.install index ab80af9c7..960c05b27 100644 --- a/islandora.install +++ b/islandora.install @@ -185,7 +185,7 @@ function update_jsonld_included_namespaces() { function islandora_update_8007() { $module_handler = \Drupal::moduleHandler(); if ($module_handler->moduleExists('ctools')) { - return t('The "@module_name" module is already enabled.', [ + return t('The "@module_name" module is already enabled, no action necessary.', [ '@module_name' => 'ctools', ]); } From c2a084a6aa1688dffb92b1ee23661b99981a4e7d Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 3 Oct 2022 16:49:41 -0300 Subject: [PATCH 19/21] Excessively long line in comment... ... whoops. --- islandora.install | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/islandora.install b/islandora.install index 960c05b27..ad2eb8e1c 100644 --- a/islandora.install +++ b/islandora.install @@ -207,7 +207,8 @@ function islandora_update_8007() { 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? + // 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.'); } From 481582dfcfef484134690f60b3f000cc942f520c Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Wed, 5 Oct 2022 15:25:36 -0300 Subject: [PATCH 20/21] 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? --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 29b7b5c33..c02d7c168 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "drupal/flysystem" : "^2.0@alpha", "islandora/crayfish-commons": "^2", "drupal/file_replace": "^1.1", - "drupal/ctools": "^3.8" + "drupal/ctools": "^3.8 || ^4" }, "require-dev": { "phpunit/phpunit": "^6", From 4d5a063a59d8a9099dba7cab49b4f8501c35adc5 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 11 Oct 2022 14:50:05 -0300 Subject: [PATCH 21/21] Fix undefined "count" index. --- src/Form/AddChildrenWizard/AbstractBatchProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Form/AddChildrenWizard/AbstractBatchProcessor.php b/src/Form/AddChildrenWizard/AbstractBatchProcessor.php index 56aba1753..6193c0c30 100644 --- a/src/Form/AddChildrenWizard/AbstractBatchProcessor.php +++ b/src/Form/AddChildrenWizard/AbstractBatchProcessor.php @@ -91,7 +91,7 @@ public function batchOperation($delta, $info, array $values, &$context) { $context['results'] = array_merge_recursive($context['results'], [ 'validation_violations' => $this->validationClassification($entities), ]); - $context['results']['count'] += 1; + $context['results']['count'] = ($context['results']['count'] ?? 0) + 1; } catch (HttpExceptionInterface $e) { $transaction->rollBack();