Browse Source

initial commit

main
astanley 7 months ago
commit
091e2995d4
  1. 6
      drush.services.yml
  2. 5
      islandora_batch_action.info.yml
  3. 7
      islandora_batch_action.links.menu.yml
  4. 7
      islandora_batch_action.routing.yml
  5. 14
      islandora_batch_action.services.yml
  6. 76
      src/Batch/NodeActionBatch.php
  7. 193
      src/Commands/BatchActionCommands.php
  8. 185
      src/Form/MissingDerivativesFm.php
  9. 59
      src/IslandoraBatchActionUtils.php

6
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 }

5
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

7
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

7
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'

14
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']

76
src/Batch/NodeActionBatch.php

@ -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;
}
}
}

193
src/Commands/BatchActionCommands.php

@ -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.");
}
}
}

185
src/Form/MissingDerivativesFm.php

@ -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;
}
}

59
src/IslandoraBatchActionUtils.php

@ -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…
Cancel
Save