From 54206de71204691ef67a28b1036a8d5f7df0ad0a Mon Sep 17 00:00:00 2001 From: Adam <607975+adam-vessey@users.noreply.github.com> Date: Wed, 24 Apr 2024 14:13:41 -0300 Subject: [PATCH 01/14] Fix Functional Javascript CI tests (#1004) * Fixup js tests * Move MINK env var into phpunit for easier local testing * Fix xpath for 10.3 --------- Co-authored-by: Joe Corall --- .github/workflows/build-2.x.yml | 42 ++-- phpunit.xml | 2 +- .../views.view.test_integer_weight.yml | 221 ++++++++++-------- .../IntegerWeightTest.php | 39 ++-- 4 files changed, 165 insertions(+), 139 deletions(-) diff --git a/.github/workflows/build-2.x.yml b/.github/workflows/build-2.x.yml index c5d9d66b0..8cd9f1d1e 100644 --- a/.github/workflows/build-2.x.yml +++ b/.github/workflows/build-2.x.yml @@ -1,30 +1,26 @@ -# This is a basic workflow to help you get started with Actions - name: CI -# Controls when the action will run. on: - # Triggers the workflow on push or pull request events but only for the 2.x branch push: branches: [ 2.x ] pull_request: branches: [ 2.x ] - - # Allows you to run this workflow manually from the Actions tab workflow_dispatch: -# A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: - # This workflow contains a single job called "build" build: - # The type of runner that the job will run on + env: + DRUPAL_VERSION: ${{ matrix.drupal-version }} + SCRIPT_DIR: ${{ github.workspace }}/islandora_ci + DRUPAL_DIR: /opt/drupal + PHPUNIT_FILE: ${{ github.workspace }}/build_dir/phpunit.xml + runs-on: ubuntu-latest continue-on-error: ${{ matrix.allowed_failure }} strategy: fail-fast: false matrix: php-versions: ["8.1", "8.2", "8.3"] - # test-suite functional-javascript will appear to pass but will skip tests; missing chromedriver. test-suite: ["kernel", "functional", "functional-javascript"] drupal-version: ["10.1.x", "10.2.x", "10.3.x-dev"] mysql: ["8.0"] @@ -51,17 +47,15 @@ jobs: - 61616:61616 - 61613:61613 - # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: build_dir - name: Checkout islandora_ci - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: islandora/islandora_ci ref: github-actions @@ -79,13 +73,6 @@ jobs: sudo apt-get remove -y mysql-client mysql-common sudo apt-get install -y mysql-client - - name: Set environment variables - run: | - echo "DRUPAL_VERSION=${{ matrix.drupal-version }}" >> $GITHUB_ENV - echo "SCRIPT_DIR=$GITHUB_WORKSPACE/islandora_ci" >> $GITHUB_ENV - echo "DRUPAL_DIR=/opt/drupal" >> $GITHUB_ENV - echo "PHPUNIT_FILE=$GITHUB_WORKSPACE/build_dir/phpunit.xml" >> $GITHUB_ENV - - name: Cache Composer dependencies uses: actions/cache@v3 with: @@ -118,8 +105,21 @@ jobs: - name: Test scripts run: $SCRIPT_DIR/travis_scripts.sh + + - name: Start chromedriver + if: matrix.test-suite == 'functional-javascript' + run: |- + /usr/local/share/chromedriver-linux64/chromedriver \ + --log-path=/tmp/chromedriver.log \ + --verbose \ + --allowed-ips= \ + --allowed-origins=* & - name: PHPUNIT tests run: | cd $DRUPAL_DIR/web/core $DRUPAL_DIR/vendor/bin/phpunit --verbose --testsuite "${{ matrix.test-suite }}" + + - name: Print chromedriver logs + if: matrix.test-suite == 'functional-javascript' + run: cat /tmp/chromedriver.log diff --git a/phpunit.xml b/phpunit.xml index 46e82e78d..bc0609fba 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -47,7 +47,7 @@ - + diff --git a/tests/modules/integer_weight_test_views/test_views/views.view.test_integer_weight.yml b/tests/modules/integer_weight_test_views/test_views/views.view.test_integer_weight.yml index 08dc4912a..9ccb76a81 100644 --- a/tests/modules/integer_weight_test_views/test_views/views.view.test_integer_weight.yml +++ b/tests/modules/integer_weight_test_views/test_views/views.view.test_integer_weight.yml @@ -4,6 +4,7 @@ dependencies: config: - node.type.repo_item module: + - islandora - node - user id: test_integer_weight @@ -13,87 +14,36 @@ description: '' tag: '' base_table: node_field_data base_field: nid -core: 8.x display: default: - display_plugin: default id: default display_title: Master + display_plugin: default position: 0 display_options: - access: - type: perm - options: - perm: 'access content' - cache: - type: tag - options: { } - query: - type: views_query - options: - disable_sql_rewrite: false - distinct: false - replica: false - query_comment: '' - query_tags: { } - exposed_form: - type: basic - options: - submit_button: Apply - reset_button: false - reset_button_label: Reset - exposed_sorts_label: 'Sort by' - expose_sort_order: true - sort_asc_label: Asc - sort_desc_label: Desc - pager: - type: mini - options: - items_per_page: 10 - offset: 0 - id: 0 - total_pages: null - expose: - items_per_page: false - items_per_page_label: 'Items per page' - items_per_page_options: '5, 10, 25, 50' - items_per_page_options_all: false - items_per_page_options_all_label: '- All -' - offset: false - offset_label: Offset - tags: - previous: ‹‹ - next: ›› - style: - type: table - row: - type: fields + title: 'test weight' fields: title: id: title table: node_field_data field: title + relationship: none + group_type: group + admin_label: '' entity_type: node entity_field: title + plugin_id: field + label: Title + exclude: false alter: alter_text: false make_link: false absolute: false - trim: false word_boundary: false ellipsis: false strip_tags: false + trim: false html: false - hide_empty: false - empty_zero: false - settings: - link_to_entity: true - plugin_id: field - relationship: none - group_type: group - admin_label: '' - label: Title - exclude: false element_type: '' element_class: '' element_label_type: '' @@ -103,9 +53,13 @@ display: element_wrapper_class: '' element_default_classes: true empty: '' + hide_empty: false + empty_zero: false hide_alter_empty: true click_sort_column: value type: string + settings: + link_to_entity: true group_column: value group_columns: { } group_rows: true @@ -123,7 +77,8 @@ display: relationship: none group_type: group admin_label: '' - label: 'Integer weight selector (field_integer_weight)' + plugin_id: integer_weight_selector + label: 'Integer Weight Selector (field_integer_weight)' exclude: false alter: alter_text: false @@ -164,44 +119,57 @@ display: hide_empty: false empty_zero: false hide_alter_empty: true - range: '20' - plugin_id: integer_weight_selector - filters: - status: - value: '1' - table: node_field_data - field: status - plugin_id: boolean - entity_type: node - entity_field: status - id: status + pager: + type: mini + options: + offset: 0 + items_per_page: 10 + total_pages: null + id: 0 + tags: + next: ›› + previous: ‹‹ expose: - operator: '' - group: 1 - type: - id: type - table: node_field_data - field: type - value: - repo_item:repo_item - entity_type: node - entity_field: type - plugin_id: bundle + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + empty: { } sorts: created: id: created table: node_field_data field: created - order: DESC - entity_type: node - entity_field: created - plugin_id: date relationship: none group_type: group admin_label: '' - exposed: false + entity_type: node + entity_field: created + plugin_id: date + order: DESC expose: label: '' + exposed: false granularity: second field_integer_weight_value: id: field_integer_weight_value @@ -210,17 +178,82 @@ display: relationship: none group_type: group admin_label: '' + plugin_id: standard order: ASC + expose: + label: '' + field_identifier: '' + exposed: false + arguments: { } + filters: + status: + id: status + table: node_field_data + field: status + entity_type: node + entity_field: status + plugin_id: boolean + value: '1' + group: 1 + expose: + operator: '' + type: + id: type + table: node_field_data + field: type + relationship: none + group_type: group + admin_label: '' + entity_type: node + entity_field: type + plugin_id: bundle + operator: in + value: + repo_item: repo_item + group: 1 exposed: false expose: + operator_id: '' label: '' - plugin_id: standard - title: 'test weight' + description: '' + use_operator: false + operator: '' + operator_limit_selection: false + operator_list: { } + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + style: + type: table + row: + type: fields + query: + type: views_query + options: + query_comment: '' + disable_sql_rewrite: false + distinct: false + replica: false + query_tags: { } + relationships: { } header: { } footer: { } - empty: { } - relationships: { } - arguments: { } display_extenders: { } cache_metadata: max-age: -1 @@ -232,9 +265,9 @@ display: - user.permissions tags: { } page_1: - display_plugin: page id: page_1 display_title: Page + display_plugin: page position: 1 display_options: display_extenders: { } diff --git a/tests/src/FunctionalJavascript/IntegerWeightTest.php b/tests/src/FunctionalJavascript/IntegerWeightTest.php index 2572c191e..5e10133c4 100644 --- a/tests/src/FunctionalJavascript/IntegerWeightTest.php +++ b/tests/src/FunctionalJavascript/IntegerWeightTest.php @@ -8,6 +8,7 @@ use Drupal\field\Entity\FieldStorageConfig; use Drupal\field\Entity\FieldConfig; use Drupal\node\Entity\Node; +use Drupal\views\Tests\ViewTestData; /** * Test integer weight selector. @@ -30,6 +31,7 @@ class IntegerWeightTest extends WebDriverTestBase { 'views', 'field_ui', 'integer_weight_test_views', + 'islandora', ]; /** @@ -82,27 +84,16 @@ class IntegerWeightTest extends WebDriverTestBase { */ public function setUp(): void { parent::setUp(); + $this->drupalCreateContentType([ + 'type' => 'repo_item', + 'name' => 'Repository Item', + ]); - $this->adminUser = $this->drupalCreateUser( - [ - 'administer content types', - 'administer node fields', - 'administer node display', - ] - ); - - // Create dummy repo_item type to sort (since we don't have - // repository_object without islandora_defaults). - $type = $this->container->get('entity_type.manager')->getStorage('node_type') - ->create([ - 'type' => 'repo_item', - 'name' => 'Repository Item', - ]); - $type->save(); - $this->container->get('router.builder')->rebuild(); + $account = $this->createUser(['edit any repo_item content'], 'test', TRUE); + $this->drupalLogin($account); $fieldStorage = FieldStorageConfig::create([ - 'fieldName' => static::$fieldName, + 'field_name' => static::$fieldName, 'entity_type' => 'node', 'type' => static::$fieldType, ]); @@ -124,16 +115,18 @@ public function setUp(): void { $this->nodes[] = $node; } - ViewsTestData::createTestViews(get_class($this), ['integer_weight_test_views']); + ViewTestData::createTestViews(get_class($this), ['integer_weight_test_views']); } /** * Test integer weight selector. */ public function testIntegerWeightSelector() { - $this->drupalGet('test-integer-weight'); - $page = $this->getSession()->getPage(); + $web_assert = $this->assertSession(); + $this->drupalGet('/test-integer-weight'); + $web_assert->pageTextContains('Item 1'); + $page = $this->getSession()->getPage(); $weight_select1 = $page->findField("field_integer_weight[0][weight]"); $weight_select2 = $page->findField("field_integer_weight[1][weight]"); $weight_select3 = $page->findField("field_integer_weight[2][weight]"); @@ -153,8 +146,8 @@ public function testIntegerWeightSelector() { $this->assertSession()->pageTextNotContains('You have unsaved changes.'); // Drag and drop 'Item 1' over 'Item 2'. - $dragged = $this->xpath("//tr[@class='draggable'][1]//a[@class='tabledrag-handle']")[0]; - $target = $this->xpath("//tr[@class='draggable'][2]//a[@class='tabledrag-handle']")[0]; + $dragged = $this->xpath("//tr[contains(@class, 'draggable')][1]//a[contains(@class, 'tabledrag-handle')]")[0]; + $target = $this->xpath("//tr[contains(@class, 'draggable')][2]//a[contains(@class, 'tabledrag-handle')]")[0]; $dragged->dragTo($target); // Pause for javascript to do it's thing. From c80769580cdc8e722b9d28be0b87beef4f803d70 Mon Sep 17 00:00:00 2001 From: Aron Novak Date: Wed, 24 Apr 2024 18:46:01 +0000 Subject: [PATCH 02/14] Do not have a fatal error on a missing action (#1014) --- src/PresetReaction/PresetReaction.php | 29 ++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/PresetReaction/PresetReaction.php b/src/PresetReaction/PresetReaction.php index 98aa69462..4ebd68235 100644 --- a/src/PresetReaction/PresetReaction.php +++ b/src/PresetReaction/PresetReaction.php @@ -7,6 +7,7 @@ use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -21,12 +22,20 @@ class PresetReaction extends ContextReactionPluginBase implements ContainerFacto */ protected $actionStorage; + /** + * The logger. + * + * @var \Psr\Log\LoggerInterface + */ + protected $logger; + /** * {@inheritdoc} */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityStorageInterface $action_storage) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityStorageInterface $action_storage, LoggerInterface $logger) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->actionStorage = $action_storage; + $this->logger = $logger; } /** @@ -37,7 +46,8 @@ public static function create(ContainerInterface $container, array $configuratio $configuration, $plugin_id, $plugin_definition, - $container->get('entity_type.manager')->getStorage('action') + $container->get('entity_type.manager')->getStorage('action'), + $container->get('logger.factory')->get('islandora') ); } @@ -56,7 +66,20 @@ public function execute(EntityInterface $entity = NULL) { $action_ids = $config['actions']; foreach ($action_ids as $action_id) { $action = $this->actionStorage->load($action_id); - $action->execute([$entity]); + if (empty($action)) { + $this->logger->warning('Action "@action" not found.', ['@action' => $action_id]); + continue; + } + try { + $action->execute([$entity]); + } + catch (\Exception $e) { + $this->logger->error('Error executing action "@action" on entity "@entity": @message', [ + '@action' => $action->label(), + '@entity' => $entity->label(), + '@message' => $e->getMessage(), + ]); + } } } From 3902cce0ac7525aa209e56f198d5059f4c42e09a Mon Sep 17 00:00:00 2001 From: Rosie Le Faive Date: Wed, 1 May 2024 14:41:48 -0300 Subject: [PATCH 03/14] Allow filehash 3 (#1016) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index cb95bbc6e..79d5ef5a4 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "drupal/ctools": "^3.8 || ^4", "drupal/eva" : "^3.0", "drupal/file_replace": "^1.1", - "drupal/filehash": "^2", + "drupal/filehash": "^2 || ^3", "drupal/flysystem" : "^2.0@alpha", "drupal/jwt": "^1.1 || ^2", "drupal/migrate_plus" : "^5.1 || ^6", From 5e958a5e10715f67e5c6341efabaeaee5a66149f Mon Sep 17 00:00:00 2001 From: Rosie Le Faive Date: Sat, 11 May 2024 08:29:03 -0700 Subject: [PATCH 04/14] Allow application/xml in OCR Action. --- .../src/Plugin/Action/GenerateOCRDerivative.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/islandora_text_extraction/src/Plugin/Action/GenerateOCRDerivative.php b/modules/islandora_text_extraction/src/Plugin/Action/GenerateOCRDerivative.php index 272e9f01b..e60931a85 100644 --- a/modules/islandora_text_extraction/src/Plugin/Action/GenerateOCRDerivative.php +++ b/modules/islandora_text_extraction/src/Plugin/Action/GenerateOCRDerivative.php @@ -48,8 +48,9 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta */ public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { parent::validateConfigurationForm($form, $form_state); - $exploded_mime = explode('/', $form_state->getValue('mimetype')); - if ($exploded_mime[0] != 'text') { + $mime = $form_state->getValue('mimetype'); + $exploded_mime = explode('/', $mime); + if ($exploded_mime[0] != 'text' && $mime != 'application/xml') { $form_state->setErrorByName( 'mimetype', $this->t('Please enter file mimetype (e.g. text/plain.)') From da47bcfb081fc9f42fa547de124431c375a3f76c Mon Sep 17 00:00:00 2001 From: Akanksha Singh Date: Thu, 16 May 2024 12:46:26 -0300 Subject: [PATCH 05/14] Node has Parent context does not explicitly check if the field exists. (#1019) * Check that the field exists on node * Early exit with disabled check --- src/IslandoraContextManager.php | 2 +- src/Plugin/Condition/NodeHasParent.php | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/IslandoraContextManager.php b/src/IslandoraContextManager.php index 9fd93fbc2..ed6f74af9 100644 --- a/src/IslandoraContextManager.php +++ b/src/IslandoraContextManager.php @@ -37,7 +37,7 @@ public function evaluateContexts(array $provided = []) { } /** @var \Drupal\context\ContextInterface $context */ foreach ($this->getContexts() as $context) { - if ($this->evaluateContextConditions($context, $provided) && !$context->disabled()) { + if (!$context->disabled() && $this->evaluateContextConditions($context, $provided)) { $this->activeContexts[$context->id()] = $context; } } diff --git a/src/Plugin/Condition/NodeHasParent.php b/src/Plugin/Condition/NodeHasParent.php index f9ef0cbf5..813fd00b1 100644 --- a/src/Plugin/Condition/NodeHasParent.php +++ b/src/Plugin/Condition/NodeHasParent.php @@ -137,9 +137,11 @@ public function evaluate() { * TRUE if entity references the specified parent. */ protected function evaluateEntity(EntityInterface $entity) { + $parent_reference_field = $this->configuration['parent_reference_field']; foreach ($entity->referencedEntities() as $referenced_entity) { - if ($entity->getEntityTypeID() == 'node' && $referenced_entity->getEntityTypeId() == 'node') { - $parent_reference_field = $this->configuration['parent_reference_field']; + // Check whether the entity and the referenced entity are nodes. + // Also make sure that the field exists. + if ($entity->getEntityTypeID() == 'node' && $entity->hasField($parent_reference_field) && $referenced_entity->getEntityTypeId() == 'node') { $field = $entity->get($parent_reference_field); if (!$field->isEmpty()) { $nids = $field->getValue(); From b8f0b9c96601d952d69a7e067ac614568d78154c Mon Sep 17 00:00:00 2001 From: Rosie Le Faive Date: Thu, 16 May 2024 17:12:26 -0300 Subject: [PATCH 06/14] Allow NodeHasMediaUse view filter to work on views with node relationships. (#1010) * Adjust addition of where clause. * Bit of other paranoia with aliases. * Dependency injection in NodeHasMediaUse views filter. --------- Co-authored-by: Adam Vessey --- src/Plugin/views/filter/NodeHasMediaUse.php | 74 +++++++++++++++++---- 1 file changed, 62 insertions(+), 12 deletions(-) diff --git a/src/Plugin/views/filter/NodeHasMediaUse.php b/src/Plugin/views/filter/NodeHasMediaUse.php index 3c69bddd5..1686c4217 100644 --- a/src/Plugin/views/filter/NodeHasMediaUse.php +++ b/src/Plugin/views/filter/NodeHasMediaUse.php @@ -2,8 +2,13 @@ namespace Drupal\islandora\Plugin\views\filter; +use Drupal\Core\Database\Connection; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\islandora\IslandoraUtils; use Drupal\views\Plugin\views\filter\FilterPluginBase; +use Psr\Log\LoggerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Views Filter on Having Media of a Type. @@ -14,6 +19,48 @@ */ class NodeHasMediaUse extends FilterPluginBase { + /** + * Islandora's utility service. + * + * @var \Drupal\islandora\IslandoraUtils + */ + protected IslandoraUtils $utils; + + /** + * Drupal's entity type manager service. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected EntityTypeManagerInterface $entityTypeManager; + + /** + * Drupal's database connection service. + * + * @var \Drupal\Core\Database\Connection + */ + protected Connection $connection; + + /** + * Logger service. + * + * @var \Psr\Log\LoggerInterface + */ + protected LoggerInterface $logger; + + /** + * {@inheritDoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); + + $instance->utils = $container->get('islandora.utils'); + $instance->entityTypeManager = $container->get('entity_type.manager'); + $instance->connection = $container->get('database'); + $instance->logger = $container->get('logger.factory')->get('islanodra'); + + return $instance; + } + /** * {@inheritdoc} */ @@ -29,7 +76,7 @@ protected function defineOptions() { */ public function validateOptionsForm(&$form, FormStateInterface $form_state) { $uri = $form_state->getValues()['options']['use_uri']; - $term = \Drupal::service('islandora.utils')->getTermForUri($uri); + $term = $this->utils->getTermForUri($uri); if (empty($term)) { $form_state->setError($form['use_uri'], $this->t('Could not find term with URI: "%uri"', ['%uri' => $uri])); } @@ -39,7 +86,7 @@ public function validateOptionsForm(&$form, FormStateInterface $form_state) { * {@inheritdoc} */ public function buildOptionsForm(&$form, FormStateInterface $form_state) { - $terms = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadByProperties(['vid' => 'islandora_media_use']); + $terms = $this->entityTypeManager->getStorage('taxonomy_term')->loadByProperties(['vid' => 'islandora_media_use']); $uris = []; foreach ($terms as $term) { foreach ($term->get('field_external_uri')->getValue() as $uri) { @@ -67,7 +114,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { */ public function adminSummary() { $operator = ($this->options['negated']) ? "does not have" : "has"; - $term = \Drupal::service('islandora.utils')->getTermForUri($this->options['use_uri']); + $term = $this->utils->getTermForUri($this->options['use_uri']); $label = (empty($term)) ? 'BROKEN TERM URI' : $term->label(); return "Node {$operator} a '{$label}' media"; } @@ -77,18 +124,21 @@ public function adminSummary() { */ public function query() { $condition = ($this->options['negated']) ? 'NOT IN' : 'IN'; - $utils = \Drupal::service('islandora.utils'); - $term = $utils->getTermForUri($this->options['use_uri']); + $term = $this->utils->getTermForUri($this->options['use_uri']); if (empty($term)) { - \Drupal::logger('islandora')->warning('Node Has Media Filter could not find term with URI: "%uri"', ['%uri' => $this->options['use_uri']]); + $this->logger->warning('Node Has Media Filter could not find term with URI: "%uri"', ['%uri' => $this->options['use_uri']]); return; } - $sub_query = \Drupal::database()->select('media', 'm'); - $sub_query->join('media__field_media_use', 'use', 'm.mid = use.entity_id'); - $sub_query->join('media__field_media_of', 'of', 'm.mid = of.entity_id'); - $sub_query->fields('of', ['field_media_of_target_id']) - ->condition('use.field_media_use_target_id', $term->id()); - $this->query->addWhere(0, 'nid', $sub_query, $condition); + $sub_query = $this->connection->select('media', 'm'); + $use_alias = $sub_query->join('media__field_media_use', 'use', 'm.mid = %alias.entity_id'); + $of_alias = $sub_query->join('media__field_media_of', 'of', 'm.mid = %alias.entity_id'); + $sub_query->fields($of_alias, ['field_media_of_target_id']) + ->condition("{$use_alias}.field_media_use_target_id", $term->id()); + + /** @var \Drupal\views\Plugin\views\query\Sql $query */ + $query = $this->query; + $alias = $query->ensureTable('node_field_data', $this->relationship); + $query->addWhere(0, "{$alias}.nid", $sub_query, $condition); } } From 71d131a2ee9acbc0191669ba12abc6f81d6adde6 Mon Sep 17 00:00:00 2001 From: Alan Stanley Date: Thu, 30 May 2024 11:39:57 -0300 Subject: [PATCH 07/14] Deletion abstraction (#1017) * Change to Boolean logic * remove accidental newline * abstracted media and file deletion * abstracted media and file deletion * codered * fixed test * Update IslandoraUtils.php Fixed PHPDoc * Update IslandoraUtils.php Tweaked PHPDocs * Type hint * More complete parameter description --------- Co-authored-by: Rosie Le Faive --- islandora.module | 130 ++--------------------- islandora.services.yml | 2 +- src/Form/ConfirmDeleteMediaAndFile.php | 118 +++----------------- src/IslandoraUtils.php | 94 ++++++++++++++-- tests/src/Functional/DeleteMediaTest.php | 2 +- 5 files changed, 117 insertions(+), 229 deletions(-) diff --git a/islandora.module b/islandora.module index d715db52d..8fa478a7f 100644 --- a/islandora.module +++ b/islandora.module @@ -16,19 +16,18 @@ use Drupal\Component\Plugin\Exception\PluginNotFoundException; use Drupal\Core\Entity\Display\EntityViewDisplayInterface; +use Drupal\Core\Entity\EntityForm; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Site\Settings; use Drupal\Core\Url; +use Drupal\file\FileInterface; use Drupal\islandora\Form\IslandoraSettingsForm; -use Drupal\node\NodeInterface; use Drupal\media\MediaInterface; -use Drupal\file\FileInterface; -use Drupal\taxonomy\TermInterface; -use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\node\NodeInterface; use Drupal\serialization\Normalizer\CacheableNormalizerInterface; -use Drupal\Core\Entity\EntityForm; -use Drupal\file\Entity\File; +use Drupal\taxonomy\TermInterface; /** * Implements hook_help(). @@ -408,132 +407,25 @@ function islandora_media_custom_form_submit(&$form, FormStateInterface $form_sta * Implements a submit handler for the delete form. */ function islandora_object_delete_form_submit($form, FormStateInterface $form_state) { - $result = $form_state->getValues('delete_associated_content'); - $utils = \Drupal::service('islandora.utils'); - if ($result['delete_associated_content'] == 1) { - + $utils = \Drupal::service('islandora.utils'); $node = $form_state->getFormObject()->getEntity(); $medias = $utils->getMedia($node); - $media_list = []; - - $entity_field_manager = \Drupal::service('entity_field.manager'); - $current_user = \Drupal::currentUser(); + $results = $utils->deleteMediaAndFiles($medias); $logger = \Drupal::logger('logger.channel.islandora'); $messenger = \Drupal::messenger(); - - $delete_media = []; - $media_translations = []; - $media_files = []; - $entity_protected_medias = []; - $inaccessible_entities = []; - - foreach ($medias as $id => $media) { - $lang = $media->language()->getId(); - $selected_langcodes[$lang] = $lang; - - if (!$media->access('delete', $current_user)) { - $inaccessible_entities[] = $media; - continue; - } - // Check for files. - $fields = $entity_field_manager->getFieldDefinitions('media', $media->bundle()); - foreach ($fields as $field) { - $type = $field->getType(); - if ($type == 'file' || $type == 'image') { - $target_id = $media->get($field->getName())->target_id; - $file = File::load($target_id); - if ($file) { - if (!$file->access('delete', $current_user)) { - $inaccessible_entities[] = $file; - continue; - } - $media_files[$id][$file->id()] = $file; - } - } - } - - foreach ($selected_langcodes as $langcode) { - // We're only working with media, which are translatable. - $entity = $media->getTranslation($langcode); - if ($entity->isDefaultTranslation()) { - $delete_media[$id] = $entity; - unset($media_translations[$id]); - } - elseif (!isset($delete_media[$id])) { - $media_translations[$id][] = $entity; - } - } + if (isset($results['inaccessible'])) { + $messenger->addWarning($results['inaccessible']); } - - if ($delete_media) { - foreach ($delete_media as $id => $media) { - try { - $media->delete(); - $media_list[] = $id; - $logger->notice('The media %label has been deleted.', [ - '%label' => $media->label(), - ]); - } - catch (Exception $e) { - $entity_protected_medias[] = $id; - } - } - } - - $delete_files = array_filter($media_files, function ($media) use ($entity_protected_medias) { - return !in_array($media, $entity_protected_medias); - }, ARRAY_FILTER_USE_KEY); - - if ($delete_files) { - foreach ($delete_files as $files_array) { - foreach ($files_array as $file) { - $file->delete(); - $logger->notice('The file %label has been deleted.', [ - '%label' => $file->label(), - ]); - } - } - } - - $delete_media_translations = array_filter($media_translations, function ($media) use ($entity_protected_medias) { - return !in_array($media, $entity_protected_medias); - }, ARRAY_FILTER_USE_KEY); - - if ($delete_media_translations) { - foreach ($delete_media_translations as $id => $translations) { - $media = $medias[$id]; - foreach ($translations as $translation) { - $media->removeTranslation($translation->language()->getId()); - } - $media->save(); - foreach ($translations as $translation) { - $logger->notice('The media %label @language translation has been deleted', [ - '%label' => $media->label(), - '@language' => $translation->language()->getName(), - ]); - } - } - } - - if ($inaccessible_entities) { - $messenger->addWarning("@count items have not been deleted because you do not have the necessary permissions.", [ - '@count' => count($inaccessible_entities), - ]); - } - + $logger->notice($results['deleted']); $build = [ 'heading' => [ '#type' => 'html_tag', '#tag' => 'div', '#value' => t("The repository item @node and @media", [ '@node' => $node->getTitle(), - '@media' => \Drupal::translation()->formatPlural( - count($media_list), 'the media with the id @media has been deleted.', - 'the medias with the ids @media have been deleted.', - ['@media' => implode(", ", $media_list)], - ), + '@media' => $results['deleted'], ]), ], ]; diff --git a/islandora.services.yml b/islandora.services.yml index 74725d803..36ef7dac3 100644 --- a/islandora.services.yml +++ b/islandora.services.yml @@ -54,7 +54,7 @@ services: arguments: ['@entity_type.manager', '@current_user', '@language_manager', '@file_system', '@islandora.utils'] islandora.utils: class: Drupal\islandora\IslandoraUtils - arguments: ['@entity_type.manager', '@entity_field.manager', '@context.manager', '@flysystem_factory', '@language_manager'] + arguments: ['@entity_type.manager', '@entity_field.manager', '@context.manager', '@flysystem_factory', '@language_manager', '@current_user'] islandora.entity_mapper: class: Islandora\EntityMapper\EntityMapper islandora.stomp.auth_header_listener: diff --git a/src/Form/ConfirmDeleteMediaAndFile.php b/src/Form/ConfirmDeleteMediaAndFile.php index a9613871f..90e61878a 100644 --- a/src/Form/ConfirmDeleteMediaAndFile.php +++ b/src/Form/ConfirmDeleteMediaAndFile.php @@ -2,7 +2,6 @@ namespace Drupal\islandora\Form; -use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\Form\DeleteMultipleForm; use Drupal\Core\Form\FormStateInterface; @@ -10,8 +9,7 @@ use Drupal\Core\Session\AccountInterface; use Drupal\Core\TempStore\PrivateTempStoreFactory; use Drupal\Core\Url; -use Drupal\file\Entity\File; -use Drupal\islandora\MediaSource\MediaSourceService; +use Drupal\islandora\IslandoraUtils; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -21,11 +19,11 @@ class ConfirmDeleteMediaAndFile extends DeleteMultipleForm { /** - * Media source service. + * The current user. * - * @var \Drupal\islandora\MediaSource\MediaSourceService + * @var \Drupal\Core\Session\AccountInterface */ - protected $mediaSourceService; + protected $currentUser; /** * Logger. @@ -42,23 +40,22 @@ class ConfirmDeleteMediaAndFile extends DeleteMultipleForm { protected $selection = []; /** - * Entity field manager. + * The Islandora Utils service. * - * @var \Drupal\Core\Entity\EntityFieldManagerInterface + * @var \Drupal\islandora\IslandoraUtils */ - protected $entityFieldManager; + protected IslandoraUtils $utils; /** * {@inheritdoc} */ - public function __construct(AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, PrivateTempStoreFactory $temp_store_factory, MessengerInterface $messenger, MediaSourceService $media_source_service, LoggerInterface $logger) { + public function __construct(AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, PrivateTempStoreFactory $temp_store_factory, MessengerInterface $messenger, LoggerInterface $logger, IslandoraUtils $utils) { $this->currentUser = $current_user; $this->entityTypeManager = $entity_type_manager; - $this->entityFieldManager = $entity_field_manager; $this->tempStore = $temp_store_factory->get('media_and_file_delete_confirm'); $this->messenger = $messenger; - $this->mediaSourceService = $media_source_service; $this->logger = $logger; + $this->utils = $utils; } /** @@ -68,11 +65,11 @@ public static function create(ContainerInterface $container) { return new static( $container->get('current_user'), $container->get('entity_type.manager'), - $container->get('entity_field.manager'), $container->get('tempstore.private'), $container->get('messenger'), - $container->get('islandora.media_source_service'), - $container->get('logger.channel.islandora')); + $container->get('logger.channel.islandora'), + $container->get('islandora.utils') + ); } /** @@ -111,94 +108,13 @@ public function buildForm(array $form, FormStateInterface $form_state, $entity_t public function submitForm(array &$form, FormStateInterface $form_state) { // Similar to parent::submitForm(), but let's blend in the related files and // optimize based on the fact that we know we're working with media. - $total_count = 0; - $delete_media = []; - $delete_media_translations = []; - $delete_files = []; - $inaccessible_entities = []; $media_storage = $this->entityTypeManager->getStorage('media'); - $file_storage = $this->entityTypeManager->getStorage('file'); $media = $media_storage->loadMultiple(array_keys($this->selection)); - foreach ($this->selection as $id => $selected_langcodes) { - $entity = $media[$id]; - if (!$entity->access('delete', $this->currentUser)) { - $inaccessible_entities[] = $entity; - continue; - } - // Check for files. - $fields = $this->entityFieldManager->getFieldDefinitions('media', $entity->bundle()); - foreach ($fields as $field) { - if ($field->getName() == 'thumbnail') { - continue; - } - $type = $field->getType(); - if ($type == 'file' || $type == 'image') { - $target_id = $entity->get($field->getName())->target_id; - $file = File::load($target_id); - if ($file) { - if (!$file->access('delete', $this->currentUser)) { - $inaccessible_entities[] = $file; - continue; - } - if (!array_key_exists($file->id(), $delete_files)) { - $delete_files[$file->id()] = $file; - $total_count++; - } - - } - } - } - - foreach ($selected_langcodes as $langcode) { - // We're only working with media, which are translatable. - $entity = $entity->getTranslation($langcode); - if ($entity->isDefaultTranslation()) { - $delete_media[$id] = $entity; - unset($delete_media_translations[$id]); - $total_count += count($entity->getTranslationLanguages()); - } - elseif (!isset($delete_media[$id])) { - $delete_media_translations[$id][] = $entity; - } - } - } - if ($delete_media) { - $media_storage->delete($delete_media); - foreach ($delete_media as $entity) { - $this->logger->notice('The media %label has been deleted.', [ - '%label' => $entity->label(), - ]); - } - } - if ($delete_files) { - $file_storage->delete($delete_files); - foreach ($delete_files as $entity) { - $this->logger->notice('The file %label has been deleted.', [ - '%label' => $entity->label(), - ]); - } - } - if ($delete_media_translations) { - foreach ($delete_media_translations as $id => $translations) { - $entity = $media[$id]; - foreach ($translations as $translation) { - $entity->removeTranslation($translation->language()->getId()); - } - $entity->save(); - foreach ($translations as $translation) { - $this->logger->notice('The media %label @language translation has been deleted', [ - '%label' => $entity->label(), - '@language' => $translation->language()->getName(), - ]); - } - $total_count += count($translations); - } - } - if ($total_count) { - $this->messenger->addStatus($this->getDeletedMessage($total_count)); - } - if ($inaccessible_entities) { - $this->messenger->addWarning($this->getInaccessibleMessage(count($inaccessible_entities))); + $results = $this->utils->deleteMediaAndFiles($media); + $this->logger->notice($results['deleted']); + $this->messenger->addStatus($results['deleted']); + if (isset($results['inaccessible'])) { + $this->messenger->addWarning($results['inaccessible']); } $this->tempStore->delete($this->currentUser->id()); $form_state->setRedirectUrl($this->getCancelUrl()); diff --git a/src/IslandoraUtils.php b/src/IslandoraUtils.php index a2df75896..5d667fc42 100644 --- a/src/IslandoraUtils.php +++ b/src/IslandoraUtils.php @@ -10,7 +10,9 @@ use Drupal\Core\Entity\Query\QueryException; use Drupal\Core\Entity\Query\QueryInterface; use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Session\AccountInterface; use Drupal\Core\Site\Settings; +use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; use Drupal\file\FileInterface; use Drupal\flysystem\FlysystemFactory; @@ -26,13 +28,15 @@ * Utility functions for figuring out when to fire derivative reactions. */ class IslandoraUtils { - + use StringTranslationTrait; const EXTERNAL_URI_FIELD = 'field_external_uri'; const MEDIA_OF_FIELD = 'field_media_of'; const MEDIA_USAGE_FIELD = 'field_media_use'; + const MEMBER_OF_FIELD = 'field_member_of'; + const MODEL_FIELD = 'field_model'; /** @@ -68,7 +72,14 @@ class IslandoraUtils { * * @var \Drupal\Core\Language\LanguageManagerInterface */ - protected $languageManager; + protected LanguageManagerInterface $languageManager; + + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected AccountInterface $currentUser; /** * Constructor. @@ -83,19 +94,23 @@ class IslandoraUtils { * Flysystem factory. * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager * Language manager. + * @param \Drupal\Core\Session\AccountInterface $current_user + * The current user. */ public function __construct( EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, ContextManager $context_manager, FlysystemFactory $flysystem_factory, - LanguageManagerInterface $language_manager + LanguageManagerInterface $language_manager, + AccountInterface $current_user ) { $this->entityTypeManager = $entity_type_manager; $this->entityFieldManager = $entity_field_manager; $this->contextManager = $context_manager; $this->flysystemFactory = $flysystem_factory; $this->languageManager = $language_manager; + $this->currentUser = $current_user; } /** @@ -423,7 +438,6 @@ public function executeDerivativeReactions($reaction_type, NodeInterface $node, * TRUE if the fields have changed. */ public function haveFieldsChanged(ContentEntityInterface $entity, ContentEntityInterface $original) { - $field_definitions = $this->entityFieldManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()); $ignore_list = ['vid' => 1, 'changed' => 1, 'path' => 1]; @@ -528,7 +542,8 @@ public function getMediaReferencingNodeAndTerm(NodeInterface $node, TermInterfac * Array of fields. */ public function getReferencingFields($entity_type, $target_type) { - $fields = $this->entityTypeManager->getStorage('field_storage_config')->getQuery() + $fields = $this->entityTypeManager->getStorage('field_storage_config') + ->getQuery() ->condition('entity_type', $entity_type) ->condition('settings.target_type', $target_type) ->execute(); @@ -657,8 +672,6 @@ public function isIslandoraType($entity_type, $bundle) { public function canCreateIslandoraEntity($entity_type, $bundle_type) { $bundles = $this->entityTypeManager->getStorage($bundle_type)->loadMultiple(); $access_control_handler = $this->entityTypeManager->getAccessControlHandler($entity_type); - - $allowed = []; foreach (array_keys($bundles) as $bundle) { // Skip bundles that aren't 'Islandora' types. if (!$this->isIslandoraType($entity_type, $bundle)) { @@ -755,4 +768,71 @@ protected function getParentsByEntityReference(ContentEntityInterface $entity, a return $parents; } + /** + * Deletes Media and all associated files. + * + * @param \Drupal\media\MediaInterface[] $media + * Array of media objects to be deleted along with their files. + * + * @return array + * Associative array keyed 'deleted' and 'inaccessible'. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + * @throws \Drupal\Core\Entity\EntityStorageException + */ + public function deleteMediaAndFiles(array $media) { + $results = []; + $delete_media = []; + $delete_files = []; + $inaccessible_entities = []; + $media_storage = $this->entityTypeManager->getStorage('media'); + $file_storage = $this->entityTypeManager->getStorage('file'); + foreach ($media as $entity) { + if (!$entity->access('delete', $this->currentUser)) { + $inaccessible_entities[] = $entity; + continue; + } + else { + $delete_media[$entity->id()] = $entity; + } + // Check for source and additional files. + $fields = $this->entityFieldManager->getFieldDefinitions('media', $entity->bundle()); + foreach ($fields as $field) { + if ($field->getName() == 'thumbnail') { + continue; + } + $type = $field->getType(); + if ($type == 'file' || $type == 'image') { + $target_id = $entity->get($field->getName())->target_id; + $file = $file_storage->load($target_id); + if ($file) { + if (!$file->access('delete', $this->currentUser)) { + $inaccessible_entities[] = $file; + continue; + } + if (!array_key_exists($file->id(), $delete_files)) { + $delete_files[$file->id()] = $file; + } + } + } + } + } + if ($delete_media) { + $media_storage->delete($delete_media); + } + if ($delete_files) { + $file_storage->delete($delete_files); + } + $results['deleted'] = $this->formatPlural( + count($delete_media), 'The media with the id @media has been deleted.', + 'The medias with the ids @media have been deleted.', + ['@media' => implode(", ", array_keys($delete_media))], + ); + if ($inaccessible_entities) { + $results['inaccessible'] = $this->formatPlural($inaccessible_entities, "@count item has not been deleted because you do not have the necessary permissions.", "@count items have not been deleted because you do not have the necessary permissions."); + } + return $results; + } + } diff --git a/tests/src/Functional/DeleteMediaTest.php b/tests/src/Functional/DeleteMediaTest.php index c52eca31f..957146fdc 100644 --- a/tests/src/Functional/DeleteMediaTest.php +++ b/tests/src/Functional/DeleteMediaTest.php @@ -93,7 +93,7 @@ public function testDeleteMediaAndFile() { $this->assertSession()->pageTextContains('Are you sure you want to delete this media and associated files?'); $page->pressButton('Delete'); // Should assert that a media and file were deleted. - $this->assertSession()->pageTextContains('Deleted 2 items.'); + $this->assertSession()->pageTextContains("The media with the id $mid has been deleted"); // Attempt to reload the entities. // Both media and file should be gone. From 780cf6efb93aa8e1c4257a4d921f5303795b19e9 Mon Sep 17 00:00:00 2001 From: Adam <607975+adam-vessey@users.noreply.github.com> Date: Mon, 3 Jun 2024 17:38:10 -0300 Subject: [PATCH 08/14] Only generate tokens for our Islandora-esque nodes. (#1025) --- islandora.tokens.inc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/islandora.tokens.inc b/islandora.tokens.inc index f528dee3d..d2c424c53 100644 --- a/islandora.tokens.inc +++ b/islandora.tokens.inc @@ -80,6 +80,9 @@ function islandora_tokens($type, $tokens, array $data, array $options, Bubbleabl return; } $islandoraUtils = \Drupal::service('islandora.utils'); + if (!$islandoraUtils->isIslandoraType('node', $data['node']->bundle())) { + return; + } foreach ($tokens as $name => $original) { switch ($name) { case 'media-original-file:basename': From 3f42453cd478e572473b37c0634e022bec4c0b3c Mon Sep 17 00:00:00 2001 From: Rosie Le Faive Date: Thu, 6 Jun 2024 08:48:33 -0300 Subject: [PATCH 09/14] Require Jsonld at the drupal namespace. (#1023) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 79d5ef5a4..9a3c693e8 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "drupal/file_replace": "^1.1", "drupal/filehash": "^2 || ^3", "drupal/flysystem" : "^2.0@alpha", + "drupal/jsonld": "^2 || ^3", "drupal/jwt": "^1.1 || ^2", "drupal/migrate_plus" : "^5.1 || ^6", "drupal/migrate_source_csv" : "^3.4", @@ -28,7 +29,6 @@ "drupal/token" : "^1.3", "islandora/chullo": "^2.0", "islandora/fedora-entity-mapper": "^1.0", - "islandora/jsonld": "^2 || ^3", "stomp-php/stomp-php": "4.* || ^5" }, "require-dev": { From e36ad1701cb5742d4b17e26de2bea957e1579728 Mon Sep 17 00:00:00 2001 From: Adam <607975+adam-vessey@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:43:02 -0300 Subject: [PATCH 10/14] Fix use of batch methods from decorated services. (#1027) --- .../AbstractFileSelectionForm.php | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/Form/AddChildrenWizard/AbstractFileSelectionForm.php b/src/Form/AddChildrenWizard/AbstractFileSelectionForm.php index 6aeed8795..445c251e9 100644 --- a/src/Form/AddChildrenWizard/AbstractFileSelectionForm.php +++ b/src/Form/AddChildrenWizard/AbstractFileSelectionForm.php @@ -142,16 +142,46 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $builder = (new BatchBuilder()) ->setTitle($this->t('Bulk creating...')) ->setInitMessage($this->t('Initializing...')) - ->setFinishCallback([$this->batchProcessor, 'batchProcessFinished']); + ->setFinishCallback([$this, 'batchProcessFinished']); $values = $form_state->getValue($this->getField($cached_values)->getName()); $massaged_values = $widget->massageFormValues($values, $form, $form_state); foreach ($massaged_values as $delta => $info) { $builder->addOperation( - [$this->batchProcessor, 'batchOperation'], + [$this, 'batchOperation'], [$delta, $info, $cached_values] ); } batch_set($builder->toArray()); } + /** + * Wrap batch processor operation call to side-step serialization issues. + * + * Previously, we referred to the method on the processors directly; however, + * this can lead to issues regarding the (un)serialization of the services as + * which the processors are implemented. For example, if decorating one of the + * processors to extend it, it loses the reference back to be able to load the + * "inner"/decorated processor. + * + * @see \Drupal\islandora\Form\AddChildrenWizard\AbstractBatchProcessor::batchOperation() + */ + public function batchOperation($delta, $info, $cached_values, &$context) : void { + $this->batchProcessor->batchOperation($delta, $info, $cached_values, $context); + } + + /** + * Wrap batch processor finished call to side-step serialization issues. + * + * Previously, we referred to the method on the processors directly; however, + * this can lead to issues regarding the (un)serialization of the services as + * which the processors are implemented. For example, if decorating one of the + * processors to extend it, it loses the reference back to be able to load the + * "inner"/decorated processor. + * + * @see \Drupal\islandora\Form\AddChildrenWizard\AbstractBatchProcessor::batchProcessFinished() + */ + public function batchProcessFinished($success, $results, $operations) : void { + $this->batchProcessor->batchProcessFinished($success, $results, $operations); + } + } From 849b12af4f6025db33860da3af2583fc625bb0b3 Mon Sep 17 00:00:00 2001 From: Rosie Le Faive Date: Wed, 26 Jun 2024 18:24:54 -0300 Subject: [PATCH 11/14] D10.3 (#1032) * Deprecate 10.1, add 10.4.x-dev. * Correct permission list. --- .github/workflows/build-2.x.yml | 6 +----- islandora.info.yml | 2 +- tests/src/Functional/DeleteMediaTest.php | 7 +------ tests/src/Functional/JsonldTypeAlterReactionTest.php | 10 +--------- 4 files changed, 4 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build-2.x.yml b/.github/workflows/build-2.x.yml index 8cd9f1d1e..8677ddf4e 100644 --- a/.github/workflows/build-2.x.yml +++ b/.github/workflows/build-2.x.yml @@ -22,13 +22,9 @@ jobs: matrix: php-versions: ["8.1", "8.2", "8.3"] test-suite: ["kernel", "functional", "functional-javascript"] - drupal-version: ["10.1.x", "10.2.x", "10.3.x-dev"] + drupal-version: ["10.2.x", "10.3.x", "10.4.x-dev"] mysql: ["8.0"] allowed_failure: [false] - exclude: - - php-versions: "8.3" - drupal-version: "10.1.x" - name: PHP ${{ matrix.php-versions }} | drupal ${{ matrix.drupal-version }} | mysql ${{ matrix.mysql }} | test-suite ${{ matrix.test-suite }} diff --git a/islandora.info.yml b/islandora.info.yml index 6dd0597f2..a43196a42 100644 --- a/islandora.info.yml +++ b/islandora.info.yml @@ -4,7 +4,7 @@ name: 'islandora' description: "Islandora Core" type: module package: Islandora -core_version_requirement: ^9 || ^10 +core_version_requirement: ^10.2 dependencies: - context:context_ui - ctools:ctools diff --git a/tests/src/Functional/DeleteMediaTest.php b/tests/src/Functional/DeleteMediaTest.php index 957146fdc..76298501a 100644 --- a/tests/src/Functional/DeleteMediaTest.php +++ b/tests/src/Functional/DeleteMediaTest.php @@ -50,12 +50,7 @@ class DeleteMediaTest extends IslandoraFunctionalTestBase { public function setUp(): void { parent::setUp(); - if (version_compare(\Drupal::VERSION, '10.1', '>=')) { - $permissions = ['create media', 'delete any media', 'delete any file']; - } - else { - $permissions = ['create media', 'delete any media']; - } + $permissions = ['create media', 'delete any media', 'delete any file']; // Create a test user. $this->account = $this->createUser($permissions); diff --git a/tests/src/Functional/JsonldTypeAlterReactionTest.php b/tests/src/Functional/JsonldTypeAlterReactionTest.php index 6ac70d7da..7e2005842 100644 --- a/tests/src/Functional/JsonldTypeAlterReactionTest.php +++ b/tests/src/Functional/JsonldTypeAlterReactionTest.php @@ -26,15 +26,7 @@ public function testMappingReaction() { // Add the typed predicate we will select in the reaction config. // Taken from FieldUiTestTrait->fieldUIAddNewField. - if (version_compare(\Drupal::VERSION, '10.2.x-dev', 'lt')) { - $this->submitForm([ - 'new_storage_type' => 'string', - 'label' => 'Typed Predicate', - 'field_name' => 'type_predicate', - ], 'Save and continue'); - $this->submitForm([], 'Save field settings'); - } - elseif (version_compare(\Drupal::VERSION, '10.3.x-dev', 'lt')) { + if (version_compare(\Drupal::VERSION, '10.3.x-dev', 'lt')) { $this->getSession()->getPage()->selectFieldOption('new_storage_type', 'plain_text'); // For Drupal 10.2, we first need to submit the form with the elements // displayed on initial page load. The form is using AJAX to send a From 010534349c0b4c2d72762f3268cccaa75e9ea0ca Mon Sep 17 00:00:00 2001 From: Alan Stanley Date: Thu, 27 Jun 2024 16:13:00 -0300 Subject: [PATCH 12/14] added check (#1031) * added check * blank line * cleaner implementation * check added * tidied up --- islandora.module | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/islandora.module b/islandora.module index 8fa478a7f..153eb1679 100644 --- a/islandora.module +++ b/islandora.module @@ -545,12 +545,11 @@ function islandora_entity_view(array &$build, EntityInterface $entity, EntityVie $route_match_item = \Drupal::routeMatch()->getParameters()->get($entity->getEntityTypeId()); // Ensure the entity matches the route. if ($entity === $route_match_item) { - if ($display->getComponent('field_gemini_uri')) { - $mapper = \Drupal::service('islandora.entity_mapper'); - $flysystem_config = Settings::get('flysystem'); - $fedora_root = $flysystem_config['fedora']['config']['root']; + $flysystem_config = Settings::get('flysystem'); + $fedora_root = $flysystem_config['fedora']['config']['root'] ?? NULL; + if ($display->getComponent('field_gemini_uri') && $fedora_root) { $fedora_root = rtrim($fedora_root, '/'); - + $mapper = \Drupal::service('islandora.entity_mapper'); if ($entity->getEntityTypeId() == 'media') { // Check if the source file is in Fedora or not. $media_source_service = \Drupal::service('islandora.media_source_service'); From 14c4e423237de41e982c27fbec28e5293476b726 Mon Sep 17 00:00:00 2001 From: Rosie Le Faive Date: Wed, 3 Jul 2024 21:02:24 -0300 Subject: [PATCH 13/14] Option to get alt text from Original File. (#1024) * Option to get alt text from Original File. * Add schema. * Logic re-work and fixing a undefined method. --------- Co-authored-by: Jordan Dukart --- config/schema/islandora.schema.yml | 4 + .../IslandoraImageFormatter.php | 111 +++++++++++++++--- 2 files changed, 101 insertions(+), 14 deletions(-) diff --git a/config/schema/islandora.schema.yml b/config/schema/islandora.schema.yml index 49de998b3..282f7b3c5 100644 --- a/config/schema/islandora.schema.yml +++ b/config/schema/islandora.schema.yml @@ -173,6 +173,10 @@ condition.plugin.node_had_namespace: field.formatter.settings.islandora_image: type: field.formatter.settings.image label: 'Islandora image field display format settings' + mapping: + image_alt_text: + type: string + label: "Alt text source" condition.plugin.islandora_entity_bundle: type: condition.plugin diff --git a/src/Plugin/Field/FieldFormatter/IslandoraImageFormatter.php b/src/Plugin/Field/FieldFormatter/IslandoraImageFormatter.php index deb6c7690..ffe6d47a2 100644 --- a/src/Plugin/Field/FieldFormatter/IslandoraImageFormatter.php +++ b/src/Plugin/Field/FieldFormatter/IslandoraImageFormatter.php @@ -6,9 +6,11 @@ use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\File\FileUrlGeneratorInterface; +use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountInterface; use Drupal\image\Plugin\Field\FieldFormatter\ImageFormatter; use Drupal\islandora\IslandoraUtils; +use Drupal\islandora\MediaSource\MediaSourceService; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -34,6 +36,13 @@ class IslandoraImageFormatter extends ImageFormatter { */ protected $utils; + /** + * Islandora media source service. + * + * @var \Drupal\islandora\MediaSource\MediaSourceService + */ + protected $mediaSourceService; + /** * Constructs an IslandoraImageFormatter object. * @@ -59,6 +68,8 @@ class IslandoraImageFormatter extends ImageFormatter { * Islandora utils. * @param \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator * The File URL Generator. + * @param \Drupal\islandora\MediaSource\MediaSourceService $media_source_service + * Utils to get the source file from media. */ public function __construct( $plugin_id, @@ -71,7 +82,8 @@ public function __construct( AccountInterface $current_user, EntityStorageInterface $image_style_storage, IslandoraUtils $utils, - FileUrlGeneratorInterface $file_url_generator + FileUrlGeneratorInterface $file_url_generator, + MediaSourceService $media_source_service ) { parent::__construct( $plugin_id, @@ -86,6 +98,7 @@ public function __construct( $file_url_generator ); $this->utils = $utils; + $this->mediaSourceService = $media_source_service; } /** @@ -103,10 +116,40 @@ public static function create(ContainerInterface $container, array $configuratio $container->get('current_user'), $container->get('entity_type.manager')->getStorage('image_style'), $container->get('islandora.utils'), - $container->get('file_url_generator') + $container->get('file_url_generator'), + $container->get('islandora.media_source_service') ); } + /** + * {@inheritdoc} + */ + public static function defaultSettings() { + return [ + 'image_alt_text' => 'local', + ] + parent::defaultSettings(); + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + $element = parent::settingsForm($form, $form_state); + $alt_text_options = [ + 'local' => $this->t('Local'), + 'original_file_fallback' => $this->t('Local, with fallback to Original File'), + 'original_file' => $this->t('Original File'), + ]; + $element['image_alt_text'] = [ + '#title' => $this->t('Alt text source'), + '#type' => 'select', + '#default_value' => $this->getSetting('image_alt_text'), + '#empty_option' => $this->t('None'), + '#options' => $alt_text_options, + ]; + return $element; + } + /** * {@inheritdoc} */ @@ -114,28 +157,68 @@ public function viewElements(FieldItemListInterface $items, $langcode) { $elements = parent::viewElements($items, $langcode); $image_link_setting = $this->getSetting('image_link'); - // Check if the formatter involves a link. - if ($image_link_setting != 'content') { + $alt_text_setting = $this->getsetting('image_alt_text'); + + // Check if we can leave the image as-is: + if ($image_link_setting !== 'content' && $alt_text_setting === 'local') { return $elements; } - $entity = $items->getEntity(); - if ($entity->isNew() || $entity->getEntityTypeId() != 'media') { + if ($entity->isNew() || $entity->getEntityTypeId() !== 'media') { return $elements; } - $node = $this->utils->getParentNode($entity); - - if ($node === NULL) { - return $elements; + if ($alt_text_setting === 'none') { + foreach ($elements as $element) { + $element['#item']->set('alt', ''); + } } - $url = $node->toUrl(); + if ($image_link_setting === 'content' || $alt_text_setting === 'original_file' || $alt_text_setting === 'original_file_fallback') { + $node = $this->utils->getParentNode($entity); + if ($node === NULL) { + return $elements; + } - foreach ($elements as &$element) { - $element['#url'] = $url; - } + if ($image_link_setting === 'content') { + // Set image link. + $url = $node->toUrl(); + foreach ($elements as &$element) { + $element['#url'] = $url; + } + unset($element); + } + + if ($alt_text_setting === 'original_file' || $alt_text_setting === 'original_file_fallback') { + $original_file_term = $this->utils->getTermForUri("http://pcdm.org/use#OriginalFile"); + if ($original_file_term !== NULL) { + $original_file_media = $this->utils->getMediaWithTerm($node, $original_file_term); + + if ($original_file_media !== NULL) { + $source_field_name = $this->mediaSourceService->getSourceFieldName($original_file_media->bundle()); + if ($original_file_media->hasField($source_field_name)) { + $original_file_files = $original_file_media->get($source_field_name); + // XXX: Support the multifile media use case where there could + // be multiple files in the source field. + $i = 0; + foreach ($original_file_files as $file) { + if (isset($file->alt)) { + $alt_text = $file->get('alt')->getValue(); + if (isset($elements[$i])) { + $element = $elements[$i]; + if ($alt_text_setting === 'original_file' || $element['#item']->get('alt')->getValue() === '') { + $elements[$i]['#item']->set('alt', $alt_text); + } + $i++; + } + } + } + } + } + } + } + } return $elements; } From 60a1678cac4a3a01987b67fc0f1bfcc7849d55c5 Mon Sep 17 00:00:00 2001 From: Noel Chiasson <53783039+nchiasson-dgi@users.noreply.github.com> Date: Sun, 7 Jul 2024 19:48:53 -0300 Subject: [PATCH 14/14] Shortcircuiting if search_endpoint isn't set (#1035) --- .../src/Plugin/views/style/IIIFManifest.php | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php index f0aca47da..42984eba8 100644 --- a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php +++ b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php @@ -494,17 +494,20 @@ protected function defineOptions() { */ protected function addSearchEndpoint(array &$json, array $url_components) { $url_base = $this->getRequest()->getSchemeAndHttpHost(); - $hocr_search_path = $this->options['search_endpoint']; - $hocr_search_url = $url_base . '/' . ltrim($hocr_search_path, '/'); + $hocr_search_path = $this->options['search_endpoint'] ?? null; - $hocr_search_url = str_replace('%node', $url_components[1], $hocr_search_url); + if ($hocr_search_path) { + $hocr_search_url = $url_base . '/' . ltrim($hocr_search_path, '/'); - $json['service'][] = [ - "@context" => "http://iiif.io/api/search/0/context.json", - "@id" => $hocr_search_url, - "profile" => "http://iiif.io/api/search/0/search", - "label" => t("Search inside this work"), - ]; + $hocr_search_url = str_replace('%node', $url_components[1], $hocr_search_url); + + $json['service'][] = [ + "@context" => "http://iiif.io/api/search/0/context.json", + "@id" => $hocr_search_url, + "profile" => "http://iiif.io/api/search/0/search", + "label" => t("Search inside this work"), + ]; + } } /**