diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fc260e6..7e78419 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,11 +40,11 @@ jobs: echo "GIT_BASE=${GITHUB_BASE_REF}" >> $GITHUB_ENV echo "GIT_BRANCH=${GITHUB_HEAD_REF}" >> $GITHUB_ENV echo "GIT_REPO=${{ github.event.pull_request.head.repo.full_name }}" >> $GITHUB_ENV - + - name: Set composer branch reference for main version branches if: endsWith(github.ref, '.x') run: echo "COMPOSER_REF=${GIT_BRANCH}-dev" >> $GITHUB_ENV - + - name: Set composer branch reference for main version branches if: endsWith(github.ref, '.x') == false run: echo "COMPOSER_REF=dev-${GIT_BRANCH}" >> $GITHUB_ENV @@ -188,9 +188,13 @@ jobs: run: docker-compose -f docker-compose.yml up -d - name: Run PHPUnit tests + env: + LOCALGOV_VERSION: ${{ matrix.localgov-version }} + RANDOMIZE_TESTS: "" run: | + [ "${LOCALGOV_VERSION}" != "1.x" ] && RANDOMIZE_TESTS="--order-by=random" mkdir -p ./html/web/sites/simpletest && chmod 777 ./html/web/sites/simpletest sed -i "s#http://localgov.lndo.site#http://drupal#; s#mysql://database:database@database/database#sqlite://localhost//dev/shm/test.sqlite#" ./html/phpunit.xml.dist docker exec -t drupal bash -c 'chown docker:docker -R /var/www/html' # docker exec -u docker -t drupal bash -c "cd /var/www/html && ./bin/phpunit --testdox" - docker exec -u docker -t drupal bash -c "cd /var/www/html && ./bin/paratest --processes=4" + docker exec -u docker -t drupal bash -c "cd /var/www/html && ./bin/paratest --processes=4 ${RANDOMIZE_TESTS}" diff --git a/composer.json b/composer.json index 0b17a32..1061fe4 100644 --- a/composer.json +++ b/composer.json @@ -5,6 +5,19 @@ "homepage": "https://github.com/localgovdrupal/localgov_alert_banner", "minimum-stability": "dev", "require": { + "drupal/condition_field": "^2.0", "drupal/field_group": "~3.1" + }, + "extra": { + "enable-patching": true, + "composer-exit-on-patch-failure": true, + "patchLevel": { + "drupal/core": "-p2" + }, + "patches": { + "drupal/condition_field": { + "Fix schema #3215202": "https://www.drupal.org/files/issues/2021-05-28/configuration-schema-fix-3215202-6.patch" + } + } } } diff --git a/config/install/core.entity_form_display.localgov_alert_banner.localgov_alert_banner.default.yml b/config/install/core.entity_form_display.localgov_alert_banner.localgov_alert_banner.default.yml index 3338236..785babe 100644 --- a/config/install/core.entity_form_display.localgov_alert_banner.localgov_alert_banner.default.yml +++ b/config/install/core.entity_form_display.localgov_alert_banner.localgov_alert_banner.default.yml @@ -2,6 +2,7 @@ langcode: en status: true dependencies: config: + - field.field.localgov_alert_banner.localgov_alert_banner.visibility - field.field.localgov_alert_banner.localgov_alert_banner.link - field.field.localgov_alert_banner.localgov_alert_banner.short_description - field.field.localgov_alert_banner.localgov_alert_banner.type_of_alert @@ -10,6 +11,7 @@ dependencies: module: - localgov_alert_banner module: + - condition_field - link - text id: localgov_alert_banner.localgov_alert_banner.default @@ -24,6 +26,12 @@ content: settings: display_label: true third_party_settings: { } + visibility: + weight: 5 + settings: { } + third_party_settings: { } + type: condition_field_default + region: content link: weight: 4 settings: @@ -32,6 +40,13 @@ content: third_party_settings: { } type: link_default region: content + remove_hide_link: + type: boolean_checkbox + weight: 6 + region: content + settings: + display_label: true + third_party_settings: { } short_description: weight: 3 settings: @@ -40,19 +55,6 @@ content: third_party_settings: { } type: text_textarea region: content - type_of_alert: - type: options_select - weight: 0 - region: content - settings: { } - third_party_settings: { } - remove_hide_link: - type: boolean_checkbox - weight: 6 - region: content - settings: - display_label: true - third_party_settings: { } title: type: string_textfield weight: 1 @@ -61,9 +63,15 @@ content: size: 60 placeholder: '' third_party_settings: { } + type_of_alert: + type: options_select + weight: 0 + region: content + settings: { } + third_party_settings: { } uid: type: entity_reference_autocomplete - weight: 5 + weight: 7 settings: match_operator: CONTAINS size: 60 diff --git a/config/install/core.entity_view_display.localgov_alert_banner.localgov_alert_banner.default.yml b/config/install/core.entity_view_display.localgov_alert_banner.localgov_alert_banner.default.yml index 292ca4a..7935b25 100644 --- a/config/install/core.entity_view_display.localgov_alert_banner.localgov_alert_banner.default.yml +++ b/config/install/core.entity_view_display.localgov_alert_banner.localgov_alert_banner.default.yml @@ -2,6 +2,7 @@ langcode: en status: true dependencies: config: + - field.field.localgov_alert_banner.localgov_alert_banner.visibility - field.field.localgov_alert_banner.localgov_alert_banner.link - field.field.localgov_alert_banner.localgov_alert_banner.short_description - field.field.localgov_alert_banner.localgov_alert_banner.type_of_alert @@ -10,6 +11,7 @@ dependencies: module: - localgov_alert_banner module: + - condition_field - link - text id: localgov_alert_banner.localgov_alert_banner.default @@ -17,6 +19,13 @@ targetEntityType: localgov_alert_banner bundle: localgov_alert_banner mode: default content: + visibility: + weight: 3 + label: above + settings: { } + third_party_settings: { } + type: condition_field_string + region: content link: weight: 2 label: hidden diff --git a/config/install/field.field.localgov_alert_banner.localgov_alert_banner.visibility.yml b/config/install/field.field.localgov_alert_banner.localgov_alert_banner.visibility.yml new file mode 100644 index 0000000..f3491f3 --- /dev/null +++ b/config/install/field.field.localgov_alert_banner.localgov_alert_banner.visibility.yml @@ -0,0 +1,24 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.localgov_alert_banner.visibility + - localgov_alert_banner.localgov_alert_banner_type.localgov_alert_banner + module: + - condition_field +id: localgov_alert_banner.localgov_alert_banner.visibility +field_name: visibility +entity_type: localgov_alert_banner +bundle: localgov_alert_banner +label: Visibility +description: 'Set the visibility of the alert to only show it on specific conditions.' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + enabled_plugins: + 'entity_bundle:node': false + request_path: true + user_role: false +field_type: condition_field diff --git a/config/install/field.storage.localgov_alert_banner.visibility.yml b/config/install/field.storage.localgov_alert_banner.visibility.yml new file mode 100644 index 0000000..2270505 --- /dev/null +++ b/config/install/field.storage.localgov_alert_banner.visibility.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + module: + - condition_field + - localgov_alert_banner +id: localgov_alert_banner.visibility +field_name: visibility +entity_type: localgov_alert_banner +type: condition_field +settings: { } +module: condition_field +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/localgov_alert_banner.info.yml b/localgov_alert_banner.info.yml index 13c68a7..bc929b7 100644 --- a/localgov_alert_banner.info.yml +++ b/localgov_alert_banner.info.yml @@ -12,3 +12,4 @@ dependencies: - drupal:options - drupal:user - drupal:views + - condition_field:condition_field diff --git a/localgov_alert_banner.install b/localgov_alert_banner.install index e3297ee..dbcb22a 100644 --- a/localgov_alert_banner.install +++ b/localgov_alert_banner.install @@ -5,7 +5,10 @@ * LocalGov Alert Banner install file. */ +use Drupal\Core\Config\FileStorage; use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; /** * Update alert banner entity definition to include the token on the entity. @@ -19,3 +22,47 @@ function localgov_alert_banner_update_8801() { \Drupal::entityDefinitionUpdateManager() ->installFieldStorageDefinition('token', 'localgov_alert_banner', 'localgov_alert_banner', $field_storage_definition); } + +/** + * Add visibility field to existing alert banners. + */ +function localgov_alert_banner_update_8901() { + + // Enable dependent condition_field module. + \Drupal::service('module_installer')->install(['condition_field']); + + $config_directory = new FileStorage(__DIR__ . '/config/install'); + + // Add visibility field storage. + $field_storage = $config_directory->read('field.storage.localgov_alert_banner.visibility'); + if ($field_storage && !FieldStorageConfig::loadByName('localgov_alert_banner', 'visibility')) { + FieldStorageConfig::create($field_storage)->save(); + } + + // Fetch all configured localgov_alert_banner bundles. + $alert_banner_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo('localgov_alert_banner'); + foreach ($alert_banner_bundles as $bundle => $info) { + + // Add visibility field to bundle. + $field_record = $config_directory->read('field.field.localgov_alert_banner.localgov_alert_banner.visibility'); + if ($field_record && !FieldConfig::loadByName('localgov_alert_banner', $bundle, 'visibility')) { + $field_record['bundle'] = $bundle; + FieldConfig::create($field_record)->save(); + } + + // Add visibility field to the bundles entity form. + $form_display = \Drupal::entityTypeManager() + ->getStorage('entity_form_display') + ->load('localgov_alert_banner.' . $bundle . '.default'); + if ($form_display) { + $form_display->setComponent('visibility', [ + 'region' => 'content', + 'settings' => [], + 'third_party_settings' => [], + 'type' => 'condition_field_default', + 'weight' => 5, + ]); + $form_display->save(); + } + } +} diff --git a/src/Entity/AlertBannerEntity.php b/src/Entity/AlertBannerEntity.php index 2afa827..36f2d13 100644 --- a/src/Entity/AlertBannerEntity.php +++ b/src/Entity/AlertBannerEntity.php @@ -2,6 +2,8 @@ namespace Drupal\localgov_alert_banner\Entity; +use Drupal\condition_field\ConditionAccessResolver; +use Drupal\Core\Cache\Cache; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Entity\EditorialContentEntityBase; @@ -84,6 +86,13 @@ class AlertBannerEntity extends EditorialContentEntityBase implements AlertBanne use EntityChangedTrait; use EntityPublishedTrait; + /** + * Drupal\Core\Condition\ConditionManager definition. + * + * @var \Drupal\Core\Condition\ConditionManager + */ + protected $pluginManagerCondition; + /** * {@inheritdoc} */ @@ -94,6 +103,14 @@ public static function preCreate(EntityStorageInterface $storage_controller, arr ]; } + /** + * {@inheritdoc} + */ + public function __construct(array $values, $entity_type, $bundle = FALSE, $translations = []) { + parent::__construct($values, $entity_type, $bundle, $translations); + $this->pluginManagerCondition = \Drupal::service('plugin.manager.condition'); + } + /** * {@inheritdoc} */ @@ -224,6 +241,39 @@ public function setOwner(UserInterface $account) { return $this; } + /** + * Is the alert banner visible? + * + * @return bool + * True if the alert banner is visible, otherwise FALSE. + */ + public function isVisible() { + + // Check if the field is present and has a value. + if (!$this->hasField('visibility') || $this->get('visibility')->isEmpty()) { + return TRUE; + } + + // Visibility condition config. + $conditions_config = $this->get('visibility')->getValue()[0]['conditions']; + + // Construct visibility conditions. + $conditions = []; + foreach ($conditions_config as $condition_id => $values) { + /** @var \Drupal\Core\Condition\ConditionInterface $condition */ + $condition = $this->pluginManagerCondition->createInstance($condition_id, $values); + $conditions[] = $condition; + } + + // Check if visibility conditions met. + if (ConditionAccessResolver::checkAccess($conditions, 'or')) { + return TRUE; + } + else { + return FALSE; + } + } + /** * {@inheritdoc} */ @@ -337,4 +387,26 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { return $fields; } + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + + // Add cache contexts depending on the enabled visibility condition plugins. + if ($this->hasField('visibility') && !$this->get('visibility')->isEmpty()) { + $contexts = []; + $conditions_config = $this->get('visibility')->getValue()[0]['conditions']; + + foreach ($conditions_config as $condition_id => $values) { + /** @var \Drupal\Core\Condition\ConditionInterface $condition */ + $condition = $this->pluginManagerCondition->createInstance($condition_id, $values); + $contexts = Cache::mergeContexts($contexts, $condition->getCacheContexts()); + } + + $this->addCacheContexts($contexts); + } + + return parent::getCacheContexts(); + } + } diff --git a/src/Entity/AlertBannerEntityType.php b/src/Entity/AlertBannerEntityType.php index 414e358..010beab 100644 --- a/src/Entity/AlertBannerEntityType.php +++ b/src/Entity/AlertBannerEntityType.php @@ -3,6 +3,10 @@ namespace Drupal\localgov_alert_banner\Entity; use Drupal\Core\Config\Entity\ConfigEntityBundleBase; +use Drupal\Core\Config\FileStorage; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; /** * Defines the Alert banner type entity. @@ -59,4 +63,38 @@ class AlertBannerEntityType extends ConfigEntityBundleBase implements AlertBanne */ protected $label; + /** + * {@inheritdoc} + */ + public function postSave(EntityStorageInterface $storage, $update = TRUE) { + + // Add fields from install config when creating a new alert banner type. + if (!$update) { + + $bundle = $this->id(); + $config_directory = new FileStorage(__DIR__ . '/../../config/install'); + + // Fields to add to the new alert banner type. + $fields_to_add = [ + 'visibility', + ]; + + foreach ($fields_to_add as $field_name) { + + // Add field storage if necessary (it may have been deleted). + $field_storage = $config_directory->read('field.storage.localgov_alert_banner.' . $field_name); + if ($field_storage && !FieldStorageConfig::loadByName('localgov_alert_banner', $field_name)) { + FieldStorageConfig::create($field_storage)->save(); + } + + // Add field config for new bundle. + $field_record = $config_directory->read('field.field.localgov_alert_banner.localgov_alert_banner.' . $field_name); + if ($field_record && !FieldConfig::loadByName('localgov_alert_banner', $bundle, $field_name)) { + $field_record['bundle'] = $bundle; + FieldConfig::create($field_record)->save(); + } + } + } + } + } diff --git a/src/Plugin/Block/AlertBannerBlock.php b/src/Plugin/Block/AlertBannerBlock.php index a913bd6..02a5b28 100644 --- a/src/Plugin/Block/AlertBannerBlock.php +++ b/src/Plugin/Block/AlertBannerBlock.php @@ -2,6 +2,7 @@ namespace Drupal\localgov_alert_banner\Plugin\Block; +use Drupal\Core\Cache\Cache; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -26,6 +27,13 @@ class AlertBannerBlock extends BlockBase implements ContainerFactoryPluginInterf */ protected $entityTypeManager; + /** + * Current alert banners. + * + * @var \Drupal\localgov_alert_banner\Entity\AlertBannerEntity[] + */ + protected $currentAlertBanners = []; + /** * Constructs a new AlertBannerBlock. * @@ -41,6 +49,7 @@ class AlertBannerBlock extends BlockBase implements ContainerFactoryPluginInterf public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->entityTypeManager = $entity_type_manager; + $this->currentAlertBanners = $this->getCurrentAlertBanners(); } /** @@ -119,13 +128,10 @@ public function build() { // Render the alert banner. $build = []; - foreach ($published_alert_banners as $published_alert_banner_id) { - $alert_banner = $this->entityTypeManager->getStorage('localgov_alert_banner') - ->load($published_alert_banner_id); + foreach ($this->currentAlertBanners as $alert_banner) { $build[] = $this->entityTypeManager->getViewBuilder('localgov_alert_banner') ->view($alert_banner); } - return $build; } @@ -135,10 +141,13 @@ public function build() { * Note: Default order will be by the field type_of_alert * (only on the default) and then updated date. * - * @return array - * Array with the IDs of any published alert banners. + * @return \Drupal\localgov_alert_banner\Entity\AlertBannerEntity[] + * Array of all published and visible alert banners. */ protected function getCurrentAlertBanners() { + $alert_banners = []; + + // Get list of published alert banner IDs. $types = $this->mapTypesConfigToQuery(); $published_alert_banner_query = $this->entityTypeManager->getStorage('localgov_alert_banner') ->getQuery() @@ -148,7 +157,17 @@ protected function getCurrentAlertBanners() { if (!empty($types)) { $published_alert_banner_query->condition('type', $types, 'IN'); } - return $published_alert_banner_query->execute(); + $published_alert_banners = $published_alert_banner_query->execute(); + + // Load alert banners and check they're visible. + foreach ($published_alert_banners as $alert_banner_id) { + $alert_banner = $this->entityTypeManager->getStorage('localgov_alert_banner')->load($alert_banner_id); + if ($alert_banner->isVisible()) { + $alert_banners[] = $alert_banner; + } + } + + return $alert_banners; } /** @@ -164,4 +183,23 @@ protected function mapTypesConfigToQuery() : array { }); } + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + $contexts = []; + foreach ($this->currentAlertBanners as $alert_banner) { + $contexts = Cache::mergeContexts($contexts, $alert_banner->getCacheContexts()); + } + return Cache::mergeContexts(parent::getCacheContexts(), $contexts); + } + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + // Invalidate cache on changes to alert banners. + return Cache::mergeTags(parent::getCacheTags(), ['localgov_alert_banner_list']); + } + } diff --git a/tests/src/Functional/VisibilityTest.php b/tests/src/Functional/VisibilityTest.php new file mode 100644 index 0000000..c6bd680 --- /dev/null +++ b/tests/src/Functional/VisibilityTest.php @@ -0,0 +1,76 @@ +drupalPlaceBlock('localgov_alert_banner_block'); + } + + /** + * Test the visibility conditions for alert banners. + */ + public function testAlertBannerVisibility() { + + // Create an alert banner. + $title = $this->randomMachineName(8); + $alert_message = 'Alert message: ' . $this->randomMachineName(16); + $alert = $this->container->get('entity_type.manager')->getStorage('localgov_alert_banner') + ->create([ + 'type' => 'localgov_alert_banner', + 'title' => $title, + 'short_description' => $alert_message, + 'type_of_alert' => 'minor', + 'status' => TRUE, + 'visibility' => [ + 'conditions' => [ + 'request_path' => [ + 'pages' => '/council-tax', + 'negate' => 0, + ], + ], + ], + ]); + $alert->save(); + + // Check it's not on front page. + $this->drupalGet(''); + $this->assertSession()->pageTextNotContains($title); + + // Check it's on /council-tax. + $this->drupalGet('/council-tax'); + $this->assertSession()->pageTextContains($title); + + // Check it's still not on front page. + $this->drupalGet(''); + $this->assertSession()->pageTextNotContains($title); + + // Check it's still on /council-tax. + $this->drupalGet('/council-tax'); + $this->assertSession()->pageTextContains($title); + } + +} diff --git a/tests/src/Kernel/AlertBannerBlockOrderTest.php b/tests/src/Kernel/AlertBannerBlockOrderTest.php index 599c46c..8eae387 100644 --- a/tests/src/Kernel/AlertBannerBlockOrderTest.php +++ b/tests/src/Kernel/AlertBannerBlockOrderTest.php @@ -27,6 +27,7 @@ class AlertBannerBlockOrderTest extends KernelTestBase { 'user', 'block', 'views', + 'condition_field', 'localgov_alert_banner', ]; @@ -72,7 +73,7 @@ public function testAlertBannerBlockOrder() { $alert[] = $alert_entity->id(); } - // Create a block instance of gthe alert banner block. + // Create a block instance of the alert banner block. $block_manager = $this->container->get('plugin.manager.block'); $config = []; $plugin_block = $block_manager->createInstance('localgov_alert_banner_block', $config); @@ -102,7 +103,8 @@ public function testAlertBannerBlockOrder() { $alert_new[] = $alert_entity->id(); } - // Render the block and get the alert banner IDs as an array. + // Create and render the block and get the alert banner IDs as an array. + $plugin_block = $block_manager->createInstance('localgov_alert_banner_block', $config); $render = $plugin_block->build(); foreach ($render as $render_value) { $result_2[] = $render_value['#localgov_alert_banner']->id(); @@ -126,7 +128,8 @@ public function testAlertBannerBlockOrder() { AlertBannerEntity::load($alert[$i])->set('changed', (new DrupalDateTime('now'))->getTimestamp())->save(); } - // Render the block and get the alert banner IDs as an array. + // Create and render the block and get the alert banner IDs as an array. + $plugin_block = $block_manager->createInstance('localgov_alert_banner_block', $config); $render = $plugin_block->build(); foreach ($render as $render_value) { $result_3[] = $render_value['#localgov_alert_banner']->id();