From 97a839c4912148463008755f12dda5249fb8a45f Mon Sep 17 00:00:00 2001 From: JacquesDurand <59364973+JacquesDurand@users.noreply.github.com> Date: Fri, 20 Sep 2024 09:53:00 +0200 Subject: [PATCH] fix: deactivable services and child groups (#739) --- .../admin_parentgroup_controller.js | 50 ++++++++++++++++++- fixtures/test/product.yaml | 1 + .../Admin/AbstractProductCrudController.php | 12 ++++- src/Controller/Admin/GroupCrudController.php | 12 +++++ .../Admin/ServiceCrudController.php | 37 ++++++++++++-- src/Entity/Group.php | 32 ++++++++++++ .../Type/Product/AbstractProductFormType.php | 2 +- src/Form/Type/Product/ServiceFormType.php | 6 ++- src/Repository/GroupRepository.php | 3 +- .../Admin/ServiceCrudControllerTest.php | 1 - .../Command/IndexProductsCommandTest.php | 7 +-- tests/TestReference.php | 2 + .../Admin/GroupCrudController/admin.fr.xlf | 15 ++++++ .../Admin/ServiceCrudController/admin.fr.xlf | 15 ++++++ 14 files changed, 181 insertions(+), 14 deletions(-) create mode 100644 translations/app/Controller/Admin/GroupCrudController/admin.fr.xlf create mode 100644 translations/app/Controller/Admin/ServiceCrudController/admin.fr.xlf diff --git a/assets/controllers/admin_parentgroup_controller.js b/assets/controllers/admin_parentgroup_controller.js index df6af16..5cbba13 100644 --- a/assets/controllers/admin_parentgroup_controller.js +++ b/assets/controllers/admin_parentgroup_controller.js @@ -4,6 +4,17 @@ import { Controller} from '@hotwired/stimulus' export default class extends Controller { static targets = ['servicesEnabledField', 'parentField', 'idField'] + connect() { + const parentFields = document.querySelectorAll('[data-label="Parent"]') + const trs = Array.from(parentFields) + .map(e => e.firstElementChild) + .filter(e => e.tagName === 'A') + .map(e => e.closest('tr')) + for (const tr of trs) { + this.checkDisabledServices(tr) + } + } + parentFieldTargetConnected(element) { const observer = new MutationObserver( ( ) => { if (element.tomselect) { @@ -43,9 +54,45 @@ export default class extends Controller { const params = new URLSearchParams(this.servicesEnabledFieldTarget.getAttribute('data-toggle-url')) this.disableServicesForChildGroups(params.get('entityId')) } + const parentFields = document.querySelectorAll('[data-label="Parent"]') + const trs = Array.from(parentFields) + .map(e => e.firstElementChild) + .filter(e => e.tagName === 'A') + .map(e => e.closest('tr')) + for (const tr of trs) { + this.checkDisabledServices(tr) + } }) } + async checkDisabledServices(tr) { + const id = tr.getAttribute('data-id') + const url = `/api/groups/${id}` + + const response = await fetch(url, {method: 'GET'}) + if (!response.ok) { + return + } + const group = await response.json() + + let disabledTr = false + for (const parentUrl of group.parentsRecursively) { + const parentResponse = await fetch(parentUrl, { method: 'GET' }) + if (!response.ok) { + return + } + const parent = await parentResponse.json() + if (!parent.servicesEnabled) { + tr.querySelector('[data-admin-parentgroup-target="servicesEnabledField"]').disabled = true + disabledTr = true + break + } + } + if (!disabledTr) { + tr.querySelector('[data-admin-parentgroup-target="servicesEnabledField"]').disabled = false + } + } + async disableServicesForChildGroups(groupId) { const url = `/api/groups/${groupId}/disable_child_services` @@ -59,7 +106,7 @@ export default class extends Controller { return } const data = await response.json() - const groupChild = data.children + const groupChild = data.childrenRecursively const groupChildId = groupChild.map(group => { return group.split('/')[3] @@ -70,6 +117,7 @@ export default class extends Controller { const params = new URLSearchParams(toggle.getAttribute('data-toggle-url')) if(groupChildId.includes(params.get('entityId'))) { toggle.checked = false + toggle.disabled = true } }) diff --git a/fixtures/test/product.yaml b/fixtures/test/product.yaml index c1f6de9..083eb03 100644 --- a/fixtures/test/product.yaml +++ b/fixtures/test/product.yaml @@ -8,6 +8,7 @@ App\Entity\Product: service (template, extends product): type: !php/enum App\Enum\Product\ProductType::SERVICE + visibility: !php/enum App\Enum\Product\ProductVisibility::RESTRICTED # Loic ————————————————————————————————————————————————————————————————————— loic_object_1 (extends object): diff --git a/src/Controller/Admin/AbstractProductCrudController.php b/src/Controller/Admin/AbstractProductCrudController.php index 5f0f351..b5af9e0 100755 --- a/src/Controller/Admin/AbstractProductCrudController.php +++ b/src/Controller/Admin/AbstractProductCrudController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Controller\Admin; +use App\Controller\i18nTrait; use App\EasyAdmin\Field\FieldTrait; use App\EasyAdmin\Filter\EnumFilter; use App\EasyAdmin\Filter\UuidFilter; @@ -15,8 +16,10 @@ use App\Enum\Product\ProductType; use App\Enum\Product\ProductVisibility; use App\Flysystem\EasyAdminHelper; use App\Flysystem\MediaManager; +use App\Form\Type\Product\AbstractProductFormType; use App\Helper\CsvExporter; use App\Repository\CategoryRepository; +use App\Repository\GroupRepository; use App\Repository\ProductRepository; use Doctrine\ORM\QueryBuilder; use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection; @@ -54,6 +57,7 @@ use Symfony\Contracts\Translation\TranslatorInterface; abstract class AbstractProductCrudController extends AbstractCrudController implements AdminSecuredCrudControllerInterface { use FieldTrait; + use i18nTrait; abstract public function getProductType(): ProductType; @@ -74,6 +78,7 @@ abstract class AbstractProductCrudController extends AbstractCrudController impl private readonly TranslatorInterface $translator, private readonly FilterFactory $filterFactory, private readonly SluggerInterface $slugger, + protected readonly GroupRepository $groupRepository, ) { } @@ -88,6 +93,9 @@ abstract class AbstractProductCrudController extends AbstractCrudController impl '@EasyAdmin/crud/form_theme.html.twig', 'easy_admin/crud/form_theme.html.twig', ]) + ->setFormOptions([ + 'validation_groups' => [AbstractProductFormType::class], + ]) ; } @@ -241,7 +249,8 @@ abstract class AbstractProductCrudController extends AbstractCrudController impl ->setFormType(EnumType::class) ->setFormTypeOption('class', ProductVisibility::class) ->setChoices(ProductVisibility::getAsArray()); - $groupsField = CollectionField::new('groups'); + $groupsField = AssociationField::new('groups')->onlyOnForms(); + $groupsFieldList = CollectionField::new('groups')->hideOnForm(); $ownerField = AssociationField::new('owner'); $categoryField = AssociationField::new('category') @@ -300,6 +309,7 @@ abstract class AbstractProductCrudController extends AbstractCrudController impl 'statusField', 'visibilityField', 'groupsField', + 'groupsFieldList', 'ownerField', 'categoryField', 'nameField', diff --git a/src/Controller/Admin/GroupCrudController.php b/src/Controller/Admin/GroupCrudController.php index 7417581..09264c8 100755 --- a/src/Controller/Admin/GroupCrudController.php +++ b/src/Controller/Admin/GroupCrudController.php @@ -291,6 +291,18 @@ final class GroupCrudController extends AbstractCrudController implements GroupA ]; if ($this->configurationRepository->getInstanceConfigurationOrCreate()->getServicesEnabled()) { + $i18prefix = $this->getI18nPrefix(self::class); + /** @var Group|null $group */ + $group = $this->getContext()?->getEntity()?->getInstance(); + if (null !== $group) { + foreach ($group->getParentsRecursively() as $parent) { + if (!$parent->getServicesEnabled()) { + $servicesEnabledField->setDisabled(); + $servicesEnabledField->setHelp($i18prefix.'.field.services_enabled.parent_disabled'); + break; + } + } + } array_splice($fields, 3, 0, [$servicesEnabledField]); } diff --git a/src/Controller/Admin/ServiceCrudController.php b/src/Controller/Admin/ServiceCrudController.php index bccc265..42fe556 100644 --- a/src/Controller/Admin/ServiceCrudController.php +++ b/src/Controller/Admin/ServiceCrudController.php @@ -5,10 +5,13 @@ declare(strict_types=1); namespace App\Controller\Admin; use App\Entity\Product; +use App\Enum\Group\UserMembership; use App\Enum\Product\ProductStatus; use App\Enum\Product\ProductType; use App\Enum\Product\ProductVisibility; +use Doctrine\ORM\QueryBuilder; use EasyCorp\Bundle\EasyAdminBundle\Config\Crud; +use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField; use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField; use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField; @@ -36,6 +39,7 @@ final class ServiceCrudController extends AbstractProductCrudController { $product = parent::createEntity($entityFqcn); $product->setCurrency(null); // remove the default value which is not needed here + $product->setVisibility(ProductVisibility::RESTRICTED); return $product; } @@ -49,6 +53,8 @@ final class ServiceCrudController extends AbstractProductCrudController 'typeField' => $typeField, 'statusField' => $statusField, 'visibilityField' => $visibilityField, + 'groupsField' => $groupsField, + 'groupsFieldList' => $groupsFieldList, 'ownerField' => $ownerField, 'categoryField' => $categoryField, 'nameField' => $nameField, @@ -61,7 +67,7 @@ final class ServiceCrudController extends AbstractProductCrudController // list if ($pageName === Crud::PAGE_INDEX) { - return [$nameField, $ownerField, $categoryField, $statusField, $visibilityField, $imageField, $createdAt]; + return [$nameField, $ownerField, $categoryField, $statusField, $visibilityField, $groupsFieldList, $imageField, $createdAt]; } /** @var ImageField $imageField */ @@ -71,10 +77,33 @@ final class ServiceCrudController extends AbstractProductCrudController if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) { /** @var ChoiceField $statusField */ $statusField->setChoices(ProductStatus::cases()); - /** @var ChoiceField $visibilityField */ - $visibilityField->setChoices(ProductVisibility::cases()); - return [$nameField, $ownerField, $categoryField, $statusField, $visibilityField, $descriptionField, $imageField, $durationField]; + if ($pageName === Crud::PAGE_NEW) { + return [$nameField, $ownerField, $categoryField, $statusField, $groupsField, $descriptionField, $imageField, $durationField]; + } + /** @var Product|null $product */ + $product = $this->getContext()?->getEntity()?->getInstance(); + $owner = $product?->getOwner(); + if (null !== $owner && !$owner->getUserGroupsConfirmedWithServices()->isEmpty()) { + /** @var AssociationField $groupsField */ + $groupsField->setQueryBuilder(function (QueryBuilder $queryBuilder) use ($owner) { + return $queryBuilder + ->join('entity.userGroups', 'ug') + ->andWhere('ug.membership != :membership') + ->andWhere('ug.user = :user') + ->andWhere('entity.servicesEnabled = :true') + ->setParameter('user', $owner) + ->setParameter('membership', UserMembership::INVITATION) + ->setParameter('true', true) + ; + }); + } else { + $i18prefix = $this->getI18nPrefix(self::class); + /** @var AssociationField $groupsField */ + $groupsField->setHelp($i18prefix.'.field.groups.help')->setDisabled(); + } + + return [$nameField, $ownerField, $categoryField, $statusField, $groupsField, $descriptionField, $imageField, $durationField]; } // detail diff --git a/src/Entity/Group.php b/src/Entity/Group.php index 46d98d6..7164aeb 100755 --- a/src/Entity/Group.php +++ b/src/Entity/Group.php @@ -8,6 +8,7 @@ use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Patch; use App\Doctrine\Behavior\TimestampableEntity; @@ -39,6 +40,7 @@ use Symfony\Component\Validator\Constraints as Assert; input: false, processor: GroupChildServicesEnabledProcessor::class ), + new Get(), ] )] class Group implements \Stringable @@ -412,4 +414,34 @@ class Group implements \Stringable { return !$this->getActiveOffers()->isEmpty(); } + + /** + * @return list + */ + public function getParentsRecursively(): array + { + $parents = []; + $parent = $this->getParent(); + if (null !== $parent) { + $parents = $parent->getParentsRecursively(); + $parents[] = $parent; + } + + return $parents; + } + + /** + * @return list + */ + public function getChildrenRecursively(): array + { + $result = []; + $children = $this->getChildren(); + foreach ($children as $child) { + $result = array_merge($result, $child->getChildrenRecursively()); + $result[] = $child; + } + + return $result; + } } diff --git a/src/Form/Type/Product/AbstractProductFormType.php b/src/Form/Type/Product/AbstractProductFormType.php index 1a19598..8234781 100644 --- a/src/Form/Type/Product/AbstractProductFormType.php +++ b/src/Form/Type/Product/AbstractProductFormType.php @@ -95,7 +95,7 @@ abstract class AbstractProductFormType extends AbstractType public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'validation_groups' => self::class, + 'validation_groups' => [self::class], ]); } } diff --git a/src/Form/Type/Product/ServiceFormType.php b/src/Form/Type/Product/ServiceFormType.php index 7a2a100..eb4c7f6 100644 --- a/src/Form/Type/Product/ServiceFormType.php +++ b/src/Form/Type/Product/ServiceFormType.php @@ -44,8 +44,8 @@ final class ServiceFormType extends AbstractProductFormType ]); // only if the user is connected and has groups + $groups = $this->groupRepository->getUserGroupsWithEnabledServices($user); if (!$user->getUserGroupsConfirmed()->isEmpty()) { - $groups = $this->groupRepository->getUserGroupsWithEnabledServices($user); $builder ->add('visibility', ChoiceType::class, [ 'label' => 'product.form.visibility', @@ -66,11 +66,15 @@ final class ServiceFormType extends AbstractProductFormType 'class' => Group::class, 'query_builder' => $groups, 'label' => [] === $groups->getQuery()->getResult() ? $i18nPrefix.'.form.no_groups' : $i18nPrefix.'.form.groups', + 'label_attr' => [] === $groups->getQuery()->getResult() ? ['class' => 'text-danger fs-6 fw-normal'] : [], 'expanded' => true, 'multiple' => true, 'required' => false, ]); } + if ($groups->getQuery()->getResult() === []) { + $builder->get('submit')->setDisabled(true); + } } public function getType(): ProductType diff --git a/src/Repository/GroupRepository.php b/src/Repository/GroupRepository.php index 618a088..9031d3f 100644 --- a/src/Repository/GroupRepository.php +++ b/src/Repository/GroupRepository.php @@ -126,8 +126,7 @@ final class GroupRepository extends ServiceEntityRepository public function disableServicesForChildGroup(Group $group): void { - /** @var Group $child */ - foreach ($group->getChildren() as $child) { + foreach ($group->getChildrenRecursively() as $child) { $child->setServicesEnabled(false); $this->getEntityManager()->persist($child); } diff --git a/tests/Functional/Controller/Admin/ServiceCrudControllerTest.php b/tests/Functional/Controller/Admin/ServiceCrudControllerTest.php index 12ad2f3..0b88a17 100644 --- a/tests/Functional/Controller/Admin/ServiceCrudControllerTest.php +++ b/tests/Functional/Controller/Admin/ServiceCrudControllerTest.php @@ -42,7 +42,6 @@ final class ServiceCrudControllerTest extends WebTestCase $form = $crawler->selectButton(TestReference::ACTION_SAVE_AND_RETURN)->form(); $client->submit($form, [ $form->getName().'[name]' => 'Object public', - $form->getName().'[visibility]' => 'public', $form->getName().'[status]' => 'active', $form->getName().'[owner]' => TestReference::ADMIN_LOIC, $form->getName().'[description]' => 'very nice object', diff --git a/tests/Integration/Search/Command/IndexProductsCommandTest.php b/tests/Integration/Search/Command/IndexProductsCommandTest.php index a7fee3c..f491e67 100644 --- a/tests/Integration/Search/Command/IndexProductsCommandTest.php +++ b/tests/Integration/Search/Command/IndexProductsCommandTest.php @@ -45,13 +45,14 @@ final class IndexProductsCommandTest extends KernelTestCase $searchDto = new Search(''); // all documents when not logged (-1 because of a restricted product) + // services are now restricted and do not appear here with default filters $results = $meilisearch->search($searchDto); - self::assertSame(TestReference::PRODUCTS_INDEXABLE_COUNT - 1, $results->getHitsCount()); + self::assertSame(TestReference::PRODUCTS_VISIBLE_COUNT - 1, $results->getHitsCount()); // all documents when logged with a user with access to the restricted product $searchDto->user = $this->getUserRepository()->get(TestReference::PLACE_APES); $results = $meilisearch->search($searchDto); - self::assertSame(TestReference::PRODUCTS_INDEXABLE_COUNT, $results->getHitsCount()); + self::assertSame(TestReference::PRODUCTS_VISIBLE_COUNT, $results->getHitsCount()); // keyword search $searchDto->user = null; @@ -60,7 +61,7 @@ final class IndexProductsCommandTest extends KernelTestCase self::assertSame(3, $results->getHitsCount()); // typo tolerance example - $searchDto->q = 'histiore'; + $searchDto->q = 'jumeles'; $results = $meilisearch->search($searchDto); self::assertSame(1, $results->getHitsCount()); } diff --git a/tests/TestReference.php b/tests/TestReference.php index c590ab2..f0f249a 100644 --- a/tests/TestReference.php +++ b/tests/TestReference.php @@ -69,6 +69,8 @@ final class TestReference public const PRODUCTS_COUNT = 18; public const PRODUCTS_NOT_INDEXABLE_COUNT = 1; public const PRODUCTS_INDEXABLE_COUNT = self::PRODUCTS_COUNT - self::PRODUCTS_NOT_INDEXABLE_COUNT; + public const PRODUCTS_RESTRICTED_COUNT = 5; + public const PRODUCTS_VISIBLE_COUNT = 12; public const PRODUCT_AVAILABILITIES_COUNT = 4; public const USER_17_SERVICES_COUNT = 0; diff --git a/translations/app/Controller/Admin/GroupCrudController/admin.fr.xlf b/translations/app/Controller/Admin/GroupCrudController/admin.fr.xlf new file mode 100644 index 0000000..265beb7 --- /dev/null +++ b/translations/app/Controller/Admin/GroupCrudController/admin.fr.xlf @@ -0,0 +1,15 @@ + + + +
+ +
+ + + app.controller.admin.group_crud_controller.field.services_enabled.parent_disabled + Un des groupes parents de ce groupe a désactivé la disponibilité des services + + + +
+
diff --git a/translations/app/Controller/Admin/ServiceCrudController/admin.fr.xlf b/translations/app/Controller/Admin/ServiceCrudController/admin.fr.xlf new file mode 100644 index 0000000..fed3404 --- /dev/null +++ b/translations/app/Controller/Admin/ServiceCrudController/admin.fr.xlf @@ -0,0 +1,15 @@ + + + +
+ +
+ + + app.controller.admin.service_crud_controller.field.groups.help + Aucun des groupes du/de la propriétaire n'a la disponibilité des services activée + + + +
+