commit
091e2995d4
9 changed files with 552 additions and 0 deletions
@ -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 } |
||||
@ -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 |
||||
@ -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 |
||||
@ -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' |
||||
@ -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'] |
||||
@ -0,0 +1,76 @@
|
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace Drupal\islandora_batch_action\Batch; |
||||
|
||||
use Drupal\Core\Batch\BatchBuilder; |
||||
|
||||
/** |
||||
* Provides a batch job to perform a Drupal Action on a list of node IDs. |
||||
*/ |
||||
final class NodeActionBatch { |
||||
|
||||
/** |
||||
* Executes the batch job. |
||||
* |
||||
* @param array $node_ids |
||||
* An array of node IDs. |
||||
* @param string $action_id |
||||
* The action plugin ID to perform on each node. |
||||
*/ |
||||
public function run(array $node_ids, string $action_id): void { |
||||
$batch_builder = new BatchBuilder(); |
||||
|
||||
$batch_builder->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; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,193 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_batch_action\Commands; |
||||
|
||||
use Drupal\node\Entity\Node; |
||||
use Drush\Commands\DrushCommands; |
||||
use Drupal\Core\Entity\EntityTypeManagerInterface; |
||||
use Drupal\Core\Database\Connection; |
||||
|
||||
/** |
||||
* Provides Drush commands for batch actions on nodes. |
||||
*/ |
||||
class BatchActionCommands extends DrushCommands { |
||||
|
||||
protected $entityTypeManager; |
||||
protected $database; |
||||
|
||||
/** |
||||
* Constructs the BatchActionCommands object. |
||||
* |
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager |
||||
* The entity type manager service. |
||||
* @param \Drupal\Core\Database\Connection $database |
||||
* The database connection. |
||||
*/ |
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, Connection $database) { |
||||
$this->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."); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,185 @@
|
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace Drupal\islandora_batch_action\Form; |
||||
|
||||
use Drupal\Core\Form\FormBase; |
||||
use Drupal\Core\Form\FormStateInterface; |
||||
use Drupal\Core\Entity\EntityTypeManagerInterface; |
||||
use Symfony\Component\DependencyInjection\ContainerInterface; |
||||
use Drupal\islandora_batch_action\IslandoraBatchActionUtils; |
||||
use Drupal\islandora_batch_action\Batch\NodeActionBatch; |
||||
use Drupal\Core\Action\ActionManager; |
||||
|
||||
/** |
||||
* Provides a form for sending a message in Islandora Batch Action. |
||||
*/ |
||||
final class MissingDerivativesFm extends FormBase { |
||||
|
||||
/** |
||||
* The entity type manager service. |
||||
* |
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface |
||||
*/ |
||||
protected EntityTypeManagerInterface $entityTypeManager; |
||||
|
||||
/** |
||||
* The Batch Action Utils service. |
||||
* |
||||
* @var \Drupal\islandora_batch_action\IslandoraBatchActionUtils |
||||
*/ |
||||
protected IslandoraBatchActionUtils $islandoraBatchActionUtils; |
||||
|
||||
/** |
||||
* The batch job service. |
||||
* |
||||
* @var \Drupal\islandora_batch_action\Batch\NodeActionBatch |
||||
*/ |
||||
protected NodeActionBatch $batchJob; |
||||
|
||||
/** |
||||
* The plugin manager for actions. |
||||
* |
||||
* @var \Drupal\Core\Action\ActionManager |
||||
*/ |
||||
protected ActionManager $actionPluginManager; |
||||
|
||||
/** |
||||
* Constructs the form object with dependency injection. |
||||
* |
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager |
||||
* The entity type manager service. |
||||
* @param \Drupal\islandora_batch_action\IslandoraBatchActionUtils $islandoraBatchActionUtils |
||||
* The Batch Action utils. |
||||
* @param \Drupal\islandora_batch_action\Batch\NodeActionBatch $batchJob |
||||
* The batch job service. |
||||
* @param \Drupal\Core\Action\ActionManager $actionPluginManager |
||||
* The action plugin manager. |
||||
*/ |
||||
public function __construct( |
||||
EntityTypeManagerInterface $entityTypeManager, |
||||
IslandoraBatchActionUtils $islandoraBatchActionUtils, |
||||
NodeActionBatch $batchJob, |
||||
ActionManager $actionPluginManager) { |
||||
$this->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; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,59 @@
|
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace Drupal\islandora_batch_action; |
||||
|
||||
use Drupal\Core\Database\Connection; |
||||
use Drupal\Core\Entity\EntityTypeManagerInterface; |
||||
use Drupal\Core\Logger\LoggerChannelInterface; |
||||
|
||||
/** |
||||
* Provides utility functions for bulk derivative actions in Islandora. |
||||
*/ |
||||
final class IslandoraBatchActionUtils { |
||||
|
||||
/** |
||||
* Constructs an IslandoraBatchActionUtils object. |
||||
* |
||||
* @param \Drupal\Core\Database\Connection $connection |
||||
* The database connection service. |
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager |
||||
* The entity type manager service. |
||||
* @param \Drupal\Core\Logger\LoggerChannelInterface $loggerChannelIslandora |
||||
* The logger channel for Islandora. |
||||
*/ |
||||
public function __construct( |
||||
private readonly Connection $connection, |
||||
private readonly EntityTypeManagerInterface $entityTypeManager, |
||||
private readonly LoggerChannelInterface $loggerChannelIslandora, |
||||
) {} |
||||
|
||||
/** |
||||
* Generates a list of node IDs without the specified media type. |
||||
* |
||||
* @param string $model_tid |
||||
* The taxonomy term ID of the model (e.g., Image). |
||||
* @param string $media_use_tid |
||||
* The taxonomy term ID of the media use type (e.g., Intermediate File). |
||||
* |
||||
* @return array |
||||
* Array of node IDs that match the model type but exclude the media type. |
||||
*/ |
||||
public function generateNodeList(string $model_tid, string $media_use_tid): array { |
||||
// Subquery to get NIDs with the specified media type. |
||||
$mediaQuery = $this->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(); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue