From 07812d91221a41b7d129e40882d618327e36071b Mon Sep 17 00:00:00 2001 From: mussbach <57546580+mussbach@users.noreply.github.com> Date: Wed, 26 Jun 2024 19:33:29 +0200 Subject: [PATCH 001/173] chore: check whether role to grant the permission is allowed in the given orga type (3321) to avoid e.g. granting planner permission to institution orga --- .../Logic/Permission/AccessControlService.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/demosplan/DemosPlanCoreBundle/Logic/Permission/AccessControlService.php b/demosplan/DemosPlanCoreBundle/Logic/Permission/AccessControlService.php index 392c227697..84ffc519c2 100644 --- a/demosplan/DemosPlanCoreBundle/Logic/Permission/AccessControlService.php +++ b/demosplan/DemosPlanCoreBundle/Logic/Permission/AccessControlService.php @@ -236,6 +236,13 @@ private function addPermissionBasedOnOrgaType(string $permissionToEnable, RoleIn continue; } + // check whether role to grant the permission is allowed in the given orga type + // to avoid e.g. granting planner permission to institution orga + if (array_key_exists($orgaTypeInCustomer, OrgaTypeInterface::ORGATYPE_ROLE) && + !in_array($role->getCode(), OrgaTypeInterface::ORGATYPE_ROLE[$orgaTypeInCustomer],true)) { + continue; + } + // Do not store permission if it is dryrun if (false === $dryRun) { $this->createPermission($permissionToEnable, $orgaInCustomer, $customer, $role); From 8ac9faa727b83143422eb7f4f861a45b968a4775 Mon Sep 17 00:00:00 2001 From: MoritzMandler <89914798+MoritzMandler@users.noreply.github.com> Date: Tue, 9 Jul 2024 17:20:36 +0200 Subject: [PATCH 002/173] fix (refs DPLAN-11968): grant planning_agency invitation permission to fpa-only (#3398) * fix (refs DPLAN-11968): grant planning_agency invitation permission to fp-a only * style: Apply php-cs-fixer --------- Co-authored-by: Demos-CI --- demosplan/DemosPlanCoreBundle/Permissions/Permissions.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/demosplan/DemosPlanCoreBundle/Permissions/Permissions.php b/demosplan/DemosPlanCoreBundle/Permissions/Permissions.php index 89e722f250..14c0bc54bb 100644 --- a/demosplan/DemosPlanCoreBundle/Permissions/Permissions.php +++ b/demosplan/DemosPlanCoreBundle/Permissions/Permissions.php @@ -134,7 +134,7 @@ public function __construct( /** * Initialisiere die Permissions. */ - public function initPermissions(UserInterface $user, array $context = null): PermissionsInterface + public function initPermissions(UserInterface $user, ?array $context = null): PermissionsInterface { $this->user = $user; @@ -317,10 +317,6 @@ protected function setGlobalPermissions(): void // kann empfehlungen abgeben aber nicht die Bearbeitung abschliessen 'field_statement_recommendation', ]); - - $this->disablePermissions([ - 'field_procedure_adjustments_planning_agency', // Planungsbüro einem Verfahren zuordnen - ]); } if ($this->user->hasRole(Role::PLANNING_SUPPORTING_DEPARTMENT)) { // Fachplaner-Fachbehörde GLAUTH Kommune @@ -884,7 +880,7 @@ public function hasPermissionsetWrite($scope = null): bool /** * Setzt das initiale Set von kontxtbezogenen Menue-Highlights. */ - protected function initMenuhightlighting(array $context = null): void + protected function initMenuhightlighting(?array $context = null): void { if (null !== $context) { foreach ($context as $permission) { From 9db7df5a99e045f5152e3ebdcd9ffa264c109668 Mon Sep 17 00:00:00 2001 From: MoritzMandler <89914798+MoritzMandler@users.noreply.github.com> Date: Mon, 15 Jul 2024 13:49:41 +0200 Subject: [PATCH 003/173] fix (refs DPLAN-12068): add interface to event dispatcher. (3423) * fix (refs DPLAN-12068): add interface to event dispatcher. This way the event can be caught inside addons. * style: Apply php-cs-fixer --------- Co-authored-by: Demos-CI --- .../Controller/Segment/DraftsInfoApiController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demosplan/DemosPlanCoreBundle/Controller/Segment/DraftsInfoApiController.php b/demosplan/DemosPlanCoreBundle/Controller/Segment/DraftsInfoApiController.php index a5509bf3f0..69d5db3c62 100644 --- a/demosplan/DemosPlanCoreBundle/Controller/Segment/DraftsInfoApiController.php +++ b/demosplan/DemosPlanCoreBundle/Controller/Segment/DraftsInfoApiController.php @@ -10,6 +10,7 @@ namespace demosplan\DemosPlanCoreBundle\Controller\Segment; +use DemosEurope\DemosplanAddon\Contracts\Events\AfterSegmentationEventInterface; use DemosEurope\DemosplanAddon\Controller\APIController; use DemosEurope\DemosplanAddon\Utilities\Json; use demosplan\DemosPlanCoreBundle\Annotation\DplanPermissions; @@ -149,7 +150,7 @@ public function confirmDraftsAction( $segmentHandler->addSegments($segments); // request additional statement processing (asynchronous) - $eventDispatcher->dispatch(new AfterSegmentationEvent($statementHandler->getStatementWithCertainty($statementId))); + $eventDispatcher->dispatch(new AfterSegmentationEvent($statementHandler->getStatementWithCertainty($statementId)), AfterSegmentationEventInterface::class); $currentUser = $currentUserProvider->getUser(); From 0c132ba720db96d5b4194632be3eba03ebc6dec5 Mon Sep 17 00:00:00 2001 From: MoritzMandler <89914798+MoritzMandler@users.noreply.github.com> Date: Mon, 15 Jul 2024 13:49:50 +0200 Subject: [PATCH 004/173] fix (refs DPLAN-12068): add interface to event dispatcher. (3422) * fix (refs DPLAN-12068): add interface to event dispatcher. This way the event can be caught inside addons. * style: Apply php-cs-fixer --------- Co-authored-by: Demos-CI --- .../Controller/Segment/DraftsInfoApiController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demosplan/DemosPlanCoreBundle/Controller/Segment/DraftsInfoApiController.php b/demosplan/DemosPlanCoreBundle/Controller/Segment/DraftsInfoApiController.php index a5509bf3f0..69d5db3c62 100644 --- a/demosplan/DemosPlanCoreBundle/Controller/Segment/DraftsInfoApiController.php +++ b/demosplan/DemosPlanCoreBundle/Controller/Segment/DraftsInfoApiController.php @@ -10,6 +10,7 @@ namespace demosplan\DemosPlanCoreBundle\Controller\Segment; +use DemosEurope\DemosplanAddon\Contracts\Events\AfterSegmentationEventInterface; use DemosEurope\DemosplanAddon\Controller\APIController; use DemosEurope\DemosplanAddon\Utilities\Json; use demosplan\DemosPlanCoreBundle\Annotation\DplanPermissions; @@ -149,7 +150,7 @@ public function confirmDraftsAction( $segmentHandler->addSegments($segments); // request additional statement processing (asynchronous) - $eventDispatcher->dispatch(new AfterSegmentationEvent($statementHandler->getStatementWithCertainty($statementId))); + $eventDispatcher->dispatch(new AfterSegmentationEvent($statementHandler->getStatementWithCertainty($statementId)), AfterSegmentationEventInterface::class); $currentUser = $currentUserProvider->getUser(); From bc1f4979d57ee82fd0016ffc9e134a074008e2a3 Mon Sep 17 00:00:00 2001 From: mussbach <57546580+mussbach@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:42:58 +0200 Subject: [PATCH 005/173] chore (refs DPLAN-12006): copy the FAQ from one customer to another (3371) * chore: copy the FAQ from one customer to another * chore: fix typo * style: Apply php-cs-fixer * style: Apply php-cs-fixer --------- Co-authored-by: Demos-CI (cherry picked from commit e376fe7eeb5e9a1ea3f21020fa8c39fb6669f336) --- .../Command/Data/CopyFaqCommand.php | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 demosplan/DemosPlanCoreBundle/Command/Data/CopyFaqCommand.php diff --git a/demosplan/DemosPlanCoreBundle/Command/Data/CopyFaqCommand.php b/demosplan/DemosPlanCoreBundle/Command/Data/CopyFaqCommand.php new file mode 100644 index 0000000000..643db0505d --- /dev/null +++ b/demosplan/DemosPlanCoreBundle/Command/Data/CopyFaqCommand.php @@ -0,0 +1,107 @@ +helper = new QuestionHelper(); + } + + /** + * @throws Exception + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $output = new SymfonyStyle($input, $output); + $output->writeln('Copying FAQ from one customer to another.'); + $output->writeln('Choose customer to copy from'); + $fromCustomer = $this->helpers->askCustomer($input, $output); + $output->writeln('Choose customer to copy to'); + $toCustomer = $this->helpers->askCustomer($input, $output); + + try { + $faqCategories = collect($this->faqCategoryRepository->getFaqCategoriesByCustomer($fromCustomer)); + // only "valid" faq categories should be copied + $faqCategories = $faqCategories->filter( + static fn (FaqCategory $faqCategory) => in_array($faqCategory->getType(), FaqCategoryInterface::FAQ_CATEGORY_TYPES_MANDATORY, true) + || $faqCategory->isCustom() + ); + + $categoryCount = 0; + $faqCount = 0; + foreach ($faqCategories as $faqCategory) { + ++$categoryCount; + $copiedFaqCategory = new FaqCategory(); + $copiedFaqCategory->setCustomer($toCustomer); + $copiedFaqCategory->setTitle($faqCategory->getTitle()); + $copiedFaqCategory->setType($faqCategory->getType()); + $this->entityManager->persist($copiedFaqCategory); + + $faqs = $this->faqRepository->findBy(['faqCategory' => $faqCategory]); + foreach ($faqs as $faq) { + ++$faqCount; + $copiedFaq = new Faq(); + $copiedFaq->setCategory($copiedFaqCategory); + $copiedFaq->setEnabled($faq->getEnabled()); + $copiedFaq->setRoles($faq->getRoles()->toArray()); + $copiedFaq->setText($faq->getText()); + $copiedFaq->setTitle($faq->getTitle()); + $this->entityManager->persist($copiedFaq); + } + } + + $this->entityManager->flush(); + + $output->info('Copied '.$categoryCount.' FAQ categories with '.$faqCount.' FAQs.'); + $timeSaved = ceil(($categoryCount * 30 + $faqCount * 45) / 60); + $output->success("Faq where successfully copied. Saved approx $timeSaved minutes and lots of nerves."); + + return Command::SUCCESS; + } catch (Exception $e) { + // Print Exception + $output->error('Something went wrong during faq copy: '.$e->getMessage()); + } + + return Command::FAILURE; + } +} From de4590881618c40871f6781dbf9e52163db9ad32 Mon Sep 17 00:00:00 2001 From: mussbach <57546580+mussbach@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:43:18 +0200 Subject: [PATCH 006/173] chore (refs DPLAN-12006): register default users for new customer (3370) * chore (refs DPLAN-12006): register default users for new customer When the exist the citizen user and the ai api user should be automatically registered to avoid errors * style: Apply php-cs-fixer --------- Co-authored-by: Demos-CI (cherry picked from commit 742ba1e13cd14f05fec0908fe78cb14fbbb6e51a) --- .../Command/Data/GenerateCustomerCommand.php | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/demosplan/DemosPlanCoreBundle/Command/Data/GenerateCustomerCommand.php b/demosplan/DemosPlanCoreBundle/Command/Data/GenerateCustomerCommand.php index 90a0981dee..28dfa7d6f4 100644 --- a/demosplan/DemosPlanCoreBundle/Command/Data/GenerateCustomerCommand.php +++ b/demosplan/DemosPlanCoreBundle/Command/Data/GenerateCustomerCommand.php @@ -12,9 +12,16 @@ namespace demosplan\DemosPlanCoreBundle\Command\Data; +use DemosEurope\DemosplanAddon\Contracts\Entities\RoleInterface; +use DemosEurope\DemosplanAddon\Contracts\Entities\UserInterface; use demosplan\DemosPlanCoreBundle\Command\CoreCommand; +use demosplan\DemosPlanCoreBundle\Entity\User\AiApiUser; +use demosplan\DemosPlanCoreBundle\Entity\User\Customer; +use demosplan\DemosPlanCoreBundle\Entity\User\User; use demosplan\DemosPlanCoreBundle\Exception\EntryAlreadyExistsException; use demosplan\DemosPlanCoreBundle\Logic\User\CustomerService; +use demosplan\DemosPlanCoreBundle\Logic\User\UserService; +use demosplan\DemosPlanCoreBundle\Repository\RoleRepository; use Doctrine\ORM\EntityManagerInterface; use Exception; use InvalidArgumentException; @@ -53,7 +60,9 @@ public function __construct( private readonly CustomerService $customerService, private readonly EntityManagerInterface $entityManager, ParameterBagInterface $parameterBag, - string $name = null + private readonly RoleRepository $roleRepository, + private readonly UserService $userService, + ?string $name = null ) { parent::__construct($parameterBag, $name); $this->helper = new QuestionHelper(); @@ -96,7 +105,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int try { // create customer - $this->customerService->createCustomer($name, $subdomain); + $customer = $this->customerService->createCustomer($name, $subdomain); + $this->registerDefaultUsers($customer); $this->entityManager->flush(); $output->writeln( @@ -157,4 +167,24 @@ public function assertFreeSubdomain(mixed $subdomain): string return $subdomain; } + + private function registerDefaultUsers(Customer $customer): void + { + // register AiApiUser and AnonymousUser + $this->registerUser($customer, AiApiUser::AI_API_USER_LOGIN, RoleInterface::API_AI_COMMUNICATOR); + $this->registerUser($customer, UserInterface::ANONYMOUS_USER_LOGIN, RoleInterface::CITIZEN); + } + + private function registerUser(Customer $customer, string $login, string $roleString): void + { + $user = $this->userService->findDistinctUserByEmailOrLogin($login); + if ($user instanceof User) { + // add user to customer + $user->setDplanroles( + $this->roleRepository->getUserRolesByCodes([$roleString]), + $customer + ); + $this->userService->updateUserObject($user); + } + } } From 0bb60968e9b9657a6159b6fad71c1fa25a47a0ac Mon Sep 17 00:00:00 2001 From: salisdemos <40487461+salisdemos@users.noreply.github.com> Date: Tue, 16 Jul 2024 14:07:06 +0200 Subject: [PATCH 007/173] fix(refs DPLAN-11964): Set empty array as empty Object for Territory (3420) * fix(refs DPLAN-11964): Set empty array as empty Object for Territory The Components expects an Object and can't handle (empty) Arrays. Maybe the fix should be done whereever the Array is set. But I couldn't find out where that happens (in acceptable time) * check for array, not for length --- client/js/components/map/admin/MapAdmin.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/js/components/map/admin/MapAdmin.vue b/client/js/components/map/admin/MapAdmin.vue index 8e671f768a..4743171359 100644 --- a/client/js/components/map/admin/MapAdmin.vue +++ b/client/js/components/map/admin/MapAdmin.vue @@ -37,7 +37,7 @@ :max-extent="procedureMapSettings.attributes.defaultMapExtent" :procedure-id="procedureId" :procedure-coordinates="procedureMapSettings.attributes.coordinate" - :procedure-init-territory="procedureMapSettings.attributes.territory" + :procedure-init-territory="Array.isArray(procedureMapSettings.attributes.territory) ? {} : procedureMapSettings.attributes.territory" :scales="procedureMapSettings.attributes.availableScales" @field:update="setField" /> From 75ca247cefd1de3921ac2e111706542d34ca2c99 Mon Sep 17 00:00:00 2001 From: Sami Mussbach Date: Wed, 17 Jul 2024 13:05:12 +0200 Subject: [PATCH 008/173] chore (refs AB#18220): ignore invalid roles from keycloak --- .../DemosPlanCoreBundle/Logic/OzgKeycloakUserDataMapper.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/demosplan/DemosPlanCoreBundle/Logic/OzgKeycloakUserDataMapper.php b/demosplan/DemosPlanCoreBundle/Logic/OzgKeycloakUserDataMapper.php index 20df75f6fb..aa64044927 100644 --- a/demosplan/DemosPlanCoreBundle/Logic/OzgKeycloakUserDataMapper.php +++ b/demosplan/DemosPlanCoreBundle/Logic/OzgKeycloakUserDataMapper.php @@ -392,6 +392,9 @@ private function mapUserRoleData(): array // ['Fachplanung-Administration', 'Sachplanung-Fachbearbeitung', ''] counts as ['Fachplanung-Administration'] if (array_key_exists($customer->getSubdomain(), $rolesOfCustomer)) { foreach ($rolesOfCustomer[$customer->getSubdomain()] as $roleName) { + if (null === $roleName || '' === $roleName) { + continue; + } $this->logger->info('Role found for subdomain '.$customer->getSubdomain().': '.$roleName); if (array_key_exists($roleName, self::ROLETITLE_TO_ROLECODE)) { $this->logger->info('Role recognized: '.$roleName); From d23ecac2689ba036f7184996e7a43b7b1c370a28 Mon Sep 17 00:00:00 2001 From: Sami Mussbach Date: Wed, 17 Jul 2024 13:05:32 +0200 Subject: [PATCH 009/173] chore (refs AB#18220): improve logging during keycloak login --- .../ValueObject/OzgKeycloakUserData.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/demosplan/DemosPlanCoreBundle/ValueObject/OzgKeycloakUserData.php b/demosplan/DemosPlanCoreBundle/ValueObject/OzgKeycloakUserData.php index 0760eb7f57..a5beb0153e 100644 --- a/demosplan/DemosPlanCoreBundle/ValueObject/OzgKeycloakUserData.php +++ b/demosplan/DemosPlanCoreBundle/ValueObject/OzgKeycloakUserData.php @@ -11,6 +11,7 @@ namespace demosplan\DemosPlanCoreBundle\ValueObject; use League\OAuth2\Client\Provider\ResourceOwnerInterface; +use Psr\Log\LoggerInterface; use Stringable; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException; @@ -55,8 +56,10 @@ class OzgKeycloakUserData extends ValueObject implements KeycloakUserDataInterfa protected string $lastName = ''; private readonly string $keycloakGroupRoleString; - public function __construct(ParameterBagInterface $parameterBag) - { + public function __construct( + private readonly LoggerInterface $logger, + ParameterBagInterface $parameterBag + ) { $this->keycloakGroupRoleString = $parameterBag->get('keycloak_group_role_string'); } @@ -131,9 +134,14 @@ public function checkMandatoryValuesExist(): void private function mapCustomerRoles(array $groups): void { foreach ($groups as $group) { + $this->logger->info('Parse group: '.$group); $subGroups = explode('/', $group); if (str_contains($subGroups[1], $this->keycloakGroupRoleString)) { $subdomain = strtolower(explode('-', $subGroups[2])[0]); + if (!array_key_exists(3, $subGroups)) { + $this->logger->error('Group does not contain role', ['group' => $group, 'subgroups' => $subGroups]); + continue; + } $this->customerRoleRelations[$subdomain][] = $subGroups[3]; } } From e2053e9685a26c4934e28fafd8f24a7061a9a94e Mon Sep 17 00:00:00 2001 From: sierrazamudiodemos <152305675+sierrazamudiodemos@users.noreply.github.com> Date: Mon, 22 Jul 2024 13:41:43 +0000 Subject: [PATCH 010/173] Fix Statement Deleter tests (3461) * feat (DPLAN-3796) Adjust tests WIP * feat (DPLAN-3796) Enable workable test * feat (DPLAN-3796) Enable workable test * feat (DPLAN-3796) Enable workable test * feat (DPLAN-3796) Enable workable test * style: Apply php-cs-fixer * feat (DPLAN-3796) Remove outdated test * feat (DPLAN-3796) Create mocks for test * style: Apply php-cs-fixer * feat (DPLAN-3796) Adjust test using mocks * feat (DPLAN-3796) Adjust comment --------- Co-authored-by: Demos-CI --- .../Factory/Statement/CountyFactory.php | 71 +++++++++++++++++++ .../Factory/Statement/MunicipalityFactory.php | 71 +++++++++++++++++++ .../Factory/Statement/PriorityAreaFactory.php | 71 +++++++++++++++++++ .../Statement/StatementAttributeFactory.php | 71 +++++++++++++++++++ .../Functional/StatementDeleterTest.php | 68 +++++++++--------- 5 files changed, 318 insertions(+), 34 deletions(-) create mode 100644 demosplan/DemosPlanCoreBundle/DataGenerator/Factory/Statement/CountyFactory.php create mode 100644 demosplan/DemosPlanCoreBundle/DataGenerator/Factory/Statement/MunicipalityFactory.php create mode 100644 demosplan/DemosPlanCoreBundle/DataGenerator/Factory/Statement/PriorityAreaFactory.php create mode 100644 demosplan/DemosPlanCoreBundle/DataGenerator/Factory/Statement/StatementAttributeFactory.php diff --git a/demosplan/DemosPlanCoreBundle/DataGenerator/Factory/Statement/CountyFactory.php b/demosplan/DemosPlanCoreBundle/DataGenerator/Factory/Statement/CountyFactory.php new file mode 100644 index 0000000000..5e4e626a37 --- /dev/null +++ b/demosplan/DemosPlanCoreBundle/DataGenerator/Factory/Statement/CountyFactory.php @@ -0,0 +1,71 @@ + + * + * @method County|Proxy create(array|callable $attributes = []) + * @method static County|Proxy createOne(array $attributes = []) + * @method static County|Proxy find(object|array|mixed $criteria) + * @method static County|Proxy findOrCreate(array $attributes) + * @method static County|Proxy first(string $sortedField = 'id') + * @method static County|Proxy last(string $sortedField = 'id') + * @method static County|Proxy random(array $attributes = []) + * @method static County|Proxy randomOrCreate(array $attributes = []) + * @method static CountyRepository|ProxyRepositoryDecorator repository() + * @method static County[]|Proxy[] all() + * @method static County[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static County[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static County[]|Proxy[] findBy(array $attributes) + * @method static County[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static County[]|Proxy[] randomSet(int $number, array $attributes = []) + * + * @phpstan-method County&Proxy create(array|callable $attributes = []) + * @phpstan-method static County&Proxy createOne(array $attributes = []) + * @phpstan-method static County&Proxy find(object|array|mixed $criteria) + * @phpstan-method static County&Proxy findOrCreate(array $attributes) + * @phpstan-method static County&Proxy first(string $sortedField = 'id') + * @phpstan-method static County&Proxy last(string $sortedField = 'id') + * @phpstan-method static County&Proxy random(array $attributes = []) + * @phpstan-method static County&Proxy randomOrCreate(array $attributes = []) + * @phpstan-method static ProxyRepositoryDecorator repository() + * @phpstan-method static list> all() + * @phpstan-method static list> createMany(int $number, array|callable $attributes = []) + * @phpstan-method static list> createSequence(iterable|callable $sequence) + * @phpstan-method static list> findBy(array $attributes) + * @phpstan-method static list> randomRange(int $min, int $max, array $attributes = []) + * @phpstan-method static list> randomSet(int $number, array $attributes = []) + */ +final class CountyFactory extends PersistentProxyObjectFactory +{ + public static function class(): string + { + return County::class; + } + + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories + */ + protected function defaults(): array|callable + { + return [ + 'name' => self::faker()->text(36), + ]; + } +} diff --git a/demosplan/DemosPlanCoreBundle/DataGenerator/Factory/Statement/MunicipalityFactory.php b/demosplan/DemosPlanCoreBundle/DataGenerator/Factory/Statement/MunicipalityFactory.php new file mode 100644 index 0000000000..d83715dc49 --- /dev/null +++ b/demosplan/DemosPlanCoreBundle/DataGenerator/Factory/Statement/MunicipalityFactory.php @@ -0,0 +1,71 @@ + + * + * @method Municipality|Proxy create(array|callable $attributes = []) + * @method static Municipality|Proxy createOne(array $attributes = []) + * @method static Municipality|Proxy find(object|array|mixed $criteria) + * @method static Municipality|Proxy findOrCreate(array $attributes) + * @method static Municipality|Proxy first(string $sortedField = 'id') + * @method static Municipality|Proxy last(string $sortedField = 'id') + * @method static Municipality|Proxy random(array $attributes = []) + * @method static Municipality|Proxy randomOrCreate(array $attributes = []) + * @method static MunicipalityRepository|ProxyRepositoryDecorator repository() + * @method static Municipality[]|Proxy[] all() + * @method static Municipality[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static Municipality[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static Municipality[]|Proxy[] findBy(array $attributes) + * @method static Municipality[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static Municipality[]|Proxy[] randomSet(int $number, array $attributes = []) + * + * @phpstan-method Municipality&Proxy create(array|callable $attributes = []) + * @phpstan-method static Municipality&Proxy createOne(array $attributes = []) + * @phpstan-method static Municipality&Proxy find(object|array|mixed $criteria) + * @phpstan-method static Municipality&Proxy findOrCreate(array $attributes) + * @phpstan-method static Municipality&Proxy first(string $sortedField = 'id') + * @phpstan-method static Municipality&Proxy last(string $sortedField = 'id') + * @phpstan-method static Municipality&Proxy random(array $attributes = []) + * @phpstan-method static Municipality&Proxy randomOrCreate(array $attributes = []) + * @phpstan-method static ProxyRepositoryDecorator repository() + * @phpstan-method static list> all() + * @phpstan-method static list> createMany(int $number, array|callable $attributes = []) + * @phpstan-method static list> createSequence(iterable|callable $sequence) + * @phpstan-method static list> findBy(array $attributes) + * @phpstan-method static list> randomRange(int $min, int $max, array $attributes = []) + * @phpstan-method static list> randomSet(int $number, array $attributes = []) + */ +final class MunicipalityFactory extends PersistentProxyObjectFactory +{ + public static function class(): string + { + return Municipality::class; + } + + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories + */ + protected function defaults(): array|callable + { + return [ + 'name' => self::faker()->text(255), + ]; + } +} diff --git a/demosplan/DemosPlanCoreBundle/DataGenerator/Factory/Statement/PriorityAreaFactory.php b/demosplan/DemosPlanCoreBundle/DataGenerator/Factory/Statement/PriorityAreaFactory.php new file mode 100644 index 0000000000..84a105e4e2 --- /dev/null +++ b/demosplan/DemosPlanCoreBundle/DataGenerator/Factory/Statement/PriorityAreaFactory.php @@ -0,0 +1,71 @@ + + * + * @method PriorityArea|Proxy create(array|callable $attributes = []) + * @method static PriorityArea|Proxy createOne(array $attributes = []) + * @method static PriorityArea|Proxy find(object|array|mixed $criteria) + * @method static PriorityArea|Proxy findOrCreate(array $attributes) + * @method static PriorityArea|Proxy first(string $sortedField = 'id') + * @method static PriorityArea|Proxy last(string $sortedField = 'id') + * @method static PriorityArea|Proxy random(array $attributes = []) + * @method static PriorityArea|Proxy randomOrCreate(array $attributes = []) + * @method static PriorityAreaRepository|ProxyRepositoryDecorator repository() + * @method static PriorityArea[]|Proxy[] all() + * @method static PriorityArea[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static PriorityArea[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static PriorityArea[]|Proxy[] findBy(array $attributes) + * @method static PriorityArea[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static PriorityArea[]|Proxy[] randomSet(int $number, array $attributes = []) + * + * @phpstan-method PriorityArea&Proxy create(array|callable $attributes = []) + * @phpstan-method static PriorityArea&Proxy createOne(array $attributes = []) + * @phpstan-method static PriorityArea&Proxy find(object|array|mixed $criteria) + * @phpstan-method static PriorityArea&Proxy findOrCreate(array $attributes) + * @phpstan-method static PriorityArea&Proxy first(string $sortedField = 'id') + * @phpstan-method static PriorityArea&Proxy last(string $sortedField = 'id') + * @phpstan-method static PriorityArea&Proxy random(array $attributes = []) + * @phpstan-method static PriorityArea&Proxy randomOrCreate(array $attributes = []) + * @phpstan-method static ProxyRepositoryDecorator repository() + * @phpstan-method static list> all() + * @phpstan-method static list> createMany(int $number, array|callable $attributes = []) + * @phpstan-method static list> createSequence(iterable|callable $sequence) + * @phpstan-method static list> findBy(array $attributes) + * @phpstan-method static list> randomRange(int $min, int $max, array $attributes = []) + * @phpstan-method static list> randomSet(int $number, array $attributes = []) + */ +final class PriorityAreaFactory extends PersistentProxyObjectFactory +{ + public static function class(): string + { + return PriorityArea::class; + } + + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories + */ + protected function defaults(): array|callable + { + return [ + 'key' => self::faker()->text(36), + ]; + } +} diff --git a/demosplan/DemosPlanCoreBundle/DataGenerator/Factory/Statement/StatementAttributeFactory.php b/demosplan/DemosPlanCoreBundle/DataGenerator/Factory/Statement/StatementAttributeFactory.php new file mode 100644 index 0000000000..524c2fbe70 --- /dev/null +++ b/demosplan/DemosPlanCoreBundle/DataGenerator/Factory/Statement/StatementAttributeFactory.php @@ -0,0 +1,71 @@ + + * + * @method StatementAttribute|Proxy create(array|callable $attributes = []) + * @method static StatementAttribute|Proxy createOne(array $attributes = []) + * @method static StatementAttribute|Proxy find(object|array|mixed $criteria) + * @method static StatementAttribute|Proxy findOrCreate(array $attributes) + * @method static StatementAttribute|Proxy first(string $sortedField = 'id') + * @method static StatementAttribute|Proxy last(string $sortedField = 'id') + * @method static StatementAttribute|Proxy random(array $attributes = []) + * @method static StatementAttribute|Proxy randomOrCreate(array $attributes = []) + * @method static StatementAttributeRepository|ProxyRepositoryDecorator repository() + * @method static StatementAttribute[]|Proxy[] all() + * @method static StatementAttribute[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static StatementAttribute[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static StatementAttribute[]|Proxy[] findBy(array $attributes) + * @method static StatementAttribute[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static StatementAttribute[]|Proxy[] randomSet(int $number, array $attributes = []) + * + * @phpstan-method StatementAttribute&Proxy create(array|callable $attributes = []) + * @phpstan-method static StatementAttribute&Proxy createOne(array $attributes = []) + * @phpstan-method static StatementAttribute&Proxy find(object|array|mixed $criteria) + * @phpstan-method static StatementAttribute&Proxy findOrCreate(array $attributes) + * @phpstan-method static StatementAttribute&Proxy first(string $sortedField = 'id') + * @phpstan-method static StatementAttribute&Proxy last(string $sortedField = 'id') + * @phpstan-method static StatementAttribute&Proxy random(array $attributes = []) + * @phpstan-method static StatementAttribute&Proxy randomOrCreate(array $attributes = []) + * @phpstan-method static ProxyRepositoryDecorator repository() + * @phpstan-method static list> all() + * @phpstan-method static list> createMany(int $number, array|callable $attributes = []) + * @phpstan-method static list> createSequence(iterable|callable $sequence) + * @phpstan-method static list> findBy(array $attributes) + * @phpstan-method static list> randomRange(int $min, int $max, array $attributes = []) + * @phpstan-method static list> randomSet(int $number, array $attributes = []) + */ +final class StatementAttributeFactory extends PersistentProxyObjectFactory +{ + public static function class(): string + { + return StatementAttribute::class; + } + + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories + */ + protected function defaults(): array|callable + { + return [ + 'type' => self::faker()->text(50), + ]; + } +} diff --git a/tests/backend/core/Statement/Functional/StatementDeleterTest.php b/tests/backend/core/Statement/Functional/StatementDeleterTest.php index 7204532ab9..61742a900b 100644 --- a/tests/backend/core/Statement/Functional/StatementDeleterTest.php +++ b/tests/backend/core/Statement/Functional/StatementDeleterTest.php @@ -13,7 +13,11 @@ namespace Tests\Core\Statement\Functional; use demosplan\DemosPlanCoreBundle\DataFixtures\ORM\TestData\LoadUserData; +use demosplan\DemosPlanCoreBundle\DataGenerator\Factory\Statement\CountyFactory; +use demosplan\DemosPlanCoreBundle\DataGenerator\Factory\Statement\MunicipalityFactory; +use demosplan\DemosPlanCoreBundle\DataGenerator\Factory\Statement\PriorityAreaFactory; use demosplan\DemosPlanCoreBundle\DataGenerator\Factory\Statement\SegmentFactory; +use demosplan\DemosPlanCoreBundle\DataGenerator\Factory\Statement\StatementAttributeFactory; use demosplan\DemosPlanCoreBundle\DataGenerator\Factory\Statement\StatementFactory; use demosplan\DemosPlanCoreBundle\Entity\Procedure\ProcedurePerson; use demosplan\DemosPlanCoreBundle\Entity\Statement\County; @@ -47,25 +51,6 @@ protected function setUp(): void $this->logIn($user); } - public function testEmtpyInternIdOfOriginalInCaseOfDeleteLastChild(): void - { - self::markTestSkipped('This test was skipped because of pre-existing errors. They are most likely easily fixable but prevent us from getting to a usable state of our CI.'); - - $this->enablePermissions(['feature_auto_delete_original_statement']); - - $testStatement = $this->getStatementReference('testStatementWithInternID'); - $testStatementId = $testStatement->getId(); - $relatedOriginal = $testStatement->getOriginal(); - static::assertInstanceOf(Statement::class, $relatedOriginal); - static::assertNotNull($testStatement->getInternId()); - static::assertNotNull($relatedOriginal->getInternId()); - static::assertCount(1, $testStatement->getOriginal()->getChildren()); - - $this->sut->deleteStatementObject($testStatement); - static::assertNull($this->find(Statement::class, $testStatementId)); - static::assertNull($relatedOriginal->getInternId()); - } - public function testDoNotEmtpyInternIdOfOriginalInCaseOfDeleteLastChild(): void { $this->enablePermissions(['feature_auto_delete_original_statement']); @@ -88,8 +73,6 @@ public function testDoNotEmtpyInternIdOfOriginalInCaseOfDeleteLastChild(): void public function testDeleteStatementButNotCopyOfStatement(): void { - self::markSkippedForCIElasticsearchUnavailable(); - $testStatement2 = $this->getStatementReference('testStatement2'); $testStatementId = $testStatement2->getId(); @@ -146,8 +129,6 @@ public function testCascadeDeleteRelatedSubmitterOnDeleteStatement() public function testDeleteStatement(): void { - self::markSkippedForCIElasticsearchUnavailable(); - $testTag1 = $this->getTagReference('testFixtureTag_1'); $testStatement2 = $this->getStatementReference('testStatement2'); static::assertInstanceOf(StatementMeta::class, $testStatement2->getMeta()); @@ -184,14 +165,34 @@ public function testDeleteStatement(): void ); } - // test DB-sited onDelete:Cascade - // will only work if cascading in sqlite enabled + /** + * Tests the database-side cascading delete functionality. + * + * This test verifies that when a `Statement` object is deleted, related entities such as + * `StatementAttribute` are also deleted via database-side cascading, while other related entities + * like `County`, `Municipality`, and `PriorityArea` remain unaffected in the database. + * + * The test creates a `Statement` object linked with one each of `County`, `Municipality`, + * `PriorityArea`, and `StatementAttribute`. It then deletes the `Statement` object and checks: + * - The related `StatementAttribute` is also deleted (verifying cascading delete). + * - The counts of `County`, `Municipality`, and `PriorityArea` entities in the database remain unchanged, + * indicating they are not deleted (verifying non-cascading behavior for these entities). + */ public function testDBSitedCasading(): void { - // No cascading on sqlite - self::markSkippedForCIElasticsearchUnavailable(); + $originalStatement = StatementFactory::createOne(); + $county = CountyFactory::createOne(); + $municipality = MunicipalityFactory::createOne(); + $priorityArea = PriorityAreaFactory::createOne(); + + $testStatement = StatementFactory::createOne([ + 'counties' => [$county], + 'municipalities' => [$municipality], + 'priorityAreas' => [$priorityArea], + 'original' => $originalStatement]); + + $statementAttribute = StatementAttributeFactory::createOne(['statement' => $testStatement]); - $testStatement = $this->getStatementReference('testStatement'); $countiesOfStatement = $testStatement->getCounties(); $municipalitiesOfStatement = $testStatement->getMunicipalities(); $priorityAreasOfStatement = $testStatement->getPriorityAreas(); @@ -211,7 +212,7 @@ public function testDBSitedCasading(): void static::assertInstanceOf(StatementAttribute::class, $attributesOfStatement[0]); // delete Statement - $result = $this->sut->deleteStatementObject($testStatement); + $result = $this->sut->deleteStatementObject($testStatement->_real()); static::assertTrue($result); // still the same of amount of counties/municipalities/priorityAreas in the DB @@ -241,8 +242,6 @@ public function testDBSitedCasading(): void */ public function testDeleteAssignedStatement(): void { - self::markSkippedForCIElasticsearchUnavailable(); - $this->enablePermissions(['feature_statement_assignment']); $currentUser = $this->loginTestUser(); @@ -270,7 +269,6 @@ public function testDeleteAssignedStatement(): void public function testDeleteLockedStatement(): void { - self::markSkippedForCIElasticsearchUnavailable(); $this->enablePermissions(['feature_statement_assignment']); $user = $this->getUserReference(LoadUserData::TEST_USER_PLANNER_AND_PUBLIC_INTEREST_BODY); @@ -288,9 +286,11 @@ public function testDeleteLockedStatement(): void public function testDeleteUnLockedStatement(): void { - self::markSkippedForCIElasticsearchUnavailable(); + $originalStatement = StatementFactory::createOne(); + $testStatement = StatementFactory::createOne(['original' => $originalStatement]); + $this->enablePermissions(['feature_statement_assignment']); - $statementId = $this->getStatementReference('testStatement1')->getId(); + $statementId = $testStatement->getId(); $statement = $this->statementService->getStatement($statementId); static::assertNull($statement->getAssignee()); From 4d532c0a1510c1f40e6d09a627cc864443674abc Mon Sep 17 00:00:00 2001 From: salisdemos <40487461+salisdemos@users.noreply.github.com> Date: Tue, 23 Jul 2024 09:13:00 +0200 Subject: [PATCH 011/173] fix(refs DPLAN-11488): Make departments updateable again (3432) --- .../user/DpUserList/DpUserFormFields.vue | 68 ++++++++++--------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/client/js/components/user/DpUserList/DpUserFormFields.vue b/client/js/components/user/DpUserList/DpUserFormFields.vue index 63bedf64f8..f44697205b 100644 --- a/client/js/components/user/DpUserList/DpUserFormFields.vue +++ b/client/js/components/user/DpUserList/DpUserFormFields.vue @@ -86,13 +86,13 @@ data-cy="department" :disabled="noOrgaSelected" :label="{ - hint: localUser.relationships.orga.data.id ? '' : Translator.trans('organisation.select.first'), + hint: localUser.relationships.orga?.data?.id ? '' : Translator.trans('organisation.select.first'), text: Translator.trans('department') }" :options="departmentSelectOptions" required - :selected="localUser.relationships.department.data.id" - @change="changeUserDepartment" /> + :selected="localUser.relationships.department.data?.id || ''" + @select="changeUserDepartment" /> @@ -197,6 +197,7 @@ export default { data () { return { + allowedRolesForOrga: [], currentUserOrga: { id: '', name: '', @@ -232,24 +233,6 @@ export default { initialOrgaSuggestions: 'getOrgaSuggestions' }), - /** - * - user is not set: all roles - * - user is set: roles for current organisation - */ - allowedRolesForOrga () { - let allowedRoles - - if (this.currentUserOrga.id === '') { - allowedRoles = this.rolesInRelationshipFormat - } else if (hasOwnProp(this.organisations[this.currentUserOrga.id].relationships, 'allowedRoles')) { - allowedRoles = Object.values(this.organisations[this.currentUserOrga.id].relationships.allowedRoles.list()) - } else { - allowedRoles = this.getOrgaAllowedRoles(this.currentUserOrga.id) - } - - return allowedRoles - }, - currentOrgaDepartments () { const departments = sortAlphabetically(Object.values(this.currentUserOrga.departments), 'name') const noDepartmentIdx = departments.findIndex(el => el.name === Translator.trans('department.none')) @@ -267,7 +250,7 @@ export default { }, isDepartmentSet () { - return this.localUser.relationships.department.data.id !== '' + return this.localUser.relationships.department.data?.id !== '' }, isManagingSingleOrganisation () { @@ -295,8 +278,7 @@ export default { this.emitUserUpdate('relationships.roles.data', role, 'roles', 'add') }, - changeUserDepartment (e) { - const departmentId = e.target.value + changeUserDepartment (departmentId) { this.localUser.relationships.department.data = { id: departmentId, type: 'department' @@ -323,7 +305,7 @@ export default { * Fetch organisation of user or, in DpCreateItem, of currently logged-in user */ fetchCurrentOrganisation () { - const orgaId = this.user.relationships.orga && this.user.relationships.orga.data.id + const orgaId = this.user.relationships.orga?.data?.id ? this.user.relationships.orga.data.id : this.presetUserOrgaId if (orgaId !== '') { @@ -345,12 +327,30 @@ export default { return dpApi.get(url, { include: ['allowedRoles', 'departments'].join() }) }, + /** + * - user is not set: all roles + * - user is set: roles for current organisation + */ + getAllowedRolesForOrga () { + let allowedRoles + + if (this.currentUserOrga.id === '') { + allowedRoles = this.rolesInRelationshipFormat + } else if (this.organisations[this.currentUserOrga.id].relationships?.allowedRoles?.data) { + allowedRoles = Object.values(this.organisations[this.currentUserOrga.id].relationships.allowedRoles.list()) + } else { + allowedRoles = this.getOrgaAllowedRoles(this.currentUserOrga.id) + } + + return allowedRoles + }, + getOrgaAllowedRoles (orgaId) { let allowedRoles = this.rolesInRelationshipFormat this.fetchOrgaById(orgaId).then((orga) => { this.setOrga(orga.data.data) - if (hasOwnProp(this.organisations[this.currentUserOrga.id].relationships, 'allowedRoles')) { + if (this.currentUserOrga.id && hasOwnProp(this.organisations[this.currentUserOrga.id].relationships, 'allowedRoles')) { allowedRoles = this.organisations[this.currentUserOrga.id].relationships.allowedRoles.list() } }) @@ -365,9 +365,13 @@ export default { handleUndefinedRelationships (types) { types.forEach(type => { if (typeof this.localUser.relationships[type] === 'undefined' || this.localUser.relationships[type] === null) { - this.localUser.relationships[type] = { - data: { - id: '' + if (type === 'roles') { + this.localUser.relationships[type] = { + data: [] + } + } else { + this.localUser.relationships[type] = { + data: {} } } } @@ -411,7 +415,7 @@ export default { this.localUser.relationships.department.data = { id: defaultDepartment.id, type: defaultDepartment.type } }, - setInitialOrgaData () { + async setInitialOrgaData () { /* * Fetch organisation only * - in DpUserListItem (= isUserSet), not in DpCreateItem (= isUserSet === false) @@ -425,6 +429,8 @@ export default { } }) } + + this.allowedRolesForOrga = await this.getAllowedRolesForOrga() }, setOrganisationDepartments (departments) { @@ -497,7 +503,7 @@ export default { created () { this.localUser = JSON.parse(JSON.stringify(this.user)) - this.handleUndefinedRelationships(['orga', 'department']) + this.handleUndefinedRelationships(['orga', 'department', 'roles']) }, mounted () { From 17616495aaef5c421ebd0573843198e283928d87 Mon Sep 17 00:00:00 2001 From: salisdemos <40487461+salisdemos@users.noreply.github.com> Date: Tue, 23 Jul 2024 09:16:50 +0200 Subject: [PATCH 012/173] fix(refs DPLAN-11803): properly calculate inParticipation phase (3405) all phases with a "write" permission are participation phases. So we don't have to rely of the string spelling. (cherry picked from commit 22634ae9ee56de1b8d2af855389ee2da4dadb6c2) --- .../procedure/basicSettings/AutoSwitchProcedurePhaseForm.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/js/components/procedure/basicSettings/AutoSwitchProcedurePhaseForm.vue b/client/js/components/procedure/basicSettings/AutoSwitchProcedurePhaseForm.vue index adb9f0c252..7bbf371103 100644 --- a/client/js/components/procedure/basicSettings/AutoSwitchProcedurePhaseForm.vue +++ b/client/js/components/procedure/basicSettings/AutoSwitchProcedurePhaseForm.vue @@ -209,7 +209,9 @@ export default { }, showAutoSwitchToAnalysisHint () { - return hasPermission('feature_auto_switch_to_procedure_end_phase') && this.autoSwitchPhase && ['participation', 'earlyparticipation', 'anotherparticipation'].includes(this.selectedPhase) + const isInParticipation = this.phaseOptions.find(option => option.value === this.selectedPhase)?.permission === 'write' + + return hasPermission('feature_auto_switch_to_procedure_end_phase') && this.autoSwitchPhase && isInParticipation }, startDateId () { From e5a91747349d4ddfee43325482e665c1929a7b39 Mon Sep 17 00:00:00 2001 From: Steffi <101879352+gruenbergerdemos@users.noreply.github.com> Date: Tue, 23 Jul 2024 10:26:13 +0200 Subject: [PATCH 013/173] bug (refs DPLAN-12079): Remove comments relationship (3468) - Comments need to be removed as updating them is technically not supported After completing the request, they are added again to the store to be able to display them. - Fix check: {} will always be truthy even if comments is undefined or null --- .../SegmentLocationMap.vue | 28 +++++++++++++++++++ .../StatementSegment.vue | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/client/js/components/procedure/StatementSegmentsList/SegmentLocationMap.vue b/client/js/components/procedure/StatementSegmentsList/SegmentLocationMap.vue index 003e25ed55..2740d843fc 100644 --- a/client/js/components/procedure/StatementSegmentsList/SegmentLocationMap.vue +++ b/client/js/components/procedure/StatementSegmentsList/SegmentLocationMap.vue @@ -241,6 +241,22 @@ export default { }) }, + /** + * Restore non-updatable comments from segments relationships after update request + */ + restoreComments (comments) { + if (comments) { + const segmentWithComments = { + ...this.segment, + relationships: { + ...this.segment.relationships, + comments: comments + } + } + this.setItem({ ...segmentWithComments }) + } + }, + save () { this.setItem({ ...this.segment, @@ -249,6 +265,15 @@ export default { polygon: JSON.stringify(this.featuresObject) } }) + const comments = this.segment.relationships.comments ? { ...this.segment.relationships.comments } : null + + /** + * Comments need to be removed as updating them is technically not supported + * After completing the request, they are added again to the store to be able to display them + */ + if (this.segment.relationships.comments) { + delete this.segment.relationships.comments + } return this.saveSegmentAction(this.segmentId) .then(checkResponse) @@ -258,6 +283,9 @@ export default { .catch(() => { dplan.notify.error(Translator.trans('error.changes.not.saved')) }) + .finally(() => { + this.restoreComments(comments) + }) }, setInitExtent () { diff --git a/client/js/components/procedure/StatementSegmentsList/StatementSegment.vue b/client/js/components/procedure/StatementSegmentsList/StatementSegment.vue index ac32406e66..a43778d435 100644 --- a/client/js/components/procedure/StatementSegmentsList/StatementSegment.vue +++ b/client/js/components/procedure/StatementSegmentsList/StatementSegment.vue @@ -655,7 +655,7 @@ export default { }, save () { - const comments = { ...this.segment.relationships.comments } || null + const comments = this.segment.relationships.comments ? { ...this.segment.relationships.comments } : null this.updateRelationships() return this.saveSegmentAction(this.segment.id) From 5c55c9c89e6e86a4e7098c0899bc27fdc566dbb2 Mon Sep 17 00:00:00 2001 From: salisdemos <40487461+salisdemos@users.noreply.github.com> Date: Tue, 23 Jul 2024 14:01:19 +0200 Subject: [PATCH 014/173] fix(refs DPLAN-11449): Print warning for short segments (3470) --- .../splitStatement/SplitStatementView.vue | 17 ++++++++----- client/js/lib/prosemirror/commands.js | 24 ++++++++++++++++++- translations/messages+intl-icu.de.yml | 1 + 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/client/js/components/statement/splitStatement/SplitStatementView.vue b/client/js/components/statement/splitStatement/SplitStatementView.vue index 53df1e460c..21d4f5e8e3 100644 --- a/client/js/components/statement/splitStatement/SplitStatementView.vue +++ b/client/js/components/statement/splitStatement/SplitStatementView.vue @@ -663,14 +663,14 @@ export default { }, immediatelyDeleteSegment (segmentId) { - const idToDelete = segmentId - const segment = this.segmentById(idToDelete) - this.ignoreProsemirrorUpdates = true + const segment = this.segmentById(segmentId) const { state } = this.prosemirror.view const tr = removeRange(state, segment.charStart, segment.charEnd) + + this.ignoreProsemirrorUpdates = true this.prosemirror.view.dispatch(tr) this.ignoreProsemirrorUpdates = false - this.deleteSegmentAction(idToDelete) + this.deleteSegmentAction(segmentId) this.isSegmentDraftUpdated = true this.setCurrentTime() }, @@ -752,10 +752,15 @@ export default { * the range changes locally, saveSegmentsDrafts will save the new json to our API. */ save () { - applySelectionChange(this.prosemirror.view, this.prosemirror.keyAccess.editStateTrackerKey, this.prosemirror.keyAccess.rangeTrackerKey) + const validSegment = applySelectionChange(this.prosemirror.view, this.prosemirror.keyAccess.editStateTrackerKey, this.prosemirror.keyAccess.rangeTrackerKey) + + if (!validSegment) { + return + } + this.saveSegmentsDrafts(true) - this.disableEditMode() this.isSegmentDraftUpdated = true + this.disableEditMode() this.setCurrentTime() }, diff --git a/client/js/lib/prosemirror/commands.js b/client/js/lib/prosemirror/commands.js index ed05ca5e5c..9217c36867 100644 --- a/client/js/lib/prosemirror/commands.js +++ b/client/js/lib/prosemirror/commands.js @@ -30,15 +30,25 @@ const applySelectionChange = (view, editStateTrackerKey, rangeTrackerKey) => { const nodes = flattenNode(state.doc) const marks = getMarks(nodes, 'rangeselection', 'rangeType') const { from, to } = marks.selection + let tr = state.tr + + if (to - from < 10) { + dplan.notify.notify('warning', Translator.trans('warning.segment.too_short')) + dispatch(tr) + + return false + } /** * This replaces the old range with a new range. It also removes any ranges which might now be covered by the new range. */ - let tr = state.tr tr = removeRange(state, range.from, range.to, tr) tr = replaceRange(state, from, to, { rangeId: rangeId, isActive: true, isConfirmed: true }, tr) tr = disableRangeEdit(view, editStateTrackerKey, tr) + dispatch(tr) + + return true } /** @@ -58,6 +68,7 @@ const replaceRange = (state, from, to, rangeAttrs, tr = false) => { * occur. If no transaction is passed, we will just use the current state. */ const mappedState = tr?.doc || state.doc + if (splitsExistingRange(from, to, mappedState)) { throw new Error('Ranges can not be split in two parts.') } @@ -78,7 +89,9 @@ const replaceRange = (state, from, to, rangeAttrs, tr = false) => { const removeRange = (state, from, to, tr = false) => { const rangeMarkType = state.config.schema.marks.range let transaction = tr || state.tr + transaction = transaction.removeMark(from, to, rangeMarkType) + return transaction } @@ -97,10 +110,12 @@ const removeMarkByName = (state, markName, markAttr, tr = false) => { const marks = getMarks(nodes, markName, markAttr) const transaction = tr || state.tr const markType = state.config.schema.marks[markName] + Object.values(marks).forEach(mark => { const { from, to } = mark transaction.removeMark(from, to, markType) }) + return transaction } @@ -119,6 +134,7 @@ const setRangeEditingState = (view, rangeTrackerKey, editingDecorationsKey) => ( let tr = replaceRange(state, from, to, { rangeId, isConfirmed, isActive: editingState }) tr = tr.setMeta(editingDecorationsKey, { editing: editingState, from, to, id }) + dispatch(tr) } @@ -158,15 +174,18 @@ const replaceMarkInRange = (state, from, to, markKey, markAttrs, tr = false) => } transaction = transaction.removeMark(from, to, markType) + const newMark = markType.create(newAttrs) transaction = transaction.addMark(from, to, newMark) const markCollection = getMarks(flattenNode(transaction.doc), markKey, 'pmId') const currentMarkCollection = markCollection[pmId] + currentMarkCollection.marks.forEach(m => { transaction = transaction.removeMark(m.from, m.to, markType) const uniqueMark = markType.create({ ...newAttrs, pmId: uuidv4() }) transaction = transaction.addMark(m.from, m.to, uniqueMark) }) + return transaction } @@ -181,6 +200,7 @@ const replaceMarkInRange = (state, from, to, markKey, markAttrs, tr = false) => const setRange = (view) => (from, to, rangeAttrs) => { const { state, dispatch } = view const tr = replaceRange(state, from, to, rangeAttrs) + dispatch(tr) } @@ -217,6 +237,7 @@ const makeDecoration = (id, pos, isActive = false) => { const genEditingDecorations = (state, from, to, id, activePosition = null) => { const start = Decoration.widget(from, makeDecoration(id, from, activePosition === from), { id }) const end = Decoration.widget(to, makeDecoration(id, to, activePosition === to), { id }) + return DecorationSet.create(state.doc, [ start, end @@ -269,6 +290,7 @@ const disableRangeEdit = (view, editStateTrackerKey, tr = null) => { const { state } = view let transaction = tr || state.tr transaction = transaction.setMeta(editStateTrackerKey, 'stop-editing') + return transaction } diff --git a/translations/messages+intl-icu.de.yml b/translations/messages+intl-icu.de.yml index 6aeabd7583..4b6b586b2f 100644 --- a/translations/messages+intl-icu.de.yml +++ b/translations/messages+intl-icu.de.yml @@ -4017,6 +4017,7 @@ warning.procedure.proposal.notfound: "Der Verfahrensvorschlag konnte nicht gefun warning.resistFingerPrinting: "Um alle Funktionen dieser Seite nutzen zu können, deaktivieren Sie in Ihrem Browser die Einstellung \"privacy.resistFingerprinting\" unter \"about:config\"." warning.search.query.invalid: "Folgende Zeichen haben eine Sonderfunktion und können deshalb nicht ohne Weiteres verwendet werden:

. ? + * | { } [ ] ( ) \" \\ # @ & < > ~

Sonderzeichen können aufgrund ihrer besonderen Funktion nicht gefunden werden.

Wenn ein Sonderzeichen in Ihrer Suche enthalten ist, schreiben Sie \"\\\" vor das Zeichen.
Haben Sie z.B. ein \"?\" in Ihrer Suche, müssen Sie \"\\?\" eingeben." warning.segment.needLock.generic: "Dieser Abschnitt ist zur Zeit in Bearbeitung. Um ihn zu bearbeiten, müssen Sie den Abschnitt zunächst für sich beanspruchen." +warning.segment.too_short: "Der Abschnitt ist zu kurz. Bitte erweitern Sie ihn, um ihn speichern zu können." warning.select.entries: "Bitte wählen Sie Einträge aus." warning.select.entry: "Bitte wählen Sie einen Eintrag aus." warning.selected.boilerplates.delete: "Es konnten nicht alle ausgewählten Textbausteine gelöscht werden." From 7ca746f3d221eddbd4a99e22971d7bd0b7186a2a Mon Sep 17 00:00:00 2001 From: MoritzMandler <89914798+MoritzMandler@users.noreply.github.com> Date: Tue, 23 Jul 2024 14:54:39 +0200 Subject: [PATCH 015/173] fix(refs DPLAN-12009): Fix file copies on STN copy to different procedure (3431) * fix(refs DPLAN-12009): Fix file copies on STN copy to different procedrue * style: Apply php-cs-fixer * fix(refs DPLAN-12009): fix file assignment to procedure on statement-Move / Copy into different procedures * style: Apply php-cs-fixer * fix(refs DPLAN-12009): That one was for debug purposes only. * fix(refs DPLAN-12009): We need to copy the file entity and set this copy as the filereference within the new statement attachment. * f DPLAN 12009 remove unnecessary code Signed-off-by: Hamza Bakri * f DPLAN 12009 remove comment Signed-off-by: Hamza Bakri * f DPLAN 12009 make 'copyFile' method public Signed-off-by: Hamza Bakri * f DPLAN 12009 remove unused array Signed-off-by: Hamza Bakri --------- Signed-off-by: Hamza Bakri Co-authored-by: Florian Salis Co-authored-by: Demos-CI Co-authored-by: Hamza Bakri --- .../Controller/Platform/FileController.php | 2 +- .../DemosPlanCoreBundle/Logic/FileService.php | 16 +++++------ .../Logic/Statement/StatementCopier.php | 3 ++- .../Logic/Statement/StatementMover.php | 18 +++++++++++-- .../Logic/StatementAttachmentService.php | 27 ++++++++++--------- .../Repository/FileRepository.php | 26 ++++++++++++++---- .../Repository/StatementRepository.php | 13 +++++---- 7 files changed, 69 insertions(+), 36 deletions(-) diff --git a/demosplan/DemosPlanCoreBundle/Controller/Platform/FileController.php b/demosplan/DemosPlanCoreBundle/Controller/Platform/FileController.php index ff8299b41d..92df04e45a 100644 --- a/demosplan/DemosPlanCoreBundle/Controller/Platform/FileController.php +++ b/demosplan/DemosPlanCoreBundle/Controller/Platform/FileController.php @@ -66,7 +66,7 @@ protected function prepareResponseWithHash(FileService $fileService, string $has { $fs = new Filesystem(); // @improve T14122 - $file = $fileService->getFileInfo($hash); + $file = $fileService->getFileInfo($hash, $procedureId); // ensure that procedure access check matches file procedure if (!$this->isValidProcedure($procedureId, $file, $strictCheck)) { diff --git a/demosplan/DemosPlanCoreBundle/Logic/FileService.php b/demosplan/DemosPlanCoreBundle/Logic/FileService.php index c40be222bf..44a46696fc 100644 --- a/demosplan/DemosPlanCoreBundle/Logic/FileService.php +++ b/demosplan/DemosPlanCoreBundle/Logic/FileService.php @@ -104,9 +104,9 @@ public function get($hash) * * @throws Exception */ - public function getFileInfo($hash): FileInfo + public function getFileInfo($hash, ?string $procedureId = null): FileInfo { - $file = $this->fileRepository->getFileInfo($hash); + $file = $this->fileRepository->getFileInfo($hash, $procedureId); if (null !== $file) { $path = $file->getPath(); @@ -340,10 +340,10 @@ public function getNotFoundImagePath() public function saveTemporaryFile( string $filePath, string $fileName, - string $userId = null, - string $procedureId = null, + ?string $userId = null, + ?string $procedureId = null, ?string $virencheck = FileServiceInterface::VIRUSCHECK_SYNC, - string $hash = null + ?string $hash = null ): File { $dplanFile = new File(); $symfonyFile = new \Symfony\Component\HttpFoundation\File\File($filePath); @@ -605,7 +605,7 @@ public function createCopyOfFile(string $fileString, string $procedureId): ?File * @throws InvalidDataException * @throws Throwable */ - public function copyByFileString($fileString, string $procedureId = null): ?File + public function copyByFileString($fileString, ?string $procedureId = null): ?File { $file = $this->getFileInfoFromFileString($fileString); @@ -617,7 +617,7 @@ public function copyByFileString($fileString, string $procedureId = null): ?File * * @throws InvalidDataException|Throwable */ - public function copy(?string $hash, string $targetProcedureId = null): ?File + public function copy(?string $hash, ?string $targetProcedureId = null): ?File { $fileToCopy = $this->get($hash); if (!$fileToCopy instanceof File) { @@ -724,7 +724,7 @@ public function checkMimeTypeAllowed($mimeType, $temporaryFilePath) * * @return string */ - protected function moveFile(\Symfony\Component\HttpFoundation\File\File $file, $path = '', string $existingHash = null) + protected function moveFile(\Symfony\Component\HttpFoundation\File\File $file, $path = '', ?string $existingHash = null) { // Generate a unique name for the file before saving it $hash = $existingHash ?? $this->createHash(); diff --git a/demosplan/DemosPlanCoreBundle/Logic/Statement/StatementCopier.php b/demosplan/DemosPlanCoreBundle/Logic/Statement/StatementCopier.php index 683378c30e..32a9518429 100644 --- a/demosplan/DemosPlanCoreBundle/Logic/Statement/StatementCopier.php +++ b/demosplan/DemosPlanCoreBundle/Logic/Statement/StatementCopier.php @@ -18,7 +18,6 @@ use DemosEurope\DemosplanAddon\Contracts\MessageBagInterface; use DemosEurope\DemosplanAddon\Contracts\PermissionsInterface; use demosplan\DemosPlanCoreBundle\Doctrine\Generator\NCNameGenerator; -use demosplan\DemosPlanCoreBundle\Entity\FileContainer; use demosplan\DemosPlanCoreBundle\Entity\Procedure\Procedure; use demosplan\DemosPlanCoreBundle\Entity\Statement\County; use demosplan\DemosPlanCoreBundle\Entity\Statement\Municipality; @@ -39,6 +38,7 @@ use demosplan\DemosPlanCoreBundle\Logic\Report\ReportService; use demosplan\DemosPlanCoreBundle\Logic\Report\StatementReportEntryFactory; use demosplan\DemosPlanCoreBundle\Logic\StatementAttachmentService; +use demosplan\DemosPlanCoreBundle\Repository\FileContainerRepository; use demosplan\DemosPlanCoreBundle\Repository\StatementRepository; use demosplan\DemosPlanCoreBundle\Traits\DI\RefreshElasticsearchIndexTrait; use Doctrine\ORM\EntityNotFoundException; @@ -61,6 +61,7 @@ public function __construct( private readonly CurrentUserInterface $currentUser, private readonly ElementsService $elementService, private readonly FileService $fileService, + private readonly FileContainerRepository $fileContainerRepository, IndexManager $elasticsearchIndexManager, private readonly MessageBagInterface $messageBag, private readonly PermissionsInterface $permissions, diff --git a/demosplan/DemosPlanCoreBundle/Logic/Statement/StatementMover.php b/demosplan/DemosPlanCoreBundle/Logic/Statement/StatementMover.php index 715cbe7ad4..9493010276 100644 --- a/demosplan/DemosPlanCoreBundle/Logic/Statement/StatementMover.php +++ b/demosplan/DemosPlanCoreBundle/Logic/Statement/StatementMover.php @@ -28,6 +28,7 @@ use demosplan\DemosPlanCoreBundle\Logic\EntityContentChangeService; use demosplan\DemosPlanCoreBundle\Logic\Report\ReportService; use demosplan\DemosPlanCoreBundle\Logic\Report\StatementReportEntryFactory; +use demosplan\DemosPlanCoreBundle\Repository\FileContainerRepository; use demosplan\DemosPlanCoreBundle\Repository\StatementRepository; use Doctrine\DBAL\ConnectionException; use Doctrine\DBAL\Exception; @@ -45,6 +46,7 @@ class StatementMover extends CoreService public function __construct( private readonly EntityManagerInterface $entityManager, private readonly ElementsService $elementsService, + private readonly FileContainerRepository $fileContainerRepository, private readonly PermissionsInterface $permissions, private readonly MessageBagInterface $messageBag, private readonly StatementService $statementService, @@ -54,7 +56,8 @@ public function __construct( private readonly EntityContentChangeService $entityContentChangeService, private readonly StatementReportEntryFactory $statementReportEntryFactory, private readonly ReportService $reportService, - private readonly StatementCopier $statementCopier + private readonly StatementCopier $statementCopier, + private readonly StatementRepository $statementRepository ) { $this->logger = $logger; } @@ -186,11 +189,22 @@ public function moveStatementToProcedure( $lockedByAssignment = $this->statementService->isStatementObjectLockedByAssignment($statementToMove); if (!$lockedByAssignment) { $statementToMove->setAssignee(null); + foreach ($statementToMove->getAttachments() as $attachment) { + $file = $attachment->getFile(); + $file->setProcedure($targetProcedure); + $attachment->setFile($file); + } + $statementFileContainers = $this->fileContainerRepository->getStatementFileContainers($statementToMove->getId()); + foreach ($statementFileContainers as $fileContainer) { + $file = $this->statementRepository->copyFile($fileContainer->getFile(), $statementToMove); + $fileContainer->setFile($file); + $this->fileContainerRepository->updateObject($fileContainer); + } $updatedStatementToMove = $this->statementService->updateStatementFromObject( $statementToMove, true ); - // update $originalStatement to persist changes: + $updatedOriginalStatementToMove = $this->statementService->updateStatementFromObject( $copyOfOriginalStatementToMove, true, diff --git a/demosplan/DemosPlanCoreBundle/Logic/StatementAttachmentService.php b/demosplan/DemosPlanCoreBundle/Logic/StatementAttachmentService.php index db005f9a73..35daef2b75 100644 --- a/demosplan/DemosPlanCoreBundle/Logic/StatementAttachmentService.php +++ b/demosplan/DemosPlanCoreBundle/Logic/StatementAttachmentService.php @@ -14,6 +14,7 @@ use demosplan\DemosPlanCoreBundle\Entity\Statement\Statement; use demosplan\DemosPlanCoreBundle\Entity\StatementAttachment; use demosplan\DemosPlanCoreBundle\Repository\StatementAttachmentRepository; +use demosplan\DemosPlanCoreBundle\Repository\StatementRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\OptimisticLockException; @@ -21,8 +22,10 @@ class StatementAttachmentService extends CoreService { - public function __construct(private readonly StatementAttachmentRepository $attachmentRepository) - { + public function __construct( + private readonly StatementAttachmentRepository $attachmentRepository, + private readonly StatementRepository $statementRepository + ) { } public function createOriginalAttachment(Statement $statement, File $file): StatementAttachment @@ -65,26 +68,26 @@ public function deleteOriginalAttachment(Statement $statement): Statement * * @return Collection */ - public function copyAttachmentEntries(Collection $originalAttachments, Statement $targetStatement): Collection - { + public function copyAttachmentEntries( + Collection $originalAttachments, + Statement $targetStatement, + ): Collection { $copiedAttachments = new ArrayCollection(); foreach ($originalAttachments as $originalAttachment) { $copiedAttachments->add($this->copyToStatement( $originalAttachment, - $targetStatement + $targetStatement, )); } return $copiedAttachments; } - private function copyToStatement(StatementAttachment $attachment, Statement $statement): StatementAttachment - { - return $this->createAttachment( - $statement, - $attachment->getFile(), - $attachment->getType() - ); + private function copyToStatement( + StatementAttachment $attachment, + Statement $statement, + ): StatementAttachment { + return $this->statementRepository->copyAttachment($statement, $attachment); } /** diff --git a/demosplan/DemosPlanCoreBundle/Repository/FileRepository.php b/demosplan/DemosPlanCoreBundle/Repository/FileRepository.php index fb1da883cf..7569a18160 100644 --- a/demosplan/DemosPlanCoreBundle/Repository/FileRepository.php +++ b/demosplan/DemosPlanCoreBundle/Repository/FileRepository.php @@ -30,7 +30,7 @@ class FileRepository extends FluentRepository implements ArrayInterface, ObjectI /** * Hole Infos zum File. */ - public function getFileInfo(string $hash): ?File + public function getFileInfo(string $hash, ?string $procedureId = null): ?File { // Der übergebene Hash ist der Ident der Datenbank // Die Spalte Hash bezeichnet den Namen, unter dem die Datei auf dem @@ -38,14 +38,30 @@ public function getFileInfo(string $hash): ?File /** @var File|null $result */ $result = $this->findOneBy(['ident' => $hash, 'deleted' => false]); - + if (null !== $result) { + return $result; + } // As ident and hash are historically really strangely used mistakes happened // be kind and try to find via hash when nothing was found by ident - if (null === $result) { - $result = $this->findOneBy(['hash' => $hash, 'deleted' => false]); + + // T36732 In case the same physical file is used for multiple procedures + // There will be a fileInfo entity for every reference to a physical file. + // They share the same hash - but not necessarily the procedure + // - so the findOneBy method for the kindly supported hash is insufficient here. + $fileInfos = $this->findBy(['hash' => $hash, 'deleted' => false, 'procedure' => $procedureId]); + $fileInfosCount = count($fileInfos); + // easy - has to be this one + if (0 < $fileInfosCount) { + return reset($fileInfos); + } + $fileInfos = $this->findBy(['hash' => $hash, 'deleted' => false, 'procedure' => null]); + $fileInfosCount = count($fileInfos); + // tried our best to return the correct fileInfo - but if not successful until here - just return a matching hash + if (0 === $fileInfosCount) { + return $this->findOneBy(['hash' => $hash, 'deleted' => false]); } - return $result; + return reset($fileInfos); } /** diff --git a/demosplan/DemosPlanCoreBundle/Repository/StatementRepository.php b/demosplan/DemosPlanCoreBundle/Repository/StatementRepository.php index c2876cdb63..a9e8bbb679 100644 --- a/demosplan/DemosPlanCoreBundle/Repository/StatementRepository.php +++ b/demosplan/DemosPlanCoreBundle/Repository/StatementRepository.php @@ -138,7 +138,7 @@ public function getFilesByProcedureId($procedureId) /** * Add new ClusterStatement, what basically is a Statement with a cluster of Statements. * - * @return statement - headStatement of the created statement-cluster + * @return Statement - headStatement of the created statement-cluster * * @throws Exception */ @@ -479,7 +479,7 @@ public function getSegmentableStatementsCount(string $procedureId, User $user): /** * @return array */ - public function getEntities(array $conditions, array $sortMethods, int $offset = 0, int $limit = null): array + public function getEntities(array $conditions, array $sortMethods, int $offset = 0, ?int $limit = null): array { return parent::getEntities($conditions, $sortMethods, $offset, $limit); } @@ -1011,7 +1011,7 @@ public function generateObjectValues($statement, array $data) * StatementVotes, which are not contained in $votesToSet, will be deleted. * StatementVotes, which are contained in $votesToSet, will be created if not existing, or updated. * - * @param statement $statement - related Statement + * @param Statement $statement - related Statement * @param array|Collection $votesToSet - StatementVotes to set on the given Statement * * @return Collection @@ -1783,7 +1783,7 @@ public function isInternIdUniqueForProcedure($internId, $procedureId): bool public function copyOriginalStatement( Statement $originalToCopy, Procedure $targetProcedure, - GdprConsent $gdprConsentToSet = null, + ?GdprConsent $gdprConsentToSet = null, $internIdToSet = null ): Statement { if (!$originalToCopy->isOriginal()) { @@ -1820,7 +1820,7 @@ public function copyOriginalStatement( $newOriginalStatement->setSubmit($originalToCopy->getSubmitObject()->add(new DateInterval('PT1S'))); $newStatementMeta = clone $originalToCopy->getMeta(); $newOriginalStatement->setMeta($newStatementMeta); - + $newOriginalStatement->setProcedure($targetProcedure); $newOriginalStatement->setChildren(null); /** @@ -1869,7 +1869,6 @@ public function copyOriginalStatement( } $newOriginalStatement->setExternId($newExternId); - $newOriginalStatement->setProcedure($targetProcedure); if ($originalToCopy->getProcedureId() !== $targetProcedure->getId()) { // remove all tags, because procedure specific -> impossible to keep: @@ -2057,7 +2056,7 @@ public function copyFileContainer(FileContainer $sourceFileContainer, Statement return $statementFileContainer; } - private function copyFile(File $sourceFile, Statement $targetStatement): File + public function copyFile(File $sourceFile, Statement $targetStatement): File { $fileCopy = $this->getFileRepository()->copyFile($sourceFile); $fileCopy->setProcedure($targetStatement->getProcedure()); From 79da27c6eecfb4d691c2d76d121371c9d99f0fae Mon Sep 17 00:00:00 2001 From: Steffi <101879352+gruenbergerdemos@users.noreply.github.com> Date: Wed, 24 Jul 2024 10:13:40 +0200 Subject: [PATCH 016/173] bug (refs DPLAN-1916): show warning (#3478) * bug (refs DPLAN-1916): Only show warning if condition is met We need this warning only if segment has draft list. * bug: Only add listener if ref is defined We only need to add the click listener if segments exist. --- .../js/components/procedure/SegmentsList/SegmentsList.vue | 8 +++++--- .../StatementSegmentsList/StatementSegmentsEdit.vue | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/client/js/components/procedure/SegmentsList/SegmentsList.vue b/client/js/components/procedure/SegmentsList/SegmentsList.vue index 6fd9f1ae91..aedf0c82be 100644 --- a/client/js/components/procedure/SegmentsList/SegmentsList.vue +++ b/client/js/components/procedure/SegmentsList/SegmentsList.vue @@ -572,9 +572,11 @@ export default { }) .finally(() => { this.isLoading = false - this.$nextTick(() => { - this.$refs.imageModal.addClickListener(this.$refs.dataTable.$el.querySelectorAll('img')) - }) + if (this.items.length > 0) { + this.$nextTick(() => { + this.$refs.imageModal.addClickListener(this.$refs.dataTable.$el.querySelectorAll('img')) + }) + } }) }, diff --git a/client/js/components/procedure/StatementSegmentsList/StatementSegmentsEdit.vue b/client/js/components/procedure/StatementSegmentsList/StatementSegmentsEdit.vue index 47388b1431..e48c3bf1ba 100644 --- a/client/js/components/procedure/StatementSegmentsList/StatementSegmentsEdit.vue +++ b/client/js/components/procedure/StatementSegmentsList/StatementSegmentsEdit.vue @@ -86,6 +86,7 @@ v-else class="border space-inset-s"> From 2bca11c7819fb49b3f303da5726405291ed1c5be Mon Sep 17 00:00:00 2001 From: Steffi <101879352+gruenbergerdemos@users.noreply.github.com> Date: Wed, 24 Jul 2024 10:27:20 +0200 Subject: [PATCH 017/173] bug (refs DPLAN-12137): Change header field translation (#3480) Change translation to avoid confusion. --- client/js/components/procedure/SegmentsList/SegmentsList.vue | 2 +- translations/messages+intl-icu.de.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/client/js/components/procedure/SegmentsList/SegmentsList.vue b/client/js/components/procedure/SegmentsList/SegmentsList.vue index aedf0c82be..f00b7166c8 100644 --- a/client/js/components/procedure/SegmentsList/SegmentsList.vue +++ b/client/js/components/procedure/SegmentsList/SegmentsList.vue @@ -365,7 +365,7 @@ export default { }, headerFieldsAvailable: [ { field: 'externId', label: Translator.trans('id') }, - { field: 'statementStatus', label: Translator.trans('status') }, + { field: 'statementStatus', label: Translator.trans('statement.status') }, { field: 'internId', label: Translator.trans('internId.shortened'), colWidth: '150px' }, { field: 'submitter', label: Translator.trans('submitter') }, { field: 'address', label: Translator.trans('address') }, diff --git a/translations/messages+intl-icu.de.yml b/translations/messages+intl-icu.de.yml index 4b6b586b2f..5036a4cfbd 100644 --- a/translations/messages+intl-icu.de.yml +++ b/translations/messages+intl-icu.de.yml @@ -3390,6 +3390,7 @@ statement.similarStatementSubmitters.hint: "Falls von weiteren Einreichenden inh statement.specification: "Angaben zur Stellungnahme" statement.split.complete: "Aufteilen abschließen" statement.split.complete.confirm: "Sind Sie sicher, dass Sie die Stellungnahme vollständig aufgeteilt haben? Sie können die gleiche Stellungnahme zu einem späteren Zeitpunkt nicht erneut bearbeiten." +statement.status: "STN-Status" statement.submission.default: "Standard-Stellungnahmeprozess" statement.submission.shorthand: "Verkürzter Stellungnahmeprozess" statement.submission.type: "Stellungnahmeabgabeprozess" From ec6a780e508bf5065dbcfc7efef6ba135680649a Mon Sep 17 00:00:00 2001 From: salisdemos <40487461+salisdemos@users.noreply.github.com> Date: Wed, 24 Jul 2024 10:33:59 +0200 Subject: [PATCH 018/173] fix(refs DPLAN-941): Adjust Filter to find names with Blanks (3477) --- .../components/user/DpUserList/DpUserList.vue | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/client/js/components/user/DpUserList/DpUserList.vue b/client/js/components/user/DpUserList/DpUserList.vue index 8113b27dc4..6427bca700 100644 --- a/client/js/components/user/DpUserList/DpUserList.vue +++ b/client/js/components/user/DpUserList/DpUserList.vue @@ -226,28 +226,31 @@ export default { this.isLoading = true page = page || this.currentPage const userFilter = { - firstnameFilter: { + name: { + group: { + conjunction: 'OR' + } + } + } + + this.searchValue.split(' ').filter(Boolean).forEach((value, index) => { + userFilter[`firstnameFilter${index}`] = { condition: { path: 'firstname', operator: 'STRING_CONTAINS_CASE_INSENSITIVE', - value: this.searchValue, + value: value, memberOf: 'name' } - }, - lastnameFilter: { + } + userFilter[`lastnameFilter${index}`] = { condition: { path: 'lastname', operator: 'STRING_CONTAINS_CASE_INSENSITIVE', - value: this.searchValue, + value: value, memberOf: 'name' } - }, - name: { - group: { - conjunction: 'OR' - } } - } + }) this.userList({ page: { From f71a2abfe75d5c91224ea8a6194912c73587fc79 Mon Sep 17 00:00:00 2001 From: salisdemos <40487461+salisdemos@users.noreply.github.com> Date: Wed, 24 Jul 2024 10:34:55 +0200 Subject: [PATCH 019/173] fix(refs DPLAN-12128): Add Place to Segment relationship (3479) If we don't have the place in the relationship when we want to create a comment, it breaks. --- .../procedure/StatementSegmentsList/StatementSegmentsEdit.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/js/components/procedure/StatementSegmentsList/StatementSegmentsEdit.vue b/client/js/components/procedure/StatementSegmentsList/StatementSegmentsEdit.vue index e48c3bf1ba..007851a9f9 100644 --- a/client/js/components/procedure/StatementSegmentsList/StatementSegmentsEdit.vue +++ b/client/js/components/procedure/StatementSegmentsList/StatementSegmentsEdit.vue @@ -387,7 +387,7 @@ export default { fields: { Place: ['name', 'sortIndex'].join(), SegmentComment: ['creationDate', 'place', 'submitter', 'text'].join(), - StatementSegment: ['assignee', 'comments', 'externId', 'recommendation', 'text'].join(), + StatementSegment: ['assignee', 'comments', 'externId', 'recommendation', 'text', 'place'].join(), User: ['lastname', 'firstname', 'orga'].join(), Orga: ['name'].join() }, From a6708c8582ad211a920ff94fd0e4d0524303082d Mon Sep 17 00:00:00 2001 From: Steffi <101879352+gruenbergerdemos@users.noreply.github.com> Date: Wed, 24 Jul 2024 14:02:11 +0200 Subject: [PATCH 020/173] refactoring: some refactorings for StatementExportModal (3481) - Replace button with DpButton - Replace section and headings with fieldset and legend - Update test: Since we changed button to DpButton in the component, we now need to mock the click event (DpButton is only a stub now and cannot trigger events itself) --- .../statement/StatementExportModal.vue | 47 +++++++++---------- .../unit/StatementExportModal.spec.js | 3 +- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/client/js/components/statement/StatementExportModal.vue b/client/js/components/statement/StatementExportModal.vue index 96c1a15399..777cb93bc6 100644 --- a/client/js/components/statement/StatementExportModal.vue +++ b/client/js/components/statement/StatementExportModal.vue @@ -9,13 +9,11 @@ diff --git a/client/js/components/procedure/DpSearchProcedures.vue b/client/js/components/procedure/DpSearchProcedures.vue index 9ea22cb3ba..932692bb2e 100644 --- a/client/js/components/procedure/DpSearchProcedures.vue +++ b/client/js/components/procedure/DpSearchProcedures.vue @@ -9,15 +9,14 @@ diff --git a/client/js/components/procedure/publicindex/drawer/DetailView.vue b/client/js/components/procedure/publicindex/drawer/DetailView.vue index cfbb38af6b..54858ee4c8 100644 --- a/client/js/components/procedure/publicindex/drawer/DetailView.vue +++ b/client/js/components/procedure/publicindex/drawer/DetailView.vue @@ -19,7 +19,7 @@