Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VACMS-16575: Add extra validation to allow for combination of reusable and single page QAs. #16838

Merged
merged 9 commits into from
Jan 16, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

namespace Drupal\va_gov_backend\Plugin\Validation\Constraint;

/**
* Checks that paragraphs a and b have been created as required.
*
* @Constraint(
* id = "RequiredParagraphAB",
* label = @Translation("Limit Paragraph A and B", context = "Validation"),
* type = "string"
* )
*/
class RequiredParagraphAB extends RequiredParagraph {

/**
* The field name of paragraph A.
*
* @var string
*/
public $fieldParagraphA;

/**
* The field name of paragraph B.
*
* @var string
*/
public $fieldParagraphB;

/**
* Displays validation error as Drupal message when no field values exist.
*
* @var bool
*/
public $requiredErrorDisplayAsMessage;

/**
* The plural label.
*
* @var string
*/
public $pluralLabel;

/**
* The panel label.
*
* @var string
*/
public $panelLabel;

/**
* The message that will be shown if the paragraph number is less than min.
*
* @var string
* @see \Drupal\va_gov_backend\Plugin\Validation\Constraint\RequiredParagraphABValidator
*/
public $tooFew = 'Add %plurlLabel. A minimum of %min %readables is required.';

/**
* The message that will be shown if the paragraph number is more than max.
*
* @var string
* @see \Drupal\va_gov_backend\Plugin\Validation\Constraint\RequiredParagraphABValidator
*/
public $tooMany = 'Remove %plurlLabel. A maximum of %max %readables is allowed.';

/**
* The message that will be shown if the paragraph is empty.
*
* @var string
* @see \Drupal\va_gov_backend\Plugin\Validation\Constraint\RequiredParagraphABValidator
*/
public $required = 'A minimum of %min %readables is required when the %panelLabel page segment is enabled. Disable the FAQs page segment if there are no %readables to add.';

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

namespace Drupal\va_gov_backend\Plugin\Validation\Constraint;

use Drupal\Core\Entity\FieldableEntityInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

/**
* Validates the RequiredParagraph constraint.
*/
class RequiredParagraphABValidator extends ConstraintValidator {

/**
* {@inheritdoc}
*/
public function validate($entity, Constraint $constraint) {
assert($entity instanceof FieldableEntityInterface, 'Entity should inherit from FieldableEntityInterface.');
/** @var \Drupal\va_gov_backend\Plugin\Validation\Constraint\RequiredParagraphAB $constraint */
if (!$entity->hasField($constraint->toggle) && !$entity->hasField($constraint->fieldParagraphA) && !$entity->hasField($constraint->fieldParagraphB)) {
return;
}
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$panel_enabled = $entity->get($constraint->toggle)->getString();
$countA = $this->getCountForParagraphField($constraint->fieldParagraphA);
$countB = $this->getCountForParagraphField($constraint->fieldParagraphB);
$number = $countA + $countB;
$paragraphAField = $this->getBaseField($constraint->fieldParagraphA);
$paragraphBField = $this->getBaseField($constraint->fieldParagraphB);
$errorPath = $countA ? $paragraphAField : $paragraphBField;
if ($panel_enabled && $number < $constraint->min && $number > 0) {
$this->context->buildViolation($constraint->tooFew, [
'%plurlLabel' => $constraint->pluralLabel,
'%readable' => $constraint->readable,
'%min' => $constraint->min,
])
->atPath($errorPath)
->addViolation();
}
elseif ($panel_enabled && $number > $constraint->max) {
$this->context->buildViolation($constraint->tooMany, [
'%plurlLabel' => $constraint->pluralLabel,
'%readable' => $constraint->readable,
'%max' => $constraint->max,
])
->atPath($errorPath)
->addViolation();
}
elseif ($panel_enabled && $number === 0) {
// Adding a violation in this way ensures that it is displayed even if
// paragraphA and paragraphB have no values.
$this->context->addViolation($constraint->required, [
'%min' => $constraint->min,
'%panelLabel' => $constraint->panelLabel,
'%readable' => $constraint->readable,
]);
}
}

/**
* Gets the item count from a paragraph field.
*
* To target a nested field (a field within a paragraph), specify the $field
* with a colon ":" between the parent and child field names. Only one level
* of nesting is supported. eg: field_faq_group:field_faq_items.
*
* @param string $field
* The field name to get the count from.
*
* @return int
* The item count for the number of nested items.
*/
private function getCountForParagraphField(string $field): int {
$count = 0;
$entity = $this->context->getRoot()->getEntity();
if (str_contains($field, ':')) {
$fields = explode(":", $field);
if (!empty($fields)) {
[$outerParagraphField, $innerParagraphField] = $fields;
if ($entity->hasField($outerParagraphField)) {
/** @var \Drupal\entity_reference_revisions\Plugin\Field\FieldType\EntityReferenceRevisionsItem $item */
foreach ($entity->get($outerParagraphField) as $item) {
/** @var \Drupal\paragraphs\ParagraphInterface $paragraph */
$paragraph = $item->entity;
if ($paragraph->hasField($innerParagraphField)) {
$count += $paragraph->get($innerParagraphField)->count();
}
}
}
}
}
else {
$count = $entity->get($field)->count();
}
return $count;
}

/**
* Get a base field from a given paragraph field identifier.
*
* Since fields can contain colon's (":") to separate parent:child, this
* method is used to get the base field.
*
* @return string
* The base field name.
*/
private function getBaseField(string $field): string {
if (str_contains($field, ':')) {
[$baseField] = explode(":", $field);
return $baseField;
}
else {
return $field;
}
}

}
9 changes: 0 additions & 9 deletions docroot/modules/custom/va_gov_backend/va_gov_backend.module
Original file line number Diff line number Diff line change
Expand Up @@ -1323,15 +1323,6 @@ function va_gov_backend_entity_bundle_field_info_alter(&$fields, EntityTypeInter
}
// Add paragraph checks on clp panels.
if ($entity_type->id() === 'node' && $bundle === 'campaign_landing_page') {
// Add range check on faq panel.
if (isset($fields['field_clp_faq_paragraphs'])) {
$fields['field_clp_faq_paragraphs']->addConstraint('RequiredParagraph', [
'toggle' => 'field_clp_faq_panel',
'readable' => 'Q&A',
'min' => 3,
'max' => 10,
]);
}
// Add range check on stories panel.
if (isset($fields['field_clp_stories_teasers'])) {
$fields['field_clp_stories_teasers']->addConstraint('RequiredParagraph', [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace Drupal\va_gov_clp\EventSubscriber;

use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\core_event_dispatcher\EntityHookEvents;
use Drupal\core_event_dispatcher\Event\Entity\EntityTypeAlterEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
* VA.gov Campaign Landing Page Event Subscriber.
*/
class EntityEventSubscriber implements EventSubscriberInterface {

/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
EntityHookEvents::ENTITY_TYPE_ALTER => 'entityTypeAlter',
];
}

/**
* Equivalent of hook_entity_type_alter().
*
* @param \Drupal\core_event_dispatcher\Event\Entity\EntityTypeAlterEvent $event
* The event for entityTypeAlter.
*/
public function entityTypeAlter(EntityTypeAlterEvent $event): void {
$entity_types = $event->getEntityTypes();
if (!empty($entity_types['node'])) {
$nodeEntityType = $entity_types['node'];
$this->addConstraintsToClp($nodeEntityType);
}
}

/**
* Adds constraints to Campaign Landing Page Nodes.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entityType
* The entity type.
*/
public function addConstraintsToClp(EntityTypeInterface $entityType): void {
$entityType->addConstraint('RequiredParagraphAB', [
'toggle' => 'field_clp_faq_panel',
'readable' => 'Q&A',
'pluralLabel' => 'Page-Specific or Reusable Q&As',
'panelLabel' => 'FAQ',
'fieldParagraphA' => 'field_clp_faq_paragraphs',
'fieldParagraphB' => 'field_clp_reusable_q_a:field_q_as',
'requiredErrorDisplayAsMessage' => TRUE,
'min' => 3,
'max' => 10,
]);
}

}
5 changes: 5 additions & 0 deletions docroot/modules/custom/va_gov_clp/va_gov_clp.services.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
services:
va_gov_clp.entity_event_subscriber:
class: Drupal\va_gov_clp\EventSubscriber\EntityEventSubscriber
tags:
- { name: event_subscriber }
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,46 @@ Feature: Content Type: Campaign Landing Page
And I can fill in "Text" field with fake text
And I should see "Add Reusable Q&A"
And I should see "Add a link to more FAQs"

Scenario: Test FAQ page segment requirements
Given I am logged in as a user with the "content_admin" role
Then I create a "campaign_landing_page" node and continue

# Test maximum FAQs cannot be exceeded.
When I click to expand "FAQs"
And I enable the page segment within selector "#edit-group-faqs"
And I click the "Add Page-Specific Q&A" button
And I fill in "Question" field with fake text
And I fill in ckeditor "edit-field-clp-faq-paragraphs-0-subform-field-answer-0-subform-field-wysiwyg-0-value" with "Adding Page-Specific Q&As..."
And I click the "Add Reusable Q&A Group" button
And I click to expand "Q&As"
And I select 10 items from the "Add Reusable Q&As" Entity Browser modal
And I wait "2" seconds
And I fill in field with selector "#edit-revision-log-0-value" with fake text
And I save the node
Then I should see an element with the selector "#edit-field-clp-faq-paragraphs-0-subform-field-question-0-value.error"
And I should see "Remove Page-Specific or Reusable Q&As"

# Test fewer than minimum FAQs cannot be added.
When I click the button with selector "[data-drupal-selector='edit-field-clp-reusable-q-a-0-top'] .paragraphs-dropdown-toggle"
And I click the button with selector "[name='field_clp_reusable_q_a_0_remove']"
And I fill in field with selector "#edit-revision-log-0-value" with fake text
And I save the node
Then I should see an element with the selector "#edit-field-clp-faq-paragraphs-0-subform-field-question-0-value.error"
And I should see "Add Page-Specific or Reusable Q&As"

# Test required Q&As if FAQ segment is enabled
When I click the button with selector "[data-drupal-selector='edit-field-clp-faq-paragraphs-0-top'] .paragraphs-dropdown-toggle"
And I click the button with selector "[name='field_clp_faq_paragraphs_0_remove']"
And I fill in field with selector "#edit-revision-log-0-value" with fake text
And I save the node
Then I should see "A minimum of 3 Q&As is required when the FAQ page segment is enabled. Disable the FAQs page segment if there are no Q&As to add."

# Test that no Q&A is required if the FAQ page segment is disabled
When I click to expand "FAQs"
And I disable the page segment
And I fill in field with selector "#edit-revision-log-0-value" with fake text
And I save the node
Then the element with selector ".messages__content" should contain "Campaign Landing Page"
And the element with selector ".messages__content" should contain "has been updated."

Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,19 @@ import { Given } from "@badeball/cypress-cucumber-preprocessor";
Given("I enable the page segment", () => {
cy.findAllByLabelText("Enable this page segment").check({ force: true });
});

Given("I disable the page segment", () => {
cy.findAllByLabelText("Enable this page segment").uncheck({ force: true });
});

Given("I enable the page segment within selector {string}", (text) => {
cy.get(text)
.findAllByLabelText("Enable this page segment")
.check({ force: true });
});

Given("I disable the page segment within selector {string}", (text) => {
cy.get(text)
.findAllByLabelText("Enable this page segment")
.uncheck({ force: true });
});
Loading