From 92ea91a34534eb8f9c6696250edff796d06bf2ac Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Fri, 11 Oct 2024 17:46:44 -0400 Subject: [PATCH] VBO actions using AMI UUIDs. Almost there. Probably another 20 lines of code and PUFFF... a lily transforms into a weee fish... beautiful magic @alliomeria --- ami.links.task.yml | 12 +- ami.permissions.yml | 5 + ami.routing.yml | 9 + src/AmiUtilityService.php | 2 +- .../amiSetEntityAccessControlHandler.php | 12 + .../Controller/amiSetEntityListBuilder.php | 7 + src/Entity/amiSetEntity.php | 2 + src/Form/amiSetEntityActionProcessedForm.php | 329 ++++++++++++++++++ .../AmiStrawberryfieldJsonAsWebform.php | 2 +- .../QueueWorker/ActionADOQueueWorker.php | 4 - 10 files changed, 375 insertions(+), 9 deletions(-) create mode 100644 src/Form/amiSetEntityActionProcessedForm.php diff --git a/ami.links.task.yml b/ami.links.task.yml index 36eeecd..2b8f84e 100644 --- a/ami.links.task.yml +++ b/ami.links.task.yml @@ -39,20 +39,26 @@ ami_set_entity.delete_process_form: title: Delete Processed ADOs weight: 12 +ami_set_entity.action_process_form: + route_name: entity.ami_set_entity.action_process_form + base_route: entity.ami_set_entity.canonical + title: Run Action on Processed ADOs + weight: 13 + ami_set_entity.reconcile_form: route_name: entity.ami_set_entity.reconcile_form base_route: entity.ami_set_entity.canonical title: Reconcile LoD - weight: 13 + weight: 14 entity.ami_set_entity.reconcileedit_form: route_name: entity.ami_set_entity.reconcileedit_form base_route: entity.ami_set_entity.canonical title: Edit Reconciled LoD - weight: 14 + weight: 15 entity.ami_set_entity.report_form: route_name: entity.ami_set_entity.report_form base_route: entity.ami_set_entity.canonical title: Reports - weight: 15 + weight: 16 diff --git a/ami.permissions.yml b/ami.permissions.yml index 4144721..7a42a3c 100644 --- a/ami.permissions.yml +++ b/ami.permissions.yml @@ -37,5 +37,10 @@ deleteados amiset entity: restrict access: TRUE deleteados own amiset entity: title: 'Delete ADOs generated through own AMI Set Entities' +actionados amiset entity: + title: 'Run Action on ADOs referenced on any AMI Set Entities' + restrict access: TRUE +actionados own amiset entity: + title: 'Run Action on ADOs referenced on own AMI Set Entities' override file destination ami entity: title: 'Override default persistent file destination and naming for Files ingested via AMI Set Entities. This permission breaks how Archipelago conceives file preservation as a core concern so out of the box will not trigger any chance without enabled a Drupal settings.php global option. See Documentation for this' diff --git a/ami.routing.yml b/ami.routing.yml index 8638fe8..c997322 100644 --- a/ami.routing.yml +++ b/ami.routing.yml @@ -87,6 +87,15 @@ entity.ami_set_entity.delete_process_form: requirements: _entity_access: 'ami_set_entity.deleteados' +entity.ami_set_entity.action_process_form: + path: '/amiset/{ami_set_entity}/actionprocessed' + defaults: + _entity_form: ami_set_entity.actionprocessed + _title: 'Run Action on Ami Set' + requirements: + _entity_access: 'ami_set_entity.actionados' + + entity.ami_set_entity.reconcile_form: path: '/amiset/{ami_set_entity}/reconcile' defaults: diff --git a/src/AmiUtilityService.php b/src/AmiUtilityService.php index 3c08e6f..87025ee 100644 --- a/src/AmiUtilityService.php +++ b/src/AmiUtilityService.php @@ -2680,7 +2680,7 @@ public static function checkAmiSetDeleteAdosAccess(EntityInterface $entity): boo if ($set_field instanceof \Drupal\strawberryfield\Field\StrawberryFieldItemList) { $set = json_decode($entity->get('set')->getString(), TRUE); if (json_last_error() == JSON_ERROR_NONE) { - $deleteados_access = (empty($set['pluginconfig']['op']) || !in_array($set['pluginconfig']['op'], ['update', 'patch'])); + $deleteados_access = (empty($set['pluginconfig']['op']) || !in_array($set['pluginconfig']['op'], ['update', 'patch', 'sync'])); \Drupal::cache()->set($cache_id, $deleteados_access); } } diff --git a/src/Entity/Controller/amiSetEntityAccessControlHandler.php b/src/Entity/Controller/amiSetEntityAccessControlHandler.php index 0468f20..2778c2c 100644 --- a/src/Entity/Controller/amiSetEntityAccessControlHandler.php +++ b/src/Entity/Controller/amiSetEntityAccessControlHandler.php @@ -94,6 +94,18 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter ->cachePerPermissions() ->addCacheableDependency($entity); } + case 'actionados': + if ($account->hasPermission('actionados amiset entity')) { + return AccessResult::allowed()->cachePerPermissions(); + } + if ($account->hasPermission('actionados own amiset entity') && $is_owner) { + return AccessResult::allowed()->cachePerPermissions()->cachePerUser()->addCacheableDependency($entity); + } + else { + return AccessResult::neutral() + ->cachePerPermissions() + ->addCacheableDependency($entity); + } default: return AccessResult::neutral()->cachePerPermissions(); } diff --git a/src/Entity/Controller/amiSetEntityListBuilder.php b/src/Entity/Controller/amiSetEntityListBuilder.php index 4905401..9385320 100644 --- a/src/Entity/Controller/amiSetEntityListBuilder.php +++ b/src/Entity/Controller/amiSetEntityListBuilder.php @@ -88,6 +88,13 @@ public function getOperations(EntityInterface $entity) { 'url' => $this->ensureDestination($entity->toUrl('delete-process-form')), ]; } + if ($entity->access('actionados') && $entity->hasLinkTemplate('action-process-form')) { + $operations['action_processed'] = [ + 'title' => $this->t('Run Action on Processed ADOs'), + 'weight' => 12, + 'url' => $this->ensureDestination($entity->toUrl('action-process-form')), + ]; + } } return $operations; } diff --git a/src/Entity/amiSetEntity.php b/src/Entity/amiSetEntity.php index fec15b3..7c4d44c 100644 --- a/src/Entity/amiSetEntity.php +++ b/src/Entity/amiSetEntity.php @@ -84,6 +84,7 @@ * "delete" = "Drupal\ami\Form\amiSetEntityDeleteForm", * "process" = "Drupal\ami\Form\amiSetEntityProcessForm", * "deleteprocessed" = "Drupal\ami\Form\amiSetEntityDeleteProcessedForm", + * "actionprocessed" = "Drupal\ami\Form\amiSetEntityActionProcessedForm", * "reconcile" = "Drupal\ami\Form\amiSetEntityReconcileForm", * "editreconcile" = "Drupal\ami\Form\amiSetEntityReconcileCleanUpForm", * "report" = "Drupal\ami\Form\amiSetEntityReportForm" @@ -104,6 +105,7 @@ * "edit-form" = "/amiset/{ami_set_entity}/edit", * "process-form" = "/amiset/{ami_set_entity}/process", * "delete-process-form" = "/amiset/{ami_set_entity}/deleteprocessed", + * "action-form" = "/amiset/{ami_set_entity}/actionprocessed", * "reconcile-form" = "/amiset/{ami_set_entity}/reconcile", * "edit-reconcile-form" = "/amiset/{ami_set_entity}/editreconcile", * "delete-form" = "/amiset/{ami_set_entity}/delete", diff --git a/src/Form/amiSetEntityActionProcessedForm.php b/src/Form/amiSetEntityActionProcessedForm.php new file mode 100644 index 0000000..8f72dd3 --- /dev/null +++ b/src/Form/amiSetEntityActionProcessedForm.php @@ -0,0 +1,329 @@ +AmiUtilityService = $ami_utility; + /** @var ViewsBulkOperationsActionManager $actionManager */ + $this->actionManager = $actionManager; + /** @var ViewsBulkOperationsActionProcessorInterface $actionProcessor */ + $this->actionProcessor = $actionProcessor; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.repository'), + $container->get('entity_type.bundle.info'), + $container->get('datetime.time'), + $container->get('ami.utility'), + $container->get('plugin.manager.views_bulk_operations_action'), + $container->get('views_bulk_operations.processor') + ); + } + + + + public function getQuestion() { + return $this->t('Are you sure you want to execute action on ADOs generated by %name?', ['%name' => $this->entity->label()]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('entity.ami_set_entity.action_process_form',['ami_set_entity' => $this->entity->id()]); + } + + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $csv_file_reference = $this->entity->get('source_data')->getValue(); + if (isset($csv_file_reference[0]['target_id'])) { + $file = $this->entityTypeManager->getStorage('file')->load($csv_file_reference[0]['target_id']); + } + $action_config = []; + $pluginid = $form_state->getValue('ami_select_action') ?? NULL; + if (!empty($pluginid)) { + $action = $this->actionManager->createInstance($pluginid, []); + if (\method_exists($action, 'buildConfigurationForm')) { + $action->submitConfigurationForm($form, $form_state); + } + $action_config = $action->getConfiguration(); + } + else { + $form_state->setErrorByName('ami_select_action', $this->t('You need to select an Action')); + $form_state->setRebuild(TRUE); + return; + } + + // Fetch Zip file if any + $zip_file = NULL; + $zip_file_reference = $this->entity->get('zip_file')->getValue(); + if (isset($zip_file_reference[0]['target_id'])) { + /** @var \Drupal\file\Entity\File $zip_file */ + $zip_file = $this->entityTypeManager->getStorage('file')->load( + $zip_file_reference[0]['target_id'] + ); + } + $data = new \stdClass(); + foreach($this->entity->get('set') as $item) { + /* @var \Drupal\strawberryfield\Plugin\Field\FieldType\StrawberryFieldItem $item */ + $data = $item->provideDecoded(FALSE); + } + if ($file && $data!== new \stdClass()) { + + if (TRUE) { + $data_csv = clone $data; +/* + $data->info = [ + 'csv_file' => The CSV File that will (or we hope so if well formed) generate multiple ADO Queue items + 'csv_file_name' => Only present if this is called not from the root + 'set_id' => The Set id + 'uid' => The User ID that processed the Set + 'set_url' => A direct URL to the set. + 'action' => The action to run + 'action_config' => An array of additional configs/settings the particular action takes. + 'attempt' => The number of attempts to process. We always start with a 1 + 'zip_file' => Zip File/File Entity + 'queue_name' => because well ... we use Hydroponics too + 'time_submitted' => Timestamp on when the queue was send. All Entries will share the same + 'batch_size' => the number of ADOs to process via a batch action. Some actions like detele can/should handle multiple UUIDs at the same time in a single Queue item + ]; +*/ + // Overrides the original OP + $SetURL = $this->entity->toUrl('canonical', ['absolute' => TRUE]) + ->toString(); + + $run_timestamp = $this->time->getCurrentTime(); + $data_csv->pluginconfig->op = "action"; + $data_csv->info = [ + 'zip_file' => $zip_file, + 'csv_file' => $file, + 'set_id' => $this->entity->id(), + 'uid' => $this->currentUser()->id(), + 'action' => $pluginid, + 'action_config' => $action_config ?? [], + 'set_url' => $SetURL, + 'attempt' => 1, + 'queue_name' => 'ami_action_ado', + 'time_submitted' => $run_timestamp, + 'batch_size' => 25 + ]; + \Drupal::queue('ami_csv_ado') + ->createItem($data_csv); + $form_state->setRedirectUrl($this->getCancelUrl()); + $this->messenger()->addStatus( + $this->t('Your ADOs have been enqueued for Action Processing') + ); + } + else { + // Only UUIDs you can delete will be added. + // 0.9.0 Change: This method now returns UUIDs in the keys. The values are/if any/children CSVs. + $data->info['uid'] = $this->currentUser()->id(); + $uuids = array_keys($this->AmiUtilityService->getProcessedAmiSetNodeUUids($file, $data, 'edit')); + $uuids = array_unique($uuids); + if (empty($uuids)) { + $form_state->setRebuild(); + $this->messenger()->addWarning( + $this->t('So Sorry. There either no ADOs in the current CSV that can be processed or that you have permission to. Please correct or try to manually process actions on your ADOs. You may already have delete them too!') + ); + return; + } + $operations = []; + foreach (array_chunk($uuids, 50) as $batch_data_uuid) { + $operations[] = ['\Drupal\ami\Form\amiSetEntityActionProcessedForm::batchAction' + , [$batch_data_uuid]]; + } + // Setup and define batch information. + $batch = array( + 'title' => t('processing action on ADOs in batch...'), + 'operations' => $operations, + 'finished' => '\Drupal\ami\Form\amiSetEntityDeleteProcessedForm::batchFinished', + ); + $this->entity->setStatus(amiSetEntity::STATUS_ENQUEUED); + $this->entity->save(); + $form_state->setRedirectUrl($this->getCancelUrl()); + batch_set($batch); + } + + } else { + $this->messenger()->addError( + $this->t('So Sorry. Ami Set @label has incorrect Metadata and/or has its CSV file missing. We need it to know which ADOs where generated via this Set. Please correct or manually delete your ADOs.', + [ + '@label' => $this->entity->label(), + ] + ) + ); + } + } + + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form['#prefix'] = '
'; + $form['#suffix'] = '
'; + $data = new \stdClass(); + foreach ($this->entity->get('set') as $item) { + /** @var \Drupal\strawberryfield\Plugin\Field\FieldType\StrawberryFieldItem $item */ + $data = $item->provideDecoded(FALSE); + } + if ($data !== new \stdClass()) { + $actions = []; + $form['process_enqueued'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Execute Action on Referenced ADOs via this Set'), + '#description' => $this->t('Confirming will trigger a Batch Action processing for already ingested ADOs referenced by this AMI set you have permission to act on.'), + ]; + foreach ($this->actionManager->getDefinitions() as $id => $definition) { + if (empty($definition['type']) || \in_array($definition['type'], ['node'], TRUE)) { + $actions[$id] = $definition; + } + } + $ajax = [ + 'callback' => [get_class($this), 'ajaxCallback'], + 'wrapper' => 'action-ajax-container', + ]; + $action_options = []; + foreach ($actions as $id => $definition) { + $action_options[$id] = $definition['label'] ?? $id; + } + $form['ami_select_action'] = [ + '#type' => 'select', + '#title' => $this->t('Execute Action on Ingested ADOs via this Set'), + '#description' => $this->t('Confirming will trigger a Batch Action processing for already ingested ADOs you have permission to act on.'), + "#empty_option" =>t('- Select One -'), + '#options' => $action_options, + '#ajax' => $ajax , + '#required' => TRUE, + ]; + + + + $pluginid = $form_state->getValue('ami_select_action') ?? NULL; + if (!empty($pluginid)) { + $action = $this->actionManager->createInstance($pluginid, []); + + /* if (\method_exists($action, 'setContext')) { + $action->setContext($form_data); + } + */ + if (\method_exists($action, 'buildConfigurationForm')) { + $elements = $action->buildConfigurationForm([], $form_state); + $form = $form + $elements; + } + } + } + return $form + parent::buildForm($form, $form_state); + } + + + public static function batchAction($batch_data_uuid, &$context) { + // Deleting nodes. + $storage_handler = \Drupal::entityTypeManager()->getStorage('node'); + $entities = $storage_handler->loadByProperties(['uuid' => $batch_data_uuid]); + $batch_size=sizeof($batch_data_uuid); + $batch_number=sizeof($context['results'])+1; + try { + //$storage_handler->delete($entities); + // Display data while running batch. + + $context['message'] = sprintf("processing Action %s on ADOs per batch. Batch #%s" + , $batch_size, $batch_number); + $context['results'][] = sizeof($batch_data_uuid); + } + catch (EntityStorageException $e) { + $context['message'] = sprintf("Exception while processing Action %s on ADOs per batch. Batch #%s" + , $batch_size, $batch_number); + $context['results'][] = 0; + } + + } + + // What to do after batch ran. Display success or error message. + public static function batchFinished($success, $results, $operations) { + if ($success) { + $message = count($results) . ' batches processed.'; + } + else { + $message = 'Finished with an error.'; + } + + $messenger = \Drupal::messenger(); + if (isset($message)) { + $messenger->addMessage($message); + } + } + + /** + * Ajax callback. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return array + * An partial Form. + */ + public static function ajaxCallback(array $form, FormStateInterface $form_state) { + $form_state->setRebuild(); + return $form; + } +} + diff --git a/src/Plugin/Action/AmiStrawberryfieldJsonAsWebform.php b/src/Plugin/Action/AmiStrawberryfieldJsonAsWebform.php index 3aa0ba6..d979259 100644 --- a/src/Plugin/Action/AmiStrawberryfieldJsonAsWebform.php +++ b/src/Plugin/Action/AmiStrawberryfieldJsonAsWebform.php @@ -64,7 +64,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta $form_state->setAlwaysProcess(TRUE); $webform = $this->AmiUtilityService->getWebforms(); $form['#tree'] = TRUE; - $form['webform'] =[ + $form['webform'] = [ '#type' => 'select', '#title' => $this->t('Select which Webform you want to use'), '#options' => $webform, diff --git a/src/Plugin/QueueWorker/ActionADOQueueWorker.php b/src/Plugin/QueueWorker/ActionADOQueueWorker.php index ceb3308..3aa73fa 100644 --- a/src/Plugin/QueueWorker/ActionADOQueueWorker.php +++ b/src/Plugin/QueueWorker/ActionADOQueueWorker.php @@ -258,7 +258,6 @@ private function processAction($data): bool|null { ); } catch (\Exception $e) { - error_log($e->getMessage()); $message = $this->t('Error loading NODES for @action on ADOs via Set @setid.', [ '@setid' => $data->info['set_id'], '@action' => $data->info['action'], @@ -279,8 +278,6 @@ private function processAction($data): bool|null { $account = $data->info['uid'] == \Drupal::currentUser()->id() ? \Drupal::currentUser() : $this->entityTypeManager->getStorage('user')->load($data->info['uid']); // Each Action might have its own check/permission. But we know for sure delete requires `delete` $access_type = "delete"; - error_log($account->getAccountName()); - if ($account) { foreach ($existing as $key => $existing_object) { if (!$existing_object->access($access_type, $account)) { @@ -309,7 +306,6 @@ private function processAction($data): bool|null { } } $existing = array_filter($existing); - error_log("number to delete". count($existing)); if ($data->info['action'] ?? NULL == 'delete') { try { $this->entityTypeManager->getStorage('node')->delete($existing);