From 091e2995d45aa853a58103078899c701fbabc667 Mon Sep 17 00:00:00 2001 From: astanley Date: Fri, 16 May 2025 18:37:35 +0000 Subject: [PATCH] initial commit --- drush.services.yml | 6 + islandora_batch_action.info.yml | 5 + islandora_batch_action.links.menu.yml | 7 + islandora_batch_action.routing.yml | 7 + islandora_batch_action.services.yml | 14 ++ src/Batch/NodeActionBatch.php | 76 ++++++++++ src/Commands/BatchActionCommands.php | 193 ++++++++++++++++++++++++++ src/Form/MissingDerivativesFm.php | 185 ++++++++++++++++++++++++ src/IslandoraBatchActionUtils.php | 59 ++++++++ 9 files changed, 552 insertions(+) create mode 100644 drush.services.yml create mode 100644 islandora_batch_action.info.yml create mode 100644 islandora_batch_action.links.menu.yml create mode 100644 islandora_batch_action.routing.yml create mode 100644 islandora_batch_action.services.yml create mode 100644 src/Batch/NodeActionBatch.php create mode 100644 src/Commands/BatchActionCommands.php create mode 100644 src/Form/MissingDerivativesFm.php create mode 100644 src/IslandoraBatchActionUtils.php diff --git a/drush.services.yml b/drush.services.yml new file mode 100644 index 0000000..afe1ff3 --- /dev/null +++ b/drush.services.yml @@ -0,0 +1,6 @@ +services: + islandora_batch_action.commands: + class: Drupal\islandora_batch_action\Commands\BatchActionCommands + arguments: ['@entity_type.manager', '@database'] + tags: + - { name: drush.command } \ No newline at end of file diff --git a/islandora_batch_action.info.yml b/islandora_batch_action.info.yml new file mode 100644 index 0000000..69a5ecd --- /dev/null +++ b/islandora_batch_action.info.yml @@ -0,0 +1,5 @@ +name: Islandora Batch Action +description: Provides a Drush command to run a batch action on a list of nodes. +package: Custom +type: module +core_version_requirement: ^8 || ^9 || ^10 diff --git a/islandora_batch_action.links.menu.yml b/islandora_batch_action.links.menu.yml new file mode 100644 index 0000000..2d3858c --- /dev/null +++ b/islandora_batch_action.links.menu.yml @@ -0,0 +1,7 @@ +islandora_batch_action_create_media: + title: 'Add missing derivatives' + description: 'Add missing media by Media use and node Model' + route_name: islandora_batch_action.missing_derivatives_fm + parent: system.admin_config_media + menu_name: administration + weight: 10 \ No newline at end of file diff --git a/islandora_batch_action.routing.yml b/islandora_batch_action.routing.yml new file mode 100644 index 0000000..9ebf081 --- /dev/null +++ b/islandora_batch_action.routing.yml @@ -0,0 +1,7 @@ +islandora_batch_action.missing_derivatives_fm: + path: '/islandora-batch-action/missing-derivatives-fm' + defaults: + _title: 'Missing Derivatives' + _form: 'Drupal\islandora_batch_action\Form\MissingDerivativesFm' + requirements: + _permission: 'administer media' diff --git a/islandora_batch_action.services.yml b/islandora_batch_action.services.yml new file mode 100644 index 0000000..a948152 --- /dev/null +++ b/islandora_batch_action.services.yml @@ -0,0 +1,14 @@ +services: + islandora_batch_action.commands: + class: Drupal\islandora_batch_action\Commands\BatchActionCommands + arguments: ['@entity_type.manager'] + tags: + - { name: drush.command } + + islandora_batch_action.utils: + class: Drupal\islandora_batch_action\IslandoraBatchActionUtils + arguments: ['@database', '@entity_type.manager', '@logger.channel.islandora', '@islandora.utils'] + + islandora_batch_action.node_action_batch: + class: 'Drupal\islandora_batch_action\Batch\NodeActionBatch' + arguments: ['@entity_type.manager', '@plugin.manager.action'] \ No newline at end of file diff --git a/src/Batch/NodeActionBatch.php b/src/Batch/NodeActionBatch.php new file mode 100644 index 0000000..b79955c --- /dev/null +++ b/src/Batch/NodeActionBatch.php @@ -0,0 +1,76 @@ +setTitle(t('Performing batch action on nodes.')) + ->setInitMessage(t('Starting the batch action.')) + ->setProgressMessage(t('Processing @current out of @total.')) + ->setErrorMessage(t('An error occurred.')); + + // Register the operation for each node. + foreach ($node_ids as $node_id) { + $batch_builder->addOperation([__CLASS__, 'processNode'], [$node_id, $action_id]); + } + + batch_set($batch_builder->toArray()); + } + + /** + * Processes an individual node for the batch operation. + * + * @param int $node_id + * The node ID to process. + * @param string $action_id + * The action plugin ID. + * @param array $context + * The batch context array. + */ + public static function processNode(int $node_id, string $action_id, array &$context): void { + $entityTypeManager = \Drupal::service('entity_type.manager'); + $node = $entityTypeManager->getStorage('node')->load($node_id); + if (!$node) { + $context['results']['failed'][] = $node_id; + return; + } + $action_storage = $entityTypeManager->getStorage('action'); + $configured_action = $action_storage->load($action_id); + + if (!$configured_action) { + $context['results']['failed'][] = $node_id; + return; + } + try { + $configured_action->execute([$node]); + $context['results']['processed'][] = $node_id; + } + catch (\Exception $e) { + \Drupal::logger('islandora_batch_action')->error('Batch action failed for node @nid: @message', [ + '@nid' => $node_id, + '@message' => $e->getMessage(), + ]); + $context['results']['failed'][] = $node_id; + } + + } + +} diff --git a/src/Commands/BatchActionCommands.php b/src/Commands/BatchActionCommands.php new file mode 100644 index 0000000..ceff5ec --- /dev/null +++ b/src/Commands/BatchActionCommands.php @@ -0,0 +1,193 @@ +entityTypeManager = $entity_type_manager; + $this->database = $database; + } + + /** + * Runs a batch action on a list of nodes. + * + * @command batch:action + * @param string $action_id + * The machine name of the action to run. + * + * @usage drush batch:action my_custom_action + */ + public function runBatchAction($action_id) { + $action = $this->entityTypeManager->getStorage("action")->load($action_id); + + if (!$action) { + $this->logger()->error("Action not found: $action_id"); + return; + } + + $node_ids = $this->generateNodeList(); + + if (empty($node_ids)) { + $this->logger()->notice("No nodes found to process."); + return; + } + + $batch = [ + 'title' => t('Running action on nodes'), + 'operations' => [], + 'finished' => [__CLASS__, 'batchFinished'], + ]; + + foreach ($node_ids as $nid) { + $batch['operations'][] = [ + [__CLASS__, 'processNode'], + [$nid, $action_id], + ]; + } + + batch_set($batch); + drush_backend_batch_process(); + } + + /** + * Generates a list of node IDs that do not have 'Intermediate File' media. + * + * @return array + * An array of node IDs. + */ + + /** + * Generates a list of node IDs that do not have 'Intermediate File' media + * but have the model field set to 'Image'. + * + * @return array + * An array of node IDs. + */ + + /** + * Generates a list of node IDs that do not have 'Intermediate File' media + * but have the model field set to 'Image'. + * + * @return array + * An array of node IDs. + */ + protected function generateNodeList() { + // Subquery to get the TID for the term 'Image'. + $image_tid_query = $this->database->select('taxonomy_term_field_data', 't') + ->fields('t', ['tid']) + ->condition('t.name', 'Image') + ->condition('t.vid', 'islandora_models') + ->execute() + ->fetchField(); + + if (!$image_tid_query) { + $this->logger()->error("Taxonomy term 'Image' not found."); + return []; + } + $this->logger()->notice("Taxonomy term 'Image' is $image_tid_query."); + // Subquery to get nids with 'Intermediate File' media. + $subquery = $this->database->select('node', 'n') + ->fields('n', ['nid']); + + $subquery->innerJoin('media__field_media_of', 'mo', 'n.nid = mo.field_media_of_target_id'); + $subquery->innerJoin('media__field_media_use', 'mu', 'mu.entity_id = mo.entity_id'); + $subquery->innerJoin('taxonomy_term_field_data', 't', 'mu.field_media_use_target_id = t.tid'); + $subquery->condition('t.name', 'Intermediate File'); + + // Main query to get all nids excluding the subquery result, + // and where field_model_target_id is equal to the TID of 'Image' using EXISTS. + $query = $this->database->select('node', 'n') + ->fields('n', ['nid']) + ->condition('n.nid', $subquery, 'NOT IN'); + + // Adding the EXISTS condition for field_model_target_id. + $query->exists( + $this->database->select('node__field_model', 'fm') + ->fields('fm', ['entity_id']) + ->condition('fm.entity_id', 'n.nid', '=') + ->condition('fm.field_model_target_id', $image_tid_query) + ); + + return $query->execute()->fetchCol(); + } + + /** + * Processes an individual node in the batch. + * + * @param int $nid + * The node ID. + * @param string $action_id + * The action ID. + * @param array $context + * The batch context. + */ + public static function processNode($nid, $action_id, array &$context) { + $node = Node::load($nid); + $action = \Drupal::entityTypeManager()->getStorage("action")->load($action_id); + + if (!$node) { + $context['results']['errors'][] = "Node not found: $nid"; + return; + } + + if (!$action) { + $context['results']['errors'][] = "Action not found: $action_id"; + return; + } + + try { + $action->execute([$node]); + $context['results']['processed'][] = "Processed action on node $nid"; + } + catch (\Exception $e) { + $context['results']['errors'][] = "Error processing node $nid: " . $e->getMessage(); + } + } + + /** + * Callback for batch finished. + * + * @param bool $success + * Whether the batch completed successfully. + * @param array $results + * An array of results. + * @param array $operations + * The list of operations. + */ + public static function batchFinished($success, array $results, array $operations) { + if ($success) { + \Drupal::messenger()->addMessage("Batch process completed successfully."); + + if (!empty($results['processed'])) { + \Drupal::messenger()->addMessage("Processed nodes: " . implode(', ', $results['processed'])); + } + if (!empty($results['errors'])) { + \Drupal::messenger()->addError("Errors encountered: " . implode(', ', $results['errors'])); + } + } + else { + \Drupal::messenger()->addError("Batch process encountered an error."); + } + } + +} diff --git a/src/Form/MissingDerivativesFm.php b/src/Form/MissingDerivativesFm.php new file mode 100644 index 0000000..da7406e --- /dev/null +++ b/src/Form/MissingDerivativesFm.php @@ -0,0 +1,185 @@ +entityTypeManager = $entityTypeManager; + $this->islandoraBatchActionUtils = $islandoraBatchActionUtils; + $this->batchJob = $batchJob; + $this->actionPluginManager = $actionPluginManager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('islandora_batch_action.utils'), + $container->get('islandora_batch_action.node_action_batch'), + $container->get('plugin.manager.action'), + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId(): string { + return 'islandora_batch_action_missing_derivatives_fm'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state): array { + $actions = $this->getDerivativeActions(); + $form['model'] = [ + '#type' => 'select', + '#title' => $this->t('Node Type'), + '#options' => $this->getVocabularyOptions('islandora_models'), + '#required' => TRUE, + ]; + $form['usage'] = [ + '#type' => 'select', + '#title' => $this->t('Derivative Type'), + '#options' => $this->getVocabularyOptions('islandora_media_use'), + '#required' => TRUE, + ]; + $form['actions_id'] = [ + '#type' => 'select', + '#title' => $this->t('Derivative Actions'), + '#options' => $this->getDerivativeActions(), + '#required' => TRUE, + ]; + + $form['actions'] = [ + '#type' => 'actions', + 'submit' => [ + '#type' => 'submit', + '#value' => $this->t('Send'), + ], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state): void { + $model = $form_state->getValue('model'); + $usage = $form_state->getValue('usage'); + $action_id = $form_state->getValue('actions_id'); + $nids = $this->islandoraBatchActionUtils->generateNodeList($model, $usage); + if (empty($nids)) { + $this->messenger()->addWarning($this->t('No nodes found for the selected model and usage.')); + return; + } + $this->batchJob->run($nids, $action_id); + } + + /** + * Generates an options array from a taxonomy vocabulary. + * + * @param string $vocabulary + * The machine name of the taxonomy vocabulary. + * + * @return array + * An associative array of term IDs and term names. + */ + public function getVocabularyOptions(string $vocabulary): array { + $options = []; + + $terms = $this->entityTypeManager + ->getStorage('taxonomy_term') + ->loadTree($vocabulary); + + foreach ($terms as $term) { + $options[$term->tid] = $term->name; + } + + return $options; + } + + /** + * Gets filtered actions. + */ + public function getDerivativeActions(): array { + $options = []; + $user_actions = $this->entityTypeManager + ->getStorage('action') + ->loadMultiple(); + + foreach ($user_actions as $action) { + if ($action_plugin = $action->getPlugin()) { + if (method_exists($action_plugin, 'getConfiguration')) { + $config = $action_plugin->getConfiguration(); + if (isset($config['event'])&& $config['event'] == 'Generate Derivative') { + $options[$action->id()] = $action->label(); + } + } + } + } + return $options; + } + +} diff --git a/src/IslandoraBatchActionUtils.php b/src/IslandoraBatchActionUtils.php new file mode 100644 index 0000000..f6a4bc3 --- /dev/null +++ b/src/IslandoraBatchActionUtils.php @@ -0,0 +1,59 @@ +connection->select('media__field_media_of', 'mo'); + $mediaQuery->fields('mo', ['field_media_of_target_id']); + $mediaQuery->innerJoin('media__field_media_use', 'mu', 'mu.entity_id = mo.entity_id'); + $mediaQuery->condition('mu.field_media_use_target_id', $media_use_tid); + + // Query to get NIDs of the specified model, excluding those in subquery. + $mainQuery = $this->connection->select('node', 'n'); + $mainQuery->fields('n', ['nid']); + $mainQuery->innerJoin('node__field_model', 'fm', 'n.nid = fm.entity_id'); + $mainQuery->condition('fm.field_model_target_id', $model_tid); + $mainQuery->condition('n.nid', $mediaQuery, 'NOT IN'); + return $mainQuery->execute()->fetchCol(); + } + +}