diff --git a/.changes/v4.11.0.md b/.changes/v4.11.0.md new file mode 100644 index 000000000..e034723a6 --- /dev/null +++ b/.changes/v4.11.0.md @@ -0,0 +1,9 @@ +## v4.11.0 - 2025-12-17 +### Feature +* ([#478](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/478)) Add filtering to admin lists: social actions, social issues, goals, results, and evaluations +### Fixed +* ([#466](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/466)) Fix migration query after previous fix + +* Fix translation key/value + +Cannot start with % and should be wrapped in "". diff --git a/.changes/v4.12.0.md b/.changes/v4.12.0.md new file mode 100644 index 000000000..b903e6c6a --- /dev/null +++ b/.changes/v4.12.0.md @@ -0,0 +1,16 @@ +## v4.12.0 - 2026-01-15 +### Feature +* ([#473](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/473)) Display version of chill bundles in application footer +* Increase the delay before removing stale workflow from 90 days to 180 days. +### Fixed +* ([#480](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/480)) Fix the condition to display concerned persons in calendar list items. +* ([#481](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/481)) Fix ordering of social actions: actions with a closing date in the future should be considered as 'still open'. +* ([#477](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/477)) Fix export group by center for persons without a center in CenterAggregator.php +* Fix the calculation of budget balance to only take into account resources and charges that are still actual +* ([#489](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/489)) Fix desactivation date for Goals and results +* ([#490](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/490)) Prevent sending a notification when the user signs the document himself +* ([#491](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/491)) Fix: acc periods of which user is the referrer should not be included if when the list is filtered by center and none of the participations are part of the center +* ([#492](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/492)) fix CommentInput: replace deprecated value binding with model-value +* ([#493](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/493)) fix issue with stored object permissions associated with workflows (as attachment, or through a related entity) + + BC: the constructor's signature of `\Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter` has changed. diff --git a/.changes/v4.12.1.md b/.changes/v4.12.1.md new file mode 100644 index 000000000..a1e69887f --- /dev/null +++ b/.changes/v4.12.1.md @@ -0,0 +1,4 @@ +## v4.12.1 - 2026-02-01 +### Fixed +* ([#496](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/496)) Add the option to deal with duplicate address in BAN adress importer + diff --git a/CHANGELOG.md b/CHANGELOG.md index 5700c47cb..1f93dc783 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,38 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## v4.12.1 - 2026-02-01 +### Fixed +* ([#496](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/496)) Add the option to deal with duplicate address in BAN adress importer + + +## v4.12.0 - 2026-01-15 +### Feature +* ([#473](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/473)) Display version of chill bundles in application footer +* Increase the delay before removing stale workflow from 90 days to 180 days. +### Fixed +* ([#480](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/480)) Fix the condition to display concerned persons in calendar list items. +* ([#481](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/481)) Fix ordering of social actions: actions with a closing date in the future should be considered as 'still open'. +* ([#477](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/477)) Fix export group by center for persons without a center in CenterAggregator.php +* Fix the calculation of budget balance to only take into account resources and charges that are still actual +* ([#489](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/489)) Fix desactivation date for Goals and results +* ([#490](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/490)) Prevent sending a notification when the user signs the document himself +* ([#491](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/491)) Fix: acc periods of which user is the referrer should not be included if when the list is filtered by center and none of the participations are part of the center +* ([#492](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/492)) fix CommentInput: replace deprecated value binding with model-value +* ([#493](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/493)) fix issue with stored object permissions associated with workflows (as attachment, or through a related entity) + + BC: the constructor's signature of `\Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter` has changed. + +## v4.11.0 - 2025-12-17 +### Feature +* ([#478](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/478)) Add filtering to admin lists: social actions, social issues, goals, results, and evaluations +### Fixed +* ([#466](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/466)) Fix migration query after previous fix + +* Fix translation key/value + +Cannot start with % and should be wrapped in "". + ## v4.10.1 - 2025-12-11 ### Fixed * Fix missing translation variable in NewLocation component diff --git a/composer.json b/composer.json index ef3185191..d9c7a51db 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "ext-openssl": "*", "ext-redis": "*", "ext-zlib": "*", + "composer-runtime-api": "*", "champs-libres/wopi-bundle": "dev-symfony-v5@dev", "champs-libres/wopi-lib": "dev-master@dev", "doctrine/data-fixtures": "^1.8", @@ -82,7 +83,7 @@ "symfony/templating": "^5.4", "symfony/translation": "^5.4", "symfony/twig-bundle": "^5.4", - "symfony/ux-translator": "^2.22", + "symfony/ux-translator": "2.31.0", "symfony/validator": "^5.4", "symfony/webpack-encore-bundle": "^1.11", "symfony/workflow": "^5.4", @@ -97,7 +98,7 @@ "require-dev": { "doctrine/doctrine-fixtures-bundle": "^3.3", "fakerphp/faker": "^1.13", - "friendsofphp/php-cs-fixer": "3.65.0", + "friendsofphp/php-cs-fixer": "3.93.0", "jangregor/phpstan-prophecy": "^1.0", "nelmio/alice": "^3.8", "nikic/php-parser": "^4.15", diff --git a/packages/ChillZimbraBundle/.changes/unreleased/Added-20260122-153223.yaml b/packages/ChillZimbraBundle/.changes/unreleased/Added-20260122-153223.yaml new file mode 100644 index 000000000..da145c2e3 --- /dev/null +++ b/packages/ChillZimbraBundle/.changes/unreleased/Added-20260122-153223.yaml @@ -0,0 +1,3 @@ +kind: Added +body: Use admin delegated account for handling authentication +time: 2026-01-22T15:32:23.932994899+01:00 diff --git a/packages/ChillZimbraBundle/src/Calendar/Connector/ZimbraConnector/CreateZimbraComponent.php b/packages/ChillZimbraBundle/src/Calendar/Connector/ZimbraConnector/CreateZimbraComponent.php index 9fa14d75c..8acfa0337 100644 --- a/packages/ChillZimbraBundle/src/Calendar/Connector/ZimbraConnector/CreateZimbraComponent.php +++ b/packages/ChillZimbraBundle/src/Calendar/Connector/ZimbraConnector/CreateZimbraComponent.php @@ -80,12 +80,19 @@ final readonly class CreateZimbraComponent $location = $calendar->getCalendar()->getLocation(); $hasLocation = $calendar->getCalendar()->hasLocation(); $isPrivate = $calendar->getCalendar()->getAccompanyingPeriod()?->isConfidential() ?? false; - } else { + } elseif ($calendar instanceof Calendar) { $startDate = $calendar->getStartDate(); $endDate = $calendar->getEndDate(); $location = $calendar->getLocation(); $hasLocation = $calendar->hasLocation(); $isPrivate = $calendar->getAccompanyingPeriod()?->isConfidential() ?? false; + } else { + // Calendar range case + $startDate = $calendar->getStartDate(); + $endDate = $calendar->getEndDate(); + $location = $calendar->getLocation(); + $hasLocation = $calendar->hasLocation(); + $isPrivate = false; } $comp = new InviteComponent(); diff --git a/packages/ChillZimbraBundle/src/Calendar/Connector/ZimbraConnector/SoapClientBuilder.php b/packages/ChillZimbraBundle/src/Calendar/Connector/ZimbraConnector/SoapClientBuilder.php index e66c45e85..bf8a2311d 100644 --- a/packages/ChillZimbraBundle/src/Calendar/Connector/ZimbraConnector/SoapClientBuilder.php +++ b/packages/ChillZimbraBundle/src/Calendar/Connector/ZimbraConnector/SoapClientBuilder.php @@ -11,48 +11,84 @@ declare(strict_types=1); namespace Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector; +use Symfony\Component\Clock\ClockInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\HttpClient\Psr18Client; use Symfony\Contracts\HttpClient\HttpClientInterface; +use Zimbra\Admin\AdminApi; use Zimbra\Common\Enum\AccountBy; use Zimbra\Common\Soap\ClientFactory; +use Zimbra\Common\Struct\AccountSelector; use Zimbra\Common\Struct\Header\AccountInfo; use Zimbra\Mail\MailApi; -final readonly class SoapClientBuilder +final class SoapClientBuilder { - private string $username; + private readonly string $username; - private string $password; + private readonly string $password; - private string $url; + private readonly string $url; - public function __construct(private ParameterBagInterface $parameterBag, private HttpClientInterface $client) - { + private readonly string $adminUrl; + + private readonly bool $verifyHost; + + private readonly bool $verifyPeer; + + private readonly bool $adminVerifyHost; + + private readonly bool $adminVerifyPeer; + + /** + * Keep the cache of the tokens. + * + * @var array + */ + private array $tokenCache = []; + + public function __construct( + private readonly ParameterBagInterface $parameterBag, + private readonly HttpClientInterface $client, + private readonly ClockInterface $clock, + ) { $dsn = $this->parameterBag->get('chill_calendar.remote_calendar_dsn'); $url = parse_url($dsn); $this->username = urldecode($url['user']); $this->password = urldecode($url['pass']); if ('zimbra+http' === $url['scheme']) { - $scheme = 'http://'; + $scheme = 'http'; $port = $url['port'] ?? 80; } elseif ('zimbra+https' === $url['scheme']) { - $scheme = 'https://'; + $scheme = 'https'; $port = $url['port'] ?? 443; } else { throw new \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException('Unsupported remote calendar scheme: '.$url['scheme']); } - $this->url = $scheme.$url['host'].':'.$port; + // get attributes for adminUrl + $query = []; + parse_str($url['query'] ?? '', $query); + $adminPort = $query['adminPort'] ?? '7071'; + $adminHost = $query['adminHost'] ?? $url['host']; + $adminScheme = $query['adminScheme'] ?? $scheme; + + $this->verifyPeer = (bool) ($query['verifyPeer'] ?? true); + $this->verifyHost = (bool) ($query['verifyHost'] ?? true); + $this->adminVerifyHost = (bool) ($query['adminVerifyHost'] ?? $this->verifyPeer); + $this->adminVerifyPeer = (bool) ($query['adminVerifyPeer'] ?? $this->verifyHost); + + $this->url = $scheme.'://'.$url['host'].':'.$port; + $this->adminUrl = $adminScheme.'://'.$adminHost.':'.$adminPort; } private function buildApi(): MailApi { $baseClient = $this->client->withOptions([ 'base_uri' => $location = $this->url.'/service/soap', - 'verify_host' => false, - 'verify_peer' => false, + 'verify_host' => $this->verifyHost, + 'verify_peer' => $this->verifyPeer, ]); $psr18Client = new Psr18Client($baseClient); $api = new MailApi(); @@ -62,12 +98,36 @@ final readonly class SoapClientBuilder return $api; } + private function buildAdminApi(): AdminApi + { + $baseClient = $this->client->withOptions([ + 'base_uri' => $location = $this->adminUrl.'/service/admin/soap', + 'verify_host' => $this->adminVerifyHost, + 'verify_peer' => $this->adminVerifyPeer, + ]); + $psr18Client = new Psr18Client($baseClient); + $api = new AdminApi(); + $client = ClientFactory::create($location, $psr18Client); + $api->setClient($client); + + return $api; + } + public function getApiForAccount(string $accountName): MailApi { - $api = $this->buildApi(); - $response = $api->authByAccountName($this->username, $this->password); + ['token' => $token, 'expirationTime' => $expirationTime] = $this->tokenCache[$accountName] + ?? ['token' => null, 'expirationTime' => null]; - $token = $response->getAuthToken(); + if (null === $token || null === $expirationTime || $expirationTime <= $this->clock->now()) { + $adminApi = $this->buildAdminApi(); + $adminApi->auth($this->username, $this->password); + + $delegateResponse = $adminApi->delegateAuth(new AccountSelector(AccountBy::NAME, $accountName)); + $token = $delegateResponse->getAuthToken(); + $expiration = $delegateResponse->getLifetime(); + $expirationTime = $this->clock->now()->add(new \DateInterval('PT'.$expiration.'S')); + $this->tokenCache[$accountName] = ['token' => $token, 'expirationTime' => $expirationTime]; + } $apiBy = $this->buildApi(); $apiBy->setAuthToken($token); diff --git a/src/Bundle/ChillActivityBundle/DependencyInjection/ChillActivityExtension.php b/src/Bundle/ChillActivityBundle/DependencyInjection/ChillActivityExtension.php index 15e7e7e4b..38aefdaea 100644 --- a/src/Bundle/ChillActivityBundle/DependencyInjection/ChillActivityExtension.php +++ b/src/Bundle/ChillActivityBundle/DependencyInjection/ChillActivityExtension.php @@ -69,7 +69,7 @@ class ChillActivityExtension extends Extension implements PrependExtensionInterf } /** (non-PHPdoc). - * @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend() + * @see PrependExtensionInterface::prepend() */ public function prependRoutes(ContainerBuilder $container) { diff --git a/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php index 6a2a27140..f7b37c54d 100644 --- a/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php +++ b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php @@ -16,7 +16,8 @@ use Chill\ActivityBundle\Repository\ActivityRepository; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; -use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; +use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository; +use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelperInterface; use Symfony\Component\Security\Core\Security; class ActivityStoredObjectVoter extends AbstractStoredObjectVoter @@ -24,9 +25,10 @@ class ActivityStoredObjectVoter extends AbstractStoredObjectVoter public function __construct( private readonly ActivityRepository $repository, Security $security, - WorkflowRelatedEntityPermissionHelper $workflowDocumentService, + WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService, + EntityWorkflowAttachmentRepository $attachmentRepository, ) { - parent::__construct($security, $workflowDocumentService); + parent::__construct($security, $attachmentRepository, $workflowDocumentService); } protected function getRepository(): AssociatedEntityToStoredObjectInterface diff --git a/src/Bundle/ChillActivityBundle/migrations/Version20251118124241.php b/src/Bundle/ChillActivityBundle/migrations/Version20251118124241.php index c06f3e674..5a318262d 100644 --- a/src/Bundle/ChillActivityBundle/migrations/Version20251118124241.php +++ b/src/Bundle/ChillActivityBundle/migrations/Version20251118124241.php @@ -39,7 +39,7 @@ final class Version20251118124241 extends AbstractMigration $this->addSql("COMMENT ON COLUMN activity_user.by_migration IS 'For backup purpose - can be safely deleted after a while. See migration \\Chill\\Migrations\\Activity\\Version20251118124241'"); $this->addSql('INSERT INTO activity_user (activity_id, user_id, by_migration) - SELECT id, user_id, true FROM activity + SELECT id, user_id, true FROM activity WHERE user_id is not null ON CONFLICT DO NOTHING'); } diff --git a/src/Bundle/ChillBudgetBundle/DependencyInjection/ChillBudgetExtension.php b/src/Bundle/ChillBudgetBundle/DependencyInjection/ChillBudgetExtension.php index 6a8842b64..6bae7c785 100644 --- a/src/Bundle/ChillBudgetBundle/DependencyInjection/ChillBudgetExtension.php +++ b/src/Bundle/ChillBudgetBundle/DependencyInjection/ChillBudgetExtension.php @@ -56,7 +56,7 @@ class ChillBudgetExtension extends Extension implements PrependExtensionInterfac } /** (non-PHPdoc). - * @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend() + * @see PrependExtensionInterface::prepend() */ public function prependRoutes(ContainerBuilder $container) { diff --git a/src/Bundle/ChillBudgetBundle/Resources/views/Budget/_macros.html.twig b/src/Bundle/ChillBudgetBundle/Resources/views/Budget/_macros.html.twig index a1fee19ce..22e80fb3a 100644 --- a/src/Bundle/ChillBudgetBundle/Resources/views/Budget/_macros.html.twig +++ b/src/Bundle/ChillBudgetBundle/Resources/views/Budget/_macros.html.twig @@ -72,14 +72,20 @@ {% macro table_results(actualCharges, actualResources, results) %} +{% set now = date() %} + {% set totalCharges = 0 %} {% for c in actualCharges %} - {% set totalCharges = totalCharges + c.amount %} + {% if c.startDate <= now and (c.endDate is null or c.endDate >= now) %} + {% set totalCharges = totalCharges + c.amount %} + {% endif %} {% endfor %} {% set totalResources = 0 %} {% for r in actualResources %} - {% set totalResources = totalResources + r.amount %} + {% if r.startDate <= now and (r.endDate is null or r.endDate >= now) %} + {% set totalResources = totalResources + r.amount %} + {% endif %} {% endfor %} {% set result = (totalResources - totalCharges) %} diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_list.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_list.html.twig index 589efc96a..5502441c2 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_list.html.twig +++ b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_list.html.twig @@ -78,7 +78,7 @@ {% if calendar.comment.comment is not empty - or calendar.users|length > 0 + or calendar.persons|length > 0 or calendar.thirdParties|length > 0 or calendar.users|length > 0 %}
diff --git a/src/Bundle/ChillCustomFieldsBundle/DependencyInjection/ChillCustomFieldsExtension.php b/src/Bundle/ChillCustomFieldsBundle/DependencyInjection/ChillCustomFieldsExtension.php index 724fd01ca..a68284989 100644 --- a/src/Bundle/ChillCustomFieldsBundle/DependencyInjection/ChillCustomFieldsExtension.php +++ b/src/Bundle/ChillCustomFieldsBundle/DependencyInjection/ChillCustomFieldsExtension.php @@ -52,7 +52,7 @@ class ChillCustomFieldsExtension extends Extension implements PrependExtensionIn } /** (non-PHPdoc). - * @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend() + * @see PrependExtensionInterface::prepend() */ public function prepend(ContainerBuilder $container) { diff --git a/src/Bundle/ChillCustomFieldsBundle/Form/Type/ChoiceWithOtherType.php b/src/Bundle/ChillCustomFieldsBundle/Form/Type/ChoiceWithOtherType.php index 8d0c049f0..6c705ec7f 100644 --- a/src/Bundle/ChillCustomFieldsBundle/Form/Type/ChoiceWithOtherType.php +++ b/src/Bundle/ChillCustomFieldsBundle/Form/Type/ChoiceWithOtherType.php @@ -25,7 +25,7 @@ class ChoiceWithOtherType extends AbstractType private string $otherValueLabel = 'Other value'; /** (non-PHPdoc). - * @see \Symfony\Component\Form\AbstractType::buildForm() + * @see AbstractType::buildForm() */ public function buildForm(FormBuilderInterface $builder, array $options) { @@ -42,7 +42,7 @@ class ChoiceWithOtherType extends AbstractType } /** (non-PHPdoc). - * @see \Symfony\Component\Form\AbstractType::configureOptions() + * @see AbstractType::configureOptions() */ public function configureOptions(OptionsResolver $resolver) { diff --git a/src/Bundle/ChillCustomFieldsBundle/Form/Type/ChoicesListType.php b/src/Bundle/ChillCustomFieldsBundle/Form/Type/ChoicesListType.php index c50bd856e..7a06217d9 100644 --- a/src/Bundle/ChillCustomFieldsBundle/Form/Type/ChoicesListType.php +++ b/src/Bundle/ChillCustomFieldsBundle/Form/Type/ChoicesListType.php @@ -22,7 +22,7 @@ use Symfony\Component\Form\FormEvents; class ChoicesListType extends AbstractType { /** (non-PHPdoc). - * @see \Symfony\Component\Form\AbstractType::buildForm() + * @see AbstractType::buildForm() */ public function buildForm(FormBuilderInterface $builder, array $options) { diff --git a/src/Bundle/ChillCustomFieldsBundle/Service/CustomFieldProvider.php b/src/Bundle/ChillCustomFieldsBundle/Service/CustomFieldProvider.php index fcccf01ed..97b2dbe71 100644 --- a/src/Bundle/ChillCustomFieldsBundle/Service/CustomFieldProvider.php +++ b/src/Bundle/ChillCustomFieldsBundle/Service/CustomFieldProvider.php @@ -82,7 +82,7 @@ class CustomFieldProvider implements ContainerAwareInterface /** * (non-PHPdoc). * - * @see \Symfony\Component\DependencyInjection\ContainerAwareInterface::setContainer() + * @see ContainerAwareInterface::setContainer() */ public function setContainer(?ContainerInterface $container = null) { diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php index 15e8d7aac..098d32de0 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php @@ -15,7 +15,10 @@ use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface; -use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; +use Chill\MainBundle\Entity\Workflow\EntityWorkflow; +use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment; +use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository; +use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelperInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Security; @@ -34,7 +37,8 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface public function __construct( private readonly Security $security, - private readonly ?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null, + private readonly EntityWorkflowAttachmentRepository $entityWorkflowAttachmentRepository, + private readonly WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService, ) {} public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool @@ -46,16 +50,6 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool { - // we first try to get the permission from the workflow, as attachement (this is the less intensive query) - $workflowPermissionAsAttachment = match ($attribute) { - StoredObjectRoleEnum::SEE => $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($subject), - StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($subject), - }; - - if (WorkflowRelatedEntityPermissionHelper::FORCE_DENIED === $workflowPermissionAsAttachment) { - return false; - } - // Retrieve the related entity $entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject); @@ -65,7 +59,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface $regularPermission = $this->security->isGranted($voterAttribute, $entity); if (!$this->canBeAssociatedWithWorkflow()) { - return $regularPermission; + return $this->voteOnStoredObjectAsAttachementOfAWorkflow($attribute, $regularPermission, $subject); } $workflowPermission = match ($attribute) { @@ -74,9 +68,41 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface }; return match ($workflowPermission) { - WorkflowRelatedEntityPermissionHelper::FORCE_GRANT => true, - WorkflowRelatedEntityPermissionHelper::FORCE_DENIED => false, - WorkflowRelatedEntityPermissionHelper::ABSTAIN => WorkflowRelatedEntityPermissionHelper::FORCE_GRANT === $workflowPermissionAsAttachment || $regularPermission, + WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT => true, + WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED => false, + WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN => $this->voteOnStoredObjectAsAttachementOfAWorkflow($attribute, $regularPermission, $subject), }; } + + private function voteOnStoredObjectAsAttachementOfAWorkflow(StoredObjectRoleEnum $attribute, bool $regularPermission, StoredObject $storedObject): bool + { + $attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($storedObject); + + // we get all the entity workflows where the stored object is attached + $entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments); + + // we compute all the permission for each entity workflow + $permissions = array_map(fn (EntityWorkflow $entityWorkflow): string => match ($attribute) { + StoredObjectRoleEnum::SEE => $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($entityWorkflow), + StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($entityWorkflow), + }, $entityWorkflows); + + // now, we reduce the permissions: abstain are ignored. Between DENIED and and GRANT, DENIED takes precedence + $computedPermission = WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN; + foreach ($permissions as $permission) { + if (WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED === $permission) { + return false; + } + if (WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT === $permission) { + $computedPermission = WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT; + } + } + + if (WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN === $computedPermission) { + return $regularPermission; + } + + // this is the case where WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT is returned + return true; + } } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php index e1a9add7d..aa34e7877 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php @@ -16,6 +16,7 @@ use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; +use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository; use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; use Symfony\Component\Security\Core\Security; @@ -25,8 +26,9 @@ final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredOb private readonly AccompanyingCourseDocumentRepository $repository, Security $security, WorkflowRelatedEntityPermissionHelper $workflowDocumentService, + EntityWorkflowAttachmentRepository $attachmentRepository, ) { - parent::__construct($security, $workflowDocumentService); + parent::__construct($security, $attachmentRepository, $workflowDocumentService); } protected function getRepository(): AssociatedEntityToStoredObjectInterface diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php index 16833c535..d9bcc5597 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php @@ -16,6 +16,7 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Repository\PersonDocumentRepository; use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; +use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository; use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; use Symfony\Component\Security\Core\Security; @@ -25,8 +26,9 @@ class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter private readonly PersonDocumentRepository $repository, Security $security, WorkflowRelatedEntityPermissionHelper $workflowDocumentService, + EntityWorkflowAttachmentRepository $attachmentRepository, ) { - parent::__construct($security, $workflowDocumentService); + parent::__construct($security, $attachmentRepository, $workflowDocumentService); } protected function getRepository(): AssociatedEntityToStoredObjectInterface diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php index d1a96a5bb..a01673165 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php @@ -16,8 +16,11 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; use Chill\MainBundle\Entity\User; -use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; +use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment; +use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository; +use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelperInterface; use PHPUnit\Framework\TestCase; +use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Security; @@ -31,21 +34,31 @@ class AbstractStoredObjectVoterTest extends TestCase { use ProphecyTrait; + /** + * @param array $attachments + * + * @return void + */ private function buildStoredObjectVoter( bool $canBeAssociatedWithWorkflow, AssociatedEntityToStoredObjectInterface $repository, Security $security, - ?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null, + ?WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService = null, + array $attachments = [], ): AbstractStoredObjectVoter { + $attachmentsRepository = $this->prophesize(EntityWorkflowAttachmentRepository::class); + $attachmentsRepository->findByStoredObject(Argument::type(StoredObject::class))->willReturn($attachments); + // Anonymous class extending the abstract class - return new class ($canBeAssociatedWithWorkflow, $repository, $security, $workflowDocumentService) extends AbstractStoredObjectVoter { + return new class ($canBeAssociatedWithWorkflow, $repository, $security, $attachmentsRepository->reveal(), $workflowDocumentService) extends AbstractStoredObjectVoter { public function __construct( private readonly bool $canBeAssociatedWithWorkflow, private readonly AssociatedEntityToStoredObjectInterface $repository, Security $security, - ?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null, + EntityWorkflowAttachmentRepository $attachmentRepository, + WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService, ) { - parent::__construct($security, $workflowDocumentService); + parent::__construct($security, $attachmentRepository, $workflowDocumentService); } protected function attributeToRole($attribute): string @@ -72,28 +85,29 @@ class AbstractStoredObjectVoterTest extends TestCase public function testSupportsOnAttribute(): void { - $voter = $this->buildStoredObjectVoter(false, new DummyRepository(new \stdClass()), $this->prophesize(Security::class)->reveal(), null); + $entityWorkflowService = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class); + + $voter = $this->buildStoredObjectVoter(false, new DummyRepository(new \stdClass()), $this->prophesize(Security::class)->reveal(), $entityWorkflowService->reveal()); self::assertTrue($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject())); - $voter = $this->buildStoredObjectVoter(false, new DummyRepository(new User()), $this->prophesize(Security::class)->reveal(), null); + $voter = $this->buildStoredObjectVoter(false, new DummyRepository(new User()), $this->prophesize(Security::class)->reveal(), $entityWorkflowService->reveal()); self::assertFalse($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject())); - $voter = $this->buildStoredObjectVoter(false, new DummyRepository(null), $this->prophesize(Security::class)->reveal(), null); + $voter = $this->buildStoredObjectVoter(false, new DummyRepository(null), $this->prophesize(Security::class)->reveal(), $entityWorkflowService->reveal()); self::assertFalse($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject())); } /** - * @dataProvider dataProviderVoteOnAttributeWithStoredObjectPermission + * @dataProvider dataProviderVoteOnAttributeWithWorkflow */ - public function testVoteOnAttributeWithStoredObjectPermission( + public function testVoteOnAttributeWithWorkflow( StoredObjectRoleEnum $attribute, bool $expected, bool $isGrantedRegularPermission, string $isGrantedWorkflowPermission, - string $isGrantedStoredObjectAttachment, ): void { $storedObject = new StoredObject(); $repository = new DummyRepository($related = new \stdClass()); @@ -102,31 +116,28 @@ class AbstractStoredObjectVoterTest extends TestCase $security = $this->prophesize(Security::class); $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission); - $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class); + $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class); $security = $this->prophesize(Security::class); $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission); + $attachementRepository = $this->prophesize(EntityWorkflowAttachmentRepository::class); + $attachementRepository->findByStoredObject($storedObject)->willReturn([]); + if (StoredObjectRoleEnum::SEE === $attribute) { - $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject) - ->shouldBeCalled() - ->willReturn($isGrantedStoredObjectAttachment); $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related) ->willReturn($isGrantedWorkflowPermission); } elseif (StoredObjectRoleEnum::EDIT === $attribute) { - $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject) - ->shouldBeCalled() - ->willReturn($isGrantedStoredObjectAttachment); $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related) ->willReturn($isGrantedWorkflowPermission); } else { throw new \LogicException('Invalid attribute for StoredObjectVoter'); } - $storedObjectVoter = new class ($repository, $workflowRelatedEntityPermissionHelper->reveal(), $security->reveal()) extends AbstractStoredObjectVoter { - public function __construct(private $repository, $helper, $security) + $storedObjectVoter = new class ($repository, $workflowRelatedEntityPermissionHelper->reveal(), $security->reveal(), $attachementRepository->reveal()) extends AbstractStoredObjectVoter { + public function __construct(private $repository, $helper, $security, EntityWorkflowAttachmentRepository $attachmentRepository) { - parent::__construct($security, $helper); + parent::__construct($security, $attachmentRepository, $helper); } protected function getRepository(): AssociatedEntityToStoredObjectInterface @@ -155,96 +166,64 @@ class AbstractStoredObjectVoterTest extends TestCase self::assertEquals($expected, $actual); } - public static function dataProviderVoteOnAttributeWithStoredObjectPermission(): iterable + public static function dataProviderVoteOnAttributeWithWorkflow(): iterable { foreach (['read' => StoredObjectRoleEnum::SEE, 'write' => StoredObjectRoleEnum::EDIT] as $action => $attribute) { yield 'Not related to any workflow nor attachment ('.$action.')' => [ $attribute, true, true, - WorkflowRelatedEntityPermissionHelper::ABSTAIN, - WorkflowRelatedEntityPermissionHelper::ABSTAIN, + WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, ]; yield 'Not related to any workflow nor attachment (refuse) ('.$action.')' => [ $attribute, false, false, - WorkflowRelatedEntityPermissionHelper::ABSTAIN, - WorkflowRelatedEntityPermissionHelper::ABSTAIN, + WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, ]; yield 'Is granted by a workflow takes precedence (workflow) ('.$action.')' => [ $attribute, false, true, - WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, - WorkflowRelatedEntityPermissionHelper::ABSTAIN, + WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, ]; yield 'Is granted by a workflow takes precedence (stored object) ('.$action.')' => [ $attribute, false, - true, - WorkflowRelatedEntityPermissionHelper::ABSTAIN, - WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, - ]; - - yield 'Is granted by a workflow takes precedence (workflow) although grant ('.$action.')' => [ - $attribute, false, - true, - WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, - WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, + WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, ]; yield 'Is granted by a workflow takes precedence (stored object) although grant ('.$action.')' => [ $attribute, - false, true, - WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, - WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, + true, + WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, ]; yield 'Is granted by a workflow takes precedence (initially refused) (workflow) although grant ('.$action.')' => [ $attribute, false, false, - WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, - WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, + WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, ]; yield 'Is granted by a workflow takes precedence (initially refused) (stored object) although grant ('.$action.')' => [ - $attribute, - false, - false, - WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, - WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, - ]; - - yield 'Force grant inverse the regular permission (workflow) ('.$action.')' => [ $attribute, true, false, - WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, - WorkflowRelatedEntityPermissionHelper::ABSTAIN, + WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, ]; - - yield 'Force grant inverse the regular permission (so) ('.$action.')' => [ - $attribute, - true, - false, - WorkflowRelatedEntityPermissionHelper::ABSTAIN, - WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, - ]; - } } /** - * @dataProvider dataProviderVoteOnAttributeWithoutStoredObjectPermission + * @dataProvider dataProviderVoteOnAttribute */ - public function testVoteOnAttributeWithoutStoredObjectPermission( + public function testVoteOnAttribute( StoredObjectRoleEnum $attribute, bool $expected, bool $canBeAssociatedWithWorkflow, @@ -260,10 +239,7 @@ class AbstractStoredObjectVoterTest extends TestCase $security = $this->prophesize(Security::class); $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission); - $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class); - - $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN); - $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN); + $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class); if (null !== $isGrantedWorkflowPermissionRead) { $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related) @@ -283,27 +259,155 @@ class AbstractStoredObjectVoterTest extends TestCase self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message); } - public static function dataProviderVoteOnAttributeWithoutStoredObjectPermission(): iterable + public static function dataProviderVoteOnAttribute(): iterable { // not associated on a workflow yield [StoredObjectRoleEnum::SEE, true, false, true, null, null, 'not associated on a workflow, granted by regular access, must not rely on helper']; yield [StoredObjectRoleEnum::SEE, false, false, false, null, null, 'not associated on a workflow, denied by regular access, must not rely on helper']; // associated on a workflow, read operation - yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be granted']; - yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelper::ABSTAIN, null, 'associated on a workflow, read by regular, abstain by workflow, ask for read, should be granted']; - yield [StoredObjectRoleEnum::SEE, false, true, true, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be denied']; - yield [StoredObjectRoleEnum::SEE, true, true, false, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be granted']; - yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelper::ABSTAIN, null, 'associated on a workflow, denied read by regular, abstain by workflow, ask for read, should be granted']; - yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be denied']; + yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be granted']; + yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, null, 'associated on a workflow, read by regular, abstain by workflow, ask for read, should be granted']; + yield [StoredObjectRoleEnum::SEE, false, true, true, WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be denied']; + yield [StoredObjectRoleEnum::SEE, true, true, false, WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be granted']; + yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, null, 'associated on a workflow, denied read by regular, abstain by workflow, ask for read, should be granted']; + yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be denied']; // association on a workflow, write operation - yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be granted']; - yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelper::ABSTAIN, 'associated on a workflow, write by regular, abstain by workflow, ask for write, should be granted']; - yield [StoredObjectRoleEnum::EDIT, false, true, true, null, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be denied']; - yield [StoredObjectRoleEnum::EDIT, true, true, false, null, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be granted']; - yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelper::ABSTAIN, 'associated on a workflow, denied write by regular, abstain by workflow, ask for write, should be granted']; - yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be denied']; + yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be granted']; + yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, 'associated on a workflow, write by regular, abstain by workflow, ask for write, should be granted']; + yield [StoredObjectRoleEnum::EDIT, false, true, true, null, WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be denied']; + yield [StoredObjectRoleEnum::EDIT, true, true, false, null, WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be granted']; + yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, 'associated on a workflow, denied write by regular, abstain by workflow, ask for write, should be granted']; + yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be denied']; + } + + /** + * @dataProvider dataProviderPrecedenceOfDirectAssociationOverWorkflowAttachments + */ + public function testPrecedenceOfDirectAssociationOverWorkflowAttachments( + StoredObjectRoleEnum $attribute, + bool $expected, + bool $regularPermission, + string $directWorkflowPermission, + string $attachmentWorkflowPermission, + string $message, + ): void { + $storedObject = new StoredObject(); + $repository = new DummyRepository($related = new \stdClass()); + $token = new UsernamePasswordToken(new User(), 'dummy'); + + $security = $this->prophesize(Security::class); + $security->isGranted('SOME_ROLE', $related)->willReturn($regularPermission); + + $workflowHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class); + + // Direct association permission + if (StoredObjectRoleEnum::SEE === $attribute) { + $workflowHelper->isAllowedByWorkflowForReadOperation($related) + ->willReturn($directWorkflowPermission); + } else { + $workflowHelper->isAllowedByWorkflowForWriteOperation($related) + ->willReturn($directWorkflowPermission); + } + + // Attachment permission + $entityWorkflow = $this->prophesize(\Chill\MainBundle\Entity\Workflow\EntityWorkflow::class)->reveal(); + $attachment = $this->prophesize(EntityWorkflowAttachment::class); + $attachment->getEntityWorkflow()->willReturn($entityWorkflow); + + if (StoredObjectRoleEnum::SEE === $attribute) { + $workflowHelper->isAllowedByWorkflowForReadOperation($entityWorkflow) + ->willReturn($attachmentWorkflowPermission); + } else { + $workflowHelper->isAllowedByWorkflowForWriteOperation($entityWorkflow) + ->willReturn($attachmentWorkflowPermission); + } + + $voter = $this->buildStoredObjectVoter( + true, + $repository, + $security->reveal(), + $workflowHelper->reveal(), + [$attachment->reveal()] + ); + + self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message); + } + + public static function dataProviderPrecedenceOfDirectAssociationOverWorkflowAttachments(): iterable + { + $cases = [ + [ + 'expected' => true, + 'regular' => false, + 'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, + 'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, + 'message' => 'Direct FORCE_GRANT should win over attachment FORCE_DENIED', + ], + [ + 'expected' => false, + 'regular' => true, + 'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, + 'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, + 'message' => 'Direct FORCE_DENIED should win over attachment FORCE_GRANT', + ], + [ + 'expected' => true, + 'regular' => false, + 'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, + 'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, + 'message' => 'Direct FORCE_GRANT should win over attachment ABSTAIN', + ], + [ + 'expected' => false, + 'regular' => true, + 'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, + 'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, + 'message' => 'Direct FORCE_DENIED should win over attachment ABSTAIN', + ], + [ + 'expected' => true, + 'regular' => false, + 'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, + 'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, + 'message' => 'Direct ABSTAIN should let attachment FORCE_GRANT win', + ], + [ + 'expected' => false, + 'regular' => true, + 'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, + 'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, + 'message' => 'Direct ABSTAIN should let attachment FORCE_DENIED win', + ], + [ + 'expected' => true, + 'regular' => true, + 'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, + 'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, + 'message' => 'Both ABSTAIN should let regular permission (true) win', + ], + [ + 'expected' => false, + 'regular' => false, + 'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, + 'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, + 'message' => 'Both ABSTAIN should let regular permission (false) win', + ], + ]; + + foreach ([StoredObjectRoleEnum::SEE, StoredObjectRoleEnum::EDIT] as $attribute) { + foreach ($cases as $case) { + yield sprintf('%s - %s', $attribute->name, $case['message']) => [ + $attribute, + $case['expected'], + $case['regular'], + $case['direct'], + $case['attachment'], + $case['message'], + ]; + } + } } } diff --git a/src/Bundle/ChillEventBundle/DependencyInjection/ChillEventExtension.php b/src/Bundle/ChillEventBundle/DependencyInjection/ChillEventExtension.php index cb59bb390..e5cbb2638 100644 --- a/src/Bundle/ChillEventBundle/DependencyInjection/ChillEventExtension.php +++ b/src/Bundle/ChillEventBundle/DependencyInjection/ChillEventExtension.php @@ -52,7 +52,7 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface } /** (non-PHPdoc). - * @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend() + * @see PrependExtensionInterface::prepend() */ public function prepend(ContainerBuilder $container): void { diff --git a/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php b/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php index 7e61db7d6..e6114276f 100644 --- a/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php +++ b/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php @@ -14,6 +14,7 @@ namespace Chill\EventBundle\Security\Authorization; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; +use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository; use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; use Chill\EventBundle\Entity\Event; use Chill\EventBundle\Repository\EventRepository; @@ -26,8 +27,9 @@ class EventStoredObjectVoter extends AbstractStoredObjectVoter private readonly EventRepository $repository, Security $security, WorkflowRelatedEntityPermissionHelper $workflowDocumentService, + EntityWorkflowAttachmentRepository $attachmentRepository, ) { - parent::__construct($security, $workflowDocumentService); + parent::__construct($security, $attachmentRepository, $workflowDocumentService); } protected function getRepository(): AssociatedEntityToStoredObjectInterface diff --git a/src/Bundle/ChillJobBundle/src/Resources/translations/messages.nl.yml b/src/Bundle/ChillJobBundle/src/Resources/translations/messages.nl.yml index d83404a09..ad18ac5c1 100644 --- a/src/Bundle/ChillJobBundle/src/Resources/translations/messages.nl.yml +++ b/src/Bundle/ChillJobBundle/src/Resources/translations/messages.nl.yml @@ -189,14 +189,14 @@ crud: title_edit: Rapport "belemmering" bewerken title_delete: Belemmering verwijderen button_delete: Verwijderen - confirm_message_delete: %as_string% verwijderen? + confirm_message_delete: "%as_string% verwijderen?" cscv: title_new: Nieuw CV voor %person% title_view: CV voor %person% title_edit: CV bewerken title_delete: CV verwijderen button_delete: Verwijderen - confirm_message_delete: %as_string% verwijderen? + confirm_message_delete: "%as_string% verwijderen?" no_date: Geen datum aangegeven no_end_date: einddatum onbekend no_start_date: startdatum onbekend @@ -206,7 +206,7 @@ crud: title_edit: Immersie bewerken title_delete: Immersie verwijderen button_delete: Verwijderen - confirm_message_delete: %as_string% verwijderen? + confirm_message_delete: "%as_string% verwijderen?" projet_prof: title_new: Nieuw professioneel project voor %person% title_view: Professioneel project voor %person% diff --git a/src/Bundle/ChillMainBundle/Command/LoadAddressesFRFromBANCommand.php b/src/Bundle/ChillMainBundle/Command/LoadAddressesFRFromBANCommand.php index 8483c0be1..a747b4c2c 100644 --- a/src/Bundle/ChillMainBundle/Command/LoadAddressesFRFromBANCommand.php +++ b/src/Bundle/ChillMainBundle/Command/LoadAddressesFRFromBANCommand.php @@ -31,7 +31,8 @@ class LoadAddressesFRFromBANCommand extends Command { $this->setName('chill:main:address-ref-from-ban') ->addArgument('departementNo', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'a list of departement numbers') - ->addOption('send-report-email', 's', InputOption::VALUE_REQUIRED, 'Email address where a list of unimported addresses can be send'); + ->addOption('send-report-email', 's', InputOption::VALUE_REQUIRED, 'Email address where a list of unimported addresses can be send') + ->addOption('allow-remove-double-refid', 'd', InputOption::VALUE_NONE, 'Should the address importer be allowed to remove same refid in the source data, if any'); } protected function execute(InputInterface $input, OutputInterface $output): int @@ -40,7 +41,7 @@ class LoadAddressesFRFromBANCommand extends Command foreach ($input->getArgument('departementNo') as $departementNo) { $output->writeln('Import addresses for '.$departementNo); - $this->addressReferenceFromBAN->import($departementNo, $input->hasOption('send-report-email') ? $input->getOption('send-report-email') : null); + $this->addressReferenceFromBAN->import($departementNo, $input->hasOption('send-report-email') ? $input->getOption('send-report-email') : null, allowRemoveDoubleRefId: $input->hasOption('allow-remove-double-refid') ? $input->getOption('allow-remove-double-refid') : false); } return Command::SUCCESS; diff --git a/src/Bundle/ChillMainBundle/Command/LoadAndUpdateLanguagesCommand.php b/src/Bundle/ChillMainBundle/Command/LoadAndUpdateLanguagesCommand.php index 376df4e52..43d63a0fa 100644 --- a/src/Bundle/ChillMainBundle/Command/LoadAndUpdateLanguagesCommand.php +++ b/src/Bundle/ChillMainBundle/Command/LoadAndUpdateLanguagesCommand.php @@ -48,7 +48,7 @@ class LoadAndUpdateLanguagesCommand extends Command /** * (non-PHPdoc). * - * @see \Symfony\Component\Console\Command\Command::configure() + * @see Command::configure() */ protected function configure() { @@ -73,7 +73,7 @@ class LoadAndUpdateLanguagesCommand extends Command /** * (non-PHPdoc). * - * @see \Symfony\Component\Console\Command\Command::execute() + * @see Command::execute() */ protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/src/Bundle/ChillMainBundle/Command/LoadCountriesCommand.php b/src/Bundle/ChillMainBundle/Command/LoadCountriesCommand.php index 3f7f82523..fd921b51a 100644 --- a/src/Bundle/ChillMainBundle/Command/LoadCountriesCommand.php +++ b/src/Bundle/ChillMainBundle/Command/LoadCountriesCommand.php @@ -51,7 +51,7 @@ class LoadCountriesCommand extends Command /** * (non-PHPdoc). * - * @see \Symfony\Component\Console\Command\Command::configure() + * @see Command::configure() */ protected function configure() { @@ -61,7 +61,7 @@ class LoadCountriesCommand extends Command /** * (non-PHPdoc). * - * @see \Symfony\Component\Console\Command\Command::execute() + * @see Command::execute() */ protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/SearchableServicesCompilerPass.php b/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/SearchableServicesCompilerPass.php index ee305f964..6d34d2e93 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/SearchableServicesCompilerPass.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/SearchableServicesCompilerPass.php @@ -20,7 +20,7 @@ class SearchableServicesCompilerPass implements CompilerPassInterface /** * (non-PHPdoc). * - * @see \Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface::process() + * @see CompilerPassInterface::process() */ public function process(ContainerBuilder $container) { diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/Widget/Factory/AbstractWidgetFactory.php b/src/Bundle/ChillMainBundle/DependencyInjection/Widget/Factory/AbstractWidgetFactory.php index b0c14f27e..05aafba77 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/Widget/Factory/AbstractWidgetFactory.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/Widget/Factory/AbstractWidgetFactory.php @@ -32,7 +32,7 @@ abstract class AbstractWidgetFactory implements WidgetFactoryInterface * Will create the definition by returning the definition from the `services.yml` * file (or `services.xml` or `what-you-want.yml`). * - * @see \Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface::createDefinition() + * @see WidgetFactoryInterface::createDefinition() */ public function createDefinition(ContainerBuilder $containerBuilder, $place, $order, array $config) { diff --git a/src/Bundle/ChillMainBundle/Resources/views/Layout/_footer.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Layout/_footer.html.twig index df6ba912b..43a91b5e1 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Layout/_footer.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Layout/_footer.html.twig @@ -2,6 +2,10 @@

{{ 'This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License'|trans|raw }}
+ {% if get_chill_version() %} + {{ 'footer.Running chill version %version%'|trans({ '%version%': get_chill_version() }) }} + {% endif %} +
{{ 'User manual'|trans }} diff --git a/src/Bundle/ChillMainBundle/Security/Resolver/ScopeResolverDispatcher.php b/src/Bundle/ChillMainBundle/Security/Resolver/ScopeResolverDispatcher.php index 50cca33d5..d7cb0ee21 100644 --- a/src/Bundle/ChillMainBundle/Security/Resolver/ScopeResolverDispatcher.php +++ b/src/Bundle/ChillMainBundle/Security/Resolver/ScopeResolverDispatcher.php @@ -33,7 +33,7 @@ final readonly class ScopeResolverDispatcher } /** - * @return Scope|iterable|Scope|null + * @return Scope|iterable|null */ public function resolveScope(mixed $entity, ?array $options = []): iterable|Scope|null { diff --git a/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceFromBAN.php b/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceFromBAN.php index 0aa4cd6ce..83def3d5c 100644 --- a/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceFromBAN.php +++ b/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceFromBAN.php @@ -23,7 +23,7 @@ class AddressReferenceFromBAN private readonly AddressToReferenceMatcher $addressToReferenceMatcher, ) {} - public function import(string $departementNo, ?string $sendAddressReportToEmail = null): void + public function import(string $departementNo, ?string $sendAddressReportToEmail = null, ?bool $allowRemoveDoubleRefId = false): void { if (!is_numeric($departementNo)) { throw new \UnexpectedValueException('Could not parse this department number'); @@ -96,7 +96,7 @@ class AddressReferenceFromBAN ); } - $this->baseImporter->finalize(sendAddressReportToEmail: $sendAddressReportToEmail); + $this->baseImporter->finalize(allowRemoveDoubleRefId: $allowRemoveDoubleRefId, sendAddressReportToEmail: $sendAddressReportToEmail); $this->addressToReferenceMatcher->checkAddressesMatchingReferences(); diff --git a/src/Bundle/ChillMainBundle/Service/VersionProvider.php b/src/Bundle/ChillMainBundle/Service/VersionProvider.php new file mode 100644 index 000000000..28b4f3ff3 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Service/VersionProvider.php @@ -0,0 +1,45 @@ +packageName); + + if (null === $version) { + return 'unknown'; + } + + return $version; + } catch (\OutOfBoundsException) { + return 'unknown'; + } + } + + public function getFormattedVersion(): string + { + $version = $this->getVersion(); + + if ('unknown' === $version) { + return 'Version unavailable'; + } + + return $version; + } +} diff --git a/src/Bundle/ChillMainBundle/Service/Workflow/CancelStaleWorkflowCronJob.php b/src/Bundle/ChillMainBundle/Service/Workflow/CancelStaleWorkflowCronJob.php index e8b67e0ab..e63459c03 100644 --- a/src/Bundle/ChillMainBundle/Service/Workflow/CancelStaleWorkflowCronJob.php +++ b/src/Bundle/ChillMainBundle/Service/Workflow/CancelStaleWorkflowCronJob.php @@ -23,7 +23,7 @@ class CancelStaleWorkflowCronJob implements CronJobInterface { public const KEY = 'remove-stale-workflow'; - public const KEEP_INTERVAL = 'P90D'; + public const KEEP_INTERVAL = 'P180D'; private const LAST_CANCELED_WORKFLOW = 'last-canceled-workflow-id'; diff --git a/src/Bundle/ChillMainBundle/Templating/VersionRenderExtension.php b/src/Bundle/ChillMainBundle/Templating/VersionRenderExtension.php new file mode 100644 index 000000000..ca3f362a2 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Templating/VersionRenderExtension.php @@ -0,0 +1,35 @@ +getChillVersion(...)), + ]; + } + + public function getChillVersion(): string + { + return $this->versionProvider->getFormattedVersion(); + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Services/Workflow/CancelStaleWorkflowHandlerTest.php b/src/Bundle/ChillMainBundle/Tests/Services/Workflow/CancelStaleWorkflowHandlerTest.php index aefcf7948..4e7ba4a00 100644 --- a/src/Bundle/ChillMainBundle/Tests/Services/Workflow/CancelStaleWorkflowHandlerTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Services/Workflow/CancelStaleWorkflowHandlerTest.php @@ -42,14 +42,14 @@ class CancelStaleWorkflowHandlerTest extends TestCase { use ProphecyTrait; - public function testWorkflowWithOneStepOlderThan90DaysIsCanceled(): void + public function testWorkflowWithOneStepOlderThan180DaysIsCanceled(): void { $clock = new MockClock('2024-01-01'); - $daysAgos = new \DateTimeImmutable('2023-09-01'); + $daysAgos = new \DateTimeImmutable('2023-06-01'); $workflow = new EntityWorkflow(); $workflow->setWorkflowName('dummy_workflow'); - $workflow->setCreatedAt(new \DateTimeImmutable('2023-09-01')); + $workflow->setCreatedAt(new \DateTimeImmutable('2023-06-01')); $workflow->setStep('step1', new WorkflowTransitionContextDTO($workflow), 'to_step1', $daysAgos, new User()); $em = $this->prophesize(EntityManagerInterface::class); @@ -94,7 +94,7 @@ class CancelStaleWorkflowHandlerTest extends TestCase $workflow = new EntityWorkflow(); $workflow->setWorkflowName('dummy_workflow'); - $workflow->setCreatedAt(new \DateTimeImmutable('2023-09-01')); + $workflow->setCreatedAt(new \DateTimeImmutable('2023-06-01')); $em = $this->prophesize(EntityManagerInterface::class); $em->flush()->shouldBeCalled(); diff --git a/src/Bundle/ChillMainBundle/Tests/Workflow/EventSubscriber/NotificationOnTransitionTest.php b/src/Bundle/ChillMainBundle/Tests/Workflow/EventSubscriber/NotificationOnTransitionTest.php index 9f90c5b12..52f3e4eba 100644 --- a/src/Bundle/ChillMainBundle/Tests/Workflow/EventSubscriber/NotificationOnTransitionTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Workflow/EventSubscriber/NotificationOnTransitionTest.php @@ -15,7 +15,9 @@ use Chill\MainBundle\Entity\Notification; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\UserGroup; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; +use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum; use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep; +use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature; use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface; use Chill\MainBundle\Workflow\EntityWorkflowManager; use Chill\MainBundle\Workflow\EventSubscriber\NotificationOnTransition; @@ -87,7 +89,7 @@ final class NotificationOnTransitionTest extends TestCase ->willReturn([]); $registry = $this->prophesize(Registry::class); - $registry->get(Argument::type(EntityWorkflow::class), Argument::type('string')) + $registry->get(Argument::type(EntityWorkflow::class), Argument::any()) ->willReturn($workflow); $security = $this->prophesize(Security::class); @@ -111,4 +113,74 @@ final class NotificationOnTransitionTest extends TestCase $notificationOnTransition->onCompletedSendNotification($event); } + + public function testOnCompleteDoNotSendNotificationIfStepCreatedByPreviousSignature(): void + { + $dest = new User(); + $currentUser = new User(); + $workflowProphecy = $this->prophesize(WorkflowInterface::class); + $workflow = $workflowProphecy->reveal(); + $entityWorkflow = new EntityWorkflow(); + $entityWorkflow + ->setWorkflowName('workflow_name') + ->setRelatedEntityClass(\stdClass::class) + ->setRelatedEntityId(1); + // force an id to entityWorkflow: + $reflection = new \ReflectionClass($entityWorkflow); + $id = $reflection->getProperty('id'); + $id->setValue($entityWorkflow, 1); + + $previousStep = new EntityWorkflowStep(); + $previousStep->addSignature($signature = new EntityWorkflowStepSignature($previousStep, $dest)); + $signature->setState(EntityWorkflowSignatureStateEnum::SIGNED); + + $currentStep = new EntityWorkflowStep(); + $currentStep->addDestUser($dest); + $currentStep->setCurrentStep('to_state'); + + $entityWorkflow->addStep($previousStep); + $entityWorkflow->addStep($currentStep); + + $em = $this->prophesize(EntityManagerInterface::class); + + // we check that NO notification has been persisted for $dest + $em->persist(Argument::that( + fn ($notificationCandidate) => $notificationCandidate instanceof Notification && $notificationCandidate->getAddressees()->contains($dest) + ))->shouldNotBeCalled(); + + $engine = $this->prophesize(\Twig\Environment::class); + $engine->render(Argument::type('string'), Argument::type('array')) + ->willReturn('dummy text'); + + $extractor = $this->prophesize(MetadataExtractor::class); + $extractor->buildArrayPresentationForPlace(Argument::type(EntityWorkflow::class), Argument::any()) + ->willReturn([]); + $extractor->buildArrayPresentationForWorkflow(Argument::any()) + ->willReturn([]); + + $registry = $this->prophesize(Registry::class); + $registry->get(Argument::type(EntityWorkflow::class), Argument::any()) + ->willReturn($workflow); + + $security = $this->prophesize(Security::class); + $security->getUser()->willReturn(null); + + $entityWorkflowHandler = $this->prophesize(EntityWorkflowHandlerInterface::class); + $entityWorkflowHandler->getEntityTitle($entityWorkflow)->willReturn('workflow title'); + $entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class); + $entityWorkflowManager->getHandler($entityWorkflow)->willReturn($entityWorkflowHandler->reveal()); + + $notificationOnTransition = new NotificationOnTransition( + $em->reveal(), + $engine->reveal(), + $extractor->reveal(), + $security->reveal(), + $registry->reveal(), + $entityWorkflowManager->reveal(), + ); + + $event = new Event($entityWorkflow, new Marking(), new Transition('dummy_transition', ['from_state'], ['to_state']), $workflow); + + $notificationOnTransition->onCompletedSendNotification($event); + } } diff --git a/src/Bundle/ChillMainBundle/Tests/Workflow/Helper/WorkflowRelatedEntityPermissionHelperTest.php b/src/Bundle/ChillMainBundle/Tests/Workflow/Helper/WorkflowRelatedEntityPermissionHelperTest.php index d28645182..a778c64af 100644 --- a/src/Bundle/ChillMainBundle/Tests/Workflow/Helper/WorkflowRelatedEntityPermissionHelperTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Workflow/Helper/WorkflowRelatedEntityPermissionHelperTest.php @@ -11,9 +11,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Tests\Workflow\Helper; -use Chill\DocStoreBundle\Entity\StoredObject; -use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment; -use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository; use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; @@ -269,217 +266,7 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase $entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class); $entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn($entityWorkflows); - $repository = $this->prophesize(EntityWorkflowAttachmentRepository::class); - $repository->findByStoredObject(Argument::type(StoredObject::class))->willReturn([]); - - return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $repository->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable())); - } - - /** - * @dataProvider provideDataAllowedByWorkflowReadOperationByAttachment - * - * @param list $entityWorkflows - */ - public function testAllowedByWorkflowReadByAttachment( - array $entityWorkflows, - User $user, - string $expected, - ?\DateTimeImmutable $atDate, - string $message, - ): void { - // all entities must have this workflow name, so we are ok to set it here - foreach ($entityWorkflows as $entityWorkflow) { - $entityWorkflow->setWorkflowName('dummy'); - } - $helper = $this->buildHelperForAttachment($entityWorkflows, $user, $atDate); - - self::assertEquals($expected, $helper->isAllowedByWorkflowForReadOperation(new StoredObject()), $message); - } - - public static function provideDataAllowedByWorkflowReadOperationByAttachment(): iterable - { - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); - - yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(), - 'abstain because the user is not present as a dest user']; - - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futureDestUsers[] = $user = new User(); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); - - yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(), - 'force grant because the user is a current user']; - - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futureDestUsers[] = $user = new User(); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); - - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futureDestUsers[] = new User(); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); - - yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(), - 'force grant because the user was a previous user']; - - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futurePersonSignatures[] = new Person(); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); - - yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(), - 'Abstain: there is a signature for person, but the attachment is not concerned']; - } - - /** - * @dataProvider provideDataAllowedByWorkflowWriteOperationByAttachment - * - * @param list $entityWorkflows - */ - public function testAllowedByWorkflowWriteByAttachment( - array $entityWorkflows, - User $user, - string $expected, - ?\DateTimeImmutable $atDate, - string $message, - ): void { - // all entities must have this workflow name, so we are ok to set it here - foreach ($entityWorkflows as $entityWorkflow) { - $entityWorkflow->setWorkflowName('dummy'); - } - $helper = $this->buildHelperForAttachment($entityWorkflows, $user, $atDate); - - self::assertEquals($expected, $helper->isAllowedByWorkflowForWriteOperation(new StoredObject()), $message); - } - - public static function provideDataAllowedByWorkflowWriteOperationByAttachment(): iterable - { - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); - - yield [[], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(), - 'abstain because there is no workflow']; - - yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(), - 'abstain because the user is not present as a dest user (and attachment)']; - - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futureDestUsers[] = $user = new User(); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); - - yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(), - 'force grant because the user is a current user']; - - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futureDestUsers[] = $user = new User(); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); - - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futureDestUsers[] = new User(); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); - - yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(), - 'force grant because the user was a previous user']; - - yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(), - 'abstain because the user was not a previous user']; - - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futureDestUsers[] = $user = new User(); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $entityWorkflow->setStep('final_positive', $dto, 'to_final_positive', new \DateTimeImmutable(), new User()); - $entityWorkflow->getCurrentStep()->setIsFinal(true); - - yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(), - 'force denied: user was a previous user, but it is finalized positive']; - - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futureDestUsers[] = $user = new User(); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable()); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable()); - $entityWorkflow->getCurrentStep()->setIsFinal(true); - - yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(), - 'abstain: user was a previous user, it is finalized, but finalized negative']; - - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futureDestUsers[] = $user = new User(); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futurePersonSignatures[] = new Person(); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); - $signature = $entityWorkflow->getCurrentStep()->getSignatures()->first(); - $signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate(new \DateTimeImmutable()); - - yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(), - 'abstain: there is a signature, but not on the attachment']; - - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futureDestUsers[] = $user = new User(); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futurePersonSignatures[] = new Person(); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); - - yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(), - 'abstain: there is a signature, but the signature is not on the attachment']; - - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futureDestUsers[] = $user = new User(); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futurePersonSignatures[] = new Person(); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); - $signature = $entityWorkflow->getCurrentStep()->getSignatures()->first(); - $signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate(new \DateTimeImmutable()); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable(), new User()); - $entityWorkflow->getCurrentStep()->setIsFinal(true); - - yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(), - 'abstain: there is a signature on a canceled workflow']; - - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futureDestUsers[] = $user = new User(); - $entityWorkflow->setStep('sent_external', $dto, 'to_sent_external', new \DateTimeImmutable(), $user); - - yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(), - 'force denied: the workflow is sent to an external user']; - } - - /** - * @param list $entityWorkflows - */ - private function buildHelperForAttachment(array $entityWorkflows, User $user, ?\DateTimeImmutable $atDateTime): WorkflowRelatedEntityPermissionHelper - { - $security = $this->prophesize(Security::class); - $security->getUser()->willReturn($user); - - $entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class); - $entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->shouldNotBeCalled(); - - $repository = $this->prophesize(EntityWorkflowAttachmentRepository::class); - $attachments = []; - foreach ($entityWorkflows as $entityWorkflow) { - $attachments[] = new EntityWorkflowAttachment('dummy', ['id' => 1], $entityWorkflow, new StoredObject()); - } - $repository->findByStoredObject(Argument::type(StoredObject::class))->willReturn($attachments); - - return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $repository->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable())); + return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable())); } private static function buildRegistry(): Registry diff --git a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php index 3775869e7..a48107156 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php +++ b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php @@ -103,7 +103,10 @@ class NotificationOnTransition implements EventSubscriberInterface foreach ($dests as $subscriber) { if ( + // prevent to send a notification to the one who created the step $this->security->getUser() === $subscriber + // prevent to send a notification if the user applyied a signature on the previous step + || $this->isStepCreatedByPreviousSignature($entityWorkflow, $subscriber) ) { continue; } @@ -131,4 +134,31 @@ class NotificationOnTransition implements EventSubscriberInterface $this->entityManager->persist($notification); } } + + /** + * Checks if the current step in the workflow was created by a previous signature of the specified user. + * + * This method retrieves the current step of the workflow and its preceding step. It iterates through + * the signatures of the preceding step to verify if the provided user is the signer of any of those + * signatures. Returns true if the user matches any signer; otherwise, returns false. + * + * @param EntityWorkflow $entityWorkflow the workflow entity containing the current step and its details + * @param User $user the user to check against the signatures of the previous step in the workflow + * + * @return bool true if the specified user created the step via a previous signature, false otherwise + */ + private function isStepCreatedByPreviousSignature(EntityWorkflow $entityWorkflow, User $user): bool + { + $step = $entityWorkflow->getCurrentStepChained(); + $previous = $step->getPrevious(); + + + foreach ($previous->getSignatures() as $signature) { + if ($signature->getSigner() === $user) { + return true; + } + } + + return false; + } } diff --git a/src/Bundle/ChillMainBundle/Workflow/Helper/WorkflowRelatedEntityPermissionHelper.php b/src/Bundle/ChillMainBundle/Workflow/Helper/WorkflowRelatedEntityPermissionHelper.php index 6d393b318..4c7c3e130 100644 --- a/src/Bundle/ChillMainBundle/Workflow/Helper/WorkflowRelatedEntityPermissionHelper.php +++ b/src/Bundle/ChillMainBundle/Workflow/Helper/WorkflowRelatedEntityPermissionHelper.php @@ -11,12 +11,9 @@ declare(strict_types=1); namespace Chill\MainBundle\Workflow\Helper; -use Chill\DocStoreBundle\Entity\StoredObject; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; -use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment; use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum; -use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository; use Chill\MainBundle\Workflow\EntityWorkflowManager; use Symfony\Component\Clock\ClockInterface; use Symfony\Component\Security\Core\Security; @@ -52,48 +49,28 @@ use Symfony\Component\Workflow\Registry; * the workflow denys write operations; * - if there is no case above and the user is involved in the workflow (is part of the current step, of a step before), the user is granted; */ -class WorkflowRelatedEntityPermissionHelper +final readonly class WorkflowRelatedEntityPermissionHelper implements WorkflowRelatedEntityPermissionHelperInterface { - public const FORCE_GRANT = 'FORCE_GRANT'; - public const FORCE_DENIED = 'FORCE_DENIED'; - public const ABSTAIN = 'ABSTAIN'; - public function __construct( - private readonly Security $security, - private readonly EntityWorkflowManager $entityWorkflowManager, - private readonly EntityWorkflowAttachmentRepository $entityWorkflowAttachmentRepository, - private readonly Registry $registry, - private readonly ClockInterface $clock, + private Security $security, + private EntityWorkflowManager $entityWorkflowManager, + private Registry $registry, + private ClockInterface $clock, ) {} /** - * @param object $entity The entity may be an + * @param object $entity The entity may be an object that is a related entity of a workflow, or an EntityWorkflow itself * * @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN' */ public function isAllowedByWorkflowForReadOperation(object $entity): string { - if ($entity instanceof StoredObject) { - $attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($entity); - $entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments); - $isAttached = true; - } else { - $entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity); - $isAttached = false; - } - - if ([] === $entityWorkflows) { - return self::ABSTAIN; - } + $entityWorkflows = $entity instanceof EntityWorkflow ? [$entity] : $this->entityWorkflowManager->findByRelatedEntity($entity); if ($this->isUserInvolvedInAWorkflow($entityWorkflows)) { return self::FORCE_GRANT; } - if ($isAttached) { - return self::ABSTAIN; - } - // give a view permission if there is a Person signature pending, or in the 12 hours following // the signature last state foreach ($entityWorkflows as $workflow) { @@ -117,24 +94,20 @@ class WorkflowRelatedEntityPermissionHelper } /** + * @param object $entity the entity may be an object which is the related entity of a workflow + * * @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN' */ public function isAllowedByWorkflowForWriteOperation(object $entity): string { - if ($entity instanceof StoredObject) { - $attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($entity); - $entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments); - $isAttached = true; - } else { - $entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity); - $isAttached = false; - } + $entityWorkflows = $entity instanceof EntityWorkflow ? [$entity] : $this->entityWorkflowManager->findByRelatedEntity($entity); if ([] === $entityWorkflows) { return self::ABSTAIN; } - // if a workflow is finalized positive, anyone is allowed to edit the document anymore + + // if a workflow is finalized positive or isSentExternal, no one is allowed to edit the document anymore foreach ($entityWorkflows as $entityWorkflow) { $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); $marking = $workflow->getMarkingStore()->getMarking($entityWorkflow); @@ -147,39 +120,64 @@ class WorkflowRelatedEntityPermissionHelper // the workflow is final, and final positive, or is sentExternal, so we stop here. return self::FORCE_DENIED; } - if ( - // if not finalized positive - $entityWorkflow->isFinal() && !($placeMetadata['isFinalPositive'] ?? false) - ) { - return self::ABSTAIN; - } } } - $runningWorkflows = array_filter($entityWorkflows, fn (EntityWorkflow $ew) => !$ew->isFinal()); + // if there is a signature on a **running workflow**, no one is allowed edit anymore, except if the workflow is canceled + foreach ($entityWorkflows as $entityWorkflow) { + // if the workflow is canceled, we ignore it + $isFinalNegative = false; + if ($entityWorkflow->isFinal()) { + $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); + $marking = $workflow->getMarkingStore()->getMarking($entityWorkflow); + foreach ($marking->getPlaces() as $place => $int) { + $placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place); + if (isset($placeMetadata['isFinalPositive']) && false === $placeMetadata['isFinalPositive']) { + $isFinalNegative = true; + } + } + } + if ($isFinalNegative) { + continue; + } - // if there is a signature on a **running workflow**, no one is allowed edit the workflow anymore - if (!$isAttached) { - foreach ($runningWorkflows as $entityWorkflow) { - foreach ($entityWorkflow->getSteps() as $step) { - foreach ($step->getSignatures() as $signature) { - if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) { - return self::FORCE_DENIED; - } + foreach ($entityWorkflow->getSteps() as $step) { + foreach ($step->getSignatures() as $signature) { + if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) { + return self::FORCE_DENIED; } } } } + // if all workflows are finalized negative (= canceled), we should abstain + $runningWorkflows = []; + foreach ($entityWorkflows as $entityWorkflow) { + $isFinalNegative = false; + if ($entityWorkflow->isFinal()) { + $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); + $marking = $workflow->getMarkingStore()->getMarking($entityWorkflow); + foreach ($marking->getPlaces() as $place => $int) { + $placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place); + if (isset($placeMetadata['isFinalPositive']) && false === $placeMetadata['isFinalPositive']) { + $isFinalNegative = true; + } + } + } + if (!$isFinalNegative) { + $runningWorkflows[] = $entityWorkflow; + } + } + + if ([] === $runningWorkflows) { + return self::ABSTAIN; + } + // allow only the users involved if ($this->isUserInvolvedInAWorkflow($runningWorkflows)) { return self::FORCE_GRANT; } - if ($isAttached) { - return self::ABSTAIN; - } - return self::FORCE_DENIED; } diff --git a/src/Bundle/ChillMainBundle/Workflow/Helper/WorkflowRelatedEntityPermissionHelperInterface.php b/src/Bundle/ChillMainBundle/Workflow/Helper/WorkflowRelatedEntityPermissionHelperInterface.php new file mode 100644 index 000000000..eefadf0f8 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Workflow/Helper/WorkflowRelatedEntityPermissionHelperInterface.php @@ -0,0 +1,63 @@ +addOrderBy('e.id', 'ASC'); return parent::orderQuery($action, $query, $request, $paginator); } + + protected function getQueryResult( + string $action, + Request $request, + int $totalItems, + PaginatorInterface $paginator, + ?FilterOrderHelper $filterOrder = null, + ) { + if (0 === $totalItems) { + return []; + } + + if (!$filterOrder instanceof FilterOrderHelper) { + return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder); + } + + $queryString = $filterOrder->getQueryString(); + $activeFilter = $filterOrder->getCheckboxData('activeFilter'); + $nb = $this->repository->countFilteredEvaluations($queryString, $activeFilter); + + $paginator = $this->getPaginatorFactory()->create($nb); + + return $this->repository->findFilteredEvaluations($queryString, $activeFilter, $paginator->getCurrentPageFirstItemNumber(), $paginator->getItemsPerPage()); + } + + protected function countEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null): int + { + if (!$filterOrder instanceof FilterOrderHelper) { + return parent::countEntities($action, $request, $filterOrder); + } + + return $this->repository->countFilteredEvaluations( + $filterOrder->getQueryString(), + $filterOrder->getCheckboxData('activeFilter') + ); + } + + protected function buildFilterOrderHelper(string $action, Request $request): ?FilterOrderHelper + { + return $this->getFilterOrderHelperFactory() + ->create(self::class) + ->addSearchBox(['label']) + ->addCheckbox('activeFilter', [true => 'Active', false => 'Inactive'], ['Active']) + ->build(); + } } diff --git a/src/Bundle/ChillPersonBundle/Controller/SocialWork/GoalController.php b/src/Bundle/ChillPersonBundle/Controller/SocialWork/GoalController.php index df57b012a..a746a89f1 100644 --- a/src/Bundle/ChillPersonBundle/Controller/SocialWork/GoalController.php +++ b/src/Bundle/ChillPersonBundle/Controller/SocialWork/GoalController.php @@ -13,14 +13,68 @@ namespace Chill\PersonBundle\Controller\SocialWork; use Chill\MainBundle\CRUD\Controller\CRUDController; use Chill\MainBundle\Pagination\PaginatorInterface; +use Chill\MainBundle\Templating\Listing\FilterOrderHelper; +use Chill\PersonBundle\Repository\SocialWork\GoalRepository; use Symfony\Component\HttpFoundation\Request; class GoalController extends CRUDController { + public function __construct(private readonly GoalRepository $repository) {} + protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator) { $query->addOrderBy('e.id', 'ASC'); return parent::orderQuery($action, $query, $request, $paginator); } + + protected function getQueryResult( + string $action, + Request $request, + int $totalItems, + PaginatorInterface $paginator, + ?FilterOrderHelper $filterOrder = null, + ) { + if (0 === $totalItems) { + return []; + } + + if (!$filterOrder instanceof FilterOrderHelper) { + return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder); + } + + $queryString = $filterOrder->getQueryString(); + $activeFilter = $filterOrder->getCheckboxData('activeFilter'); + $nb = $this->repository->countFilteredGoals($queryString, $activeFilter); + + $paginator = $this->getPaginatorFactory()->create($nb); + + return $this->repository->findFilteredGoals( + $queryString, + $activeFilter, + $paginator->getCurrentPageFirstItemNumber(), + $paginator->getItemsPerPage() + ); + } + + protected function countEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null): int + { + if (!$filterOrder instanceof FilterOrderHelper) { + return parent::countEntities($action, $request, $filterOrder); + } + + return $this->repository->countFilteredGoals( + $filterOrder->getQueryString(), + $filterOrder->getCheckboxData('activeFilter') + ); + } + + protected function buildFilterOrderHelper(string $action, Request $request): ?FilterOrderHelper + { + return $this->getFilterOrderHelperFactory() + ->create(self::class) + ->addSearchBox(['label']) + ->addCheckbox('activeFilter', [true => 'Active', false => 'Inactive'], ['Active']) + ->build(); + } } diff --git a/src/Bundle/ChillPersonBundle/Controller/SocialWork/ResultController.php b/src/Bundle/ChillPersonBundle/Controller/SocialWork/ResultController.php index 274f7136e..9cc8c38b3 100644 --- a/src/Bundle/ChillPersonBundle/Controller/SocialWork/ResultController.php +++ b/src/Bundle/ChillPersonBundle/Controller/SocialWork/ResultController.php @@ -13,14 +13,68 @@ namespace Chill\PersonBundle\Controller\SocialWork; use Chill\MainBundle\CRUD\Controller\CRUDController; use Chill\MainBundle\Pagination\PaginatorInterface; +use Chill\MainBundle\Templating\Listing\FilterOrderHelper; +use Chill\PersonBundle\Repository\SocialWork\ResultRepository; use Symfony\Component\HttpFoundation\Request; class ResultController extends CRUDController { + public function __construct(private readonly ResultRepository $repository) {} + protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator) { $query->addOrderBy('e.id', 'ASC'); return parent::orderQuery($action, $query, $request, $paginator); } + + protected function getQueryResult( + string $action, + Request $request, + int $totalItems, + PaginatorInterface $paginator, + ?FilterOrderHelper $filterOrder = null, + ) { + if (0 === $totalItems) { + return []; + } + + if (!$filterOrder instanceof FilterOrderHelper) { + return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder); + } + + $queryString = $filterOrder->getQueryString(); + $activeFilter = $filterOrder->getCheckboxData('activeFilter'); + $nb = $this->repository->countFilteredResults($queryString, $activeFilter); + + $paginator = $this->getPaginatorFactory()->create($nb); + + return $this->repository->findFilteredResults( + $queryString, + $activeFilter, + $paginator->getCurrentPageFirstItemNumber(), + $paginator->getItemsPerPage() + ); + } + + protected function countEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null): int + { + if (!$filterOrder instanceof FilterOrderHelper) { + return parent::countEntities($action, $request, $filterOrder); + } + + return $this->repository->countFilteredResults( + $filterOrder->getQueryString(), + $filterOrder->getCheckboxData('activeFilter') + ); + } + + protected function buildFilterOrderHelper(string $action, Request $request): ?FilterOrderHelper + { + return $this->getFilterOrderHelperFactory() + ->create(self::class) + ->addSearchBox(['label']) + ->addCheckbox('activeFilter', [true => 'Active', false => 'Inactive'], ['Active']) + ->build(); + } } diff --git a/src/Bundle/ChillPersonBundle/Controller/SocialWork/SocialActionController.php b/src/Bundle/ChillPersonBundle/Controller/SocialWork/SocialActionController.php index 69bbf7ace..9bdccda3a 100644 --- a/src/Bundle/ChillPersonBundle/Controller/SocialWork/SocialActionController.php +++ b/src/Bundle/ChillPersonBundle/Controller/SocialWork/SocialActionController.php @@ -13,14 +13,68 @@ namespace Chill\PersonBundle\Controller\SocialWork; use Chill\MainBundle\CRUD\Controller\CRUDController; use Chill\MainBundle\Pagination\PaginatorInterface; +use Chill\MainBundle\Templating\Listing\FilterOrderHelper; +use Chill\PersonBundle\Repository\SocialWork\SocialActionRepository; use Symfony\Component\HttpFoundation\Request; class SocialActionController extends CRUDController { + public function __construct(private readonly SocialActionRepository $repository) {} + protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator) { $query->addOrderBy('e.ordering', 'ASC'); return parent::orderQuery($action, $query, $request, $paginator); } + + protected function getQueryResult( + string $action, + Request $request, + int $totalItems, + PaginatorInterface $paginator, + ?FilterOrderHelper $filterOrder = null, + ) { + if (0 === $totalItems) { + return []; + } + + if (!$filterOrder instanceof FilterOrderHelper) { + return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder); + } + + $queryString = $filterOrder->getQueryString(); + $activeFilter = $filterOrder->getCheckboxData('activeFilter'); + $nb = $this->repository->countFilteredSocialActions($queryString, $activeFilter); + + $paginator = $this->getPaginatorFactory()->create($nb); + + return $this->repository->findFilteredSocialActions( + $queryString, + $activeFilter, + $paginator->getCurrentPageFirstItemNumber(), + $paginator->getItemsPerPage() + ); + } + + protected function countEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null): int + { + if (!$filterOrder instanceof FilterOrderHelper) { + return parent::countEntities($action, $request, $filterOrder); + } + + return $this->repository->countFilteredSocialActions( + $filterOrder->getQueryString(), + $filterOrder->getCheckboxData('activeFilter') + ); + } + + protected function buildFilterOrderHelper(string $action, Request $request): ?FilterOrderHelper + { + return $this->getFilterOrderHelperFactory() + ->create(self::class) + ->addSearchBox(['label']) + ->addCheckbox('activeFilter', [true => 'Active', false => 'Inactive'], ['Active']) + ->build(); + } } diff --git a/src/Bundle/ChillPersonBundle/Controller/SocialWork/SocialIssueController.php b/src/Bundle/ChillPersonBundle/Controller/SocialWork/SocialIssueController.php index 316417583..df76b60f2 100644 --- a/src/Bundle/ChillPersonBundle/Controller/SocialWork/SocialIssueController.php +++ b/src/Bundle/ChillPersonBundle/Controller/SocialWork/SocialIssueController.php @@ -13,11 +13,15 @@ namespace Chill\PersonBundle\Controller\SocialWork; use Chill\MainBundle\CRUD\Controller\CRUDController; use Chill\MainBundle\Pagination\PaginatorInterface; +use Chill\MainBundle\Templating\Listing\FilterOrderHelper; +use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; class SocialIssueController extends CRUDController { + public function __construct(private readonly SocialIssueRepository $repository) {} + protected function createFormFor(string $action, $entity, ?string $formClass = null, array $formOptions = []): FormInterface { if ('new' === $action) { @@ -37,4 +41,54 @@ class SocialIssueController extends CRUDController return parent::orderQuery($action, $query, $request, $paginator); } + + protected function getQueryResult( + string $action, + Request $request, + int $totalItems, + PaginatorInterface $paginator, + ?FilterOrderHelper $filterOrder = null, + ) { + if (0 === $totalItems) { + return []; + } + + if (!$filterOrder instanceof FilterOrderHelper) { + return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder); + } + + $queryString = $filterOrder->getQueryString(); + $activeFilter = $filterOrder->getCheckboxData('activeFilter'); + $nb = $this->repository->countFilteredSocialIssues($queryString, $activeFilter); + + $paginator = $this->getPaginatorFactory()->create($nb); + + return $this->repository->findFilteredSocialIssues( + $queryString, + $activeFilter, + $paginator->getCurrentPageFirstItemNumber(), + $paginator->getItemsPerPage() + ); + } + + protected function countEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null): int + { + if (!$filterOrder instanceof FilterOrderHelper) { + return parent::countEntities($action, $request, $filterOrder); + } + + return $this->repository->countFilteredSocialIssues( + $filterOrder->getQueryString(), + $filterOrder->getCheckboxData('activeFilter') + ); + } + + protected function buildFilterOrderHelper(string $action, Request $request): ?FilterOrderHelper + { + return $this->getFilterOrderHelperFactory() + ->create(self::class) + ->addSearchBox(['label']) + ->addCheckbox('activeFilter', [true => 'Active', false => 'Inactive'], ['Active']) + ->build(); + } } diff --git a/src/Bundle/ChillPersonBundle/Controller/SocialWorkEvaluationApiController.php b/src/Bundle/ChillPersonBundle/Controller/SocialWorkEvaluationApiController.php index 27a7acbf5..8c138d44a 100644 --- a/src/Bundle/ChillPersonBundle/Controller/SocialWorkEvaluationApiController.php +++ b/src/Bundle/ChillPersonBundle/Controller/SocialWorkEvaluationApiController.php @@ -38,7 +38,7 @@ class SocialWorkEvaluationApiController extends AbstractController $pagination->getCurrentPageFirstItemNumber(), $pagination->getItemsPerPage() ); - $collection = new Collection($evaluations, $pagination); + $collection = new Collection(array_values($evaluations), $pagination); return $this->json($collection, Response::HTTP_OK, [], ['groups' => ['read']]); } diff --git a/src/Bundle/ChillPersonBundle/Controller/SocialWorkGoalApiController.php b/src/Bundle/ChillPersonBundle/Controller/SocialWorkGoalApiController.php index 2a7f39712..b0da74c7f 100644 --- a/src/Bundle/ChillPersonBundle/Controller/SocialWorkGoalApiController.php +++ b/src/Bundle/ChillPersonBundle/Controller/SocialWorkGoalApiController.php @@ -25,14 +25,15 @@ class SocialWorkGoalApiController extends ApiController public function listBySocialAction(Request $request, SocialAction $action): Response { - $totalItems = $this->goalRepository->countBySocialActionWithDescendants($action); - $paginator = $this->getPaginatorFactory()->create($totalItems); + $totalItems = $this->goalRepository->countBySocialActionWithDescendants($action, true); + $paginator = $this->paginator->create($totalItems); $entities = $this->goalRepository->findBySocialActionWithDescendants( $action, ['id' => 'ASC'], $paginator->getItemsPerPage(), - $paginator->getCurrentPageFirstItemNumber() + $paginator->getCurrentPageFirstItemNumber(), + onlyActive: true ); $model = new Collection($entities, $paginator); diff --git a/src/Bundle/ChillPersonBundle/Controller/SocialWorkResultApiController.php b/src/Bundle/ChillPersonBundle/Controller/SocialWorkResultApiController.php index 9b97d8129..c758475be 100644 --- a/src/Bundle/ChillPersonBundle/Controller/SocialWorkResultApiController.php +++ b/src/Bundle/ChillPersonBundle/Controller/SocialWorkResultApiController.php @@ -25,14 +25,15 @@ class SocialWorkResultApiController extends ApiController public function listByGoal(Request $request, Goal $goal): Response { - $totalItems = $this->resultRepository->countByGoal($goal); + $totalItems = $this->resultRepository->countByGoal($goal, true); $paginator = $this->getPaginatorFactory()->create($totalItems); $entities = $this->resultRepository->findByGoal( $goal, ['id' => 'ASC'], $paginator->getItemsPerPage(), - $paginator->getCurrentPageFirstItemNumber() + $paginator->getCurrentPageFirstItemNumber(), + onlyActive: true, ); $model = new Collection($entities, $paginator); @@ -42,14 +43,15 @@ class SocialWorkResultApiController extends ApiController public function listBySocialAction(Request $request, SocialAction $action): Response { - $totalItems = $this->resultRepository->countBySocialActionWithDescendants($action); + $totalItems = $this->resultRepository->countBySocialActionWithDescendants($action, true); $paginator = $this->getPaginatorFactory()->create($totalItems); $entities = $this->resultRepository->findBySocialActionWithDescendants( $action, ['id' => 'ASC'], $paginator->getItemsPerPage(), - $paginator->getCurrentPageFirstItemNumber() + $paginator->getCurrentPageFirstItemNumber(), + onlyActive: true ); $model = new Collection($entities, $paginator); diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CenterAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CenterAggregator.php index 6f4082d46..bf9901207 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CenterAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CenterAggregator.php @@ -19,6 +19,7 @@ use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface; use Chill\PersonBundle\Export\Declarations; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Contracts\Translation\TranslatorInterface; final readonly class CenterAggregator implements AggregatorInterface { @@ -27,6 +28,7 @@ final readonly class CenterAggregator implements AggregatorInterface public function __construct( private CenterRepositoryInterface $centerRepository, private RollingDateConverterInterface $rollingDateConverter, + private TranslatorInterface $translator, ) {} public function buildForm(FormBuilderInterface $builder): void @@ -62,7 +64,7 @@ final readonly class CenterAggregator implements AggregatorInterface { return function (int|string|null $value) { if (null === $value || '' === $value) { - return ''; + return $this->translator->trans('person.export.aggregator.by_center.no_center'); } if ('_header' === $value) { @@ -94,15 +96,18 @@ final readonly class CenterAggregator implements AggregatorInterface $atDate = 'pers_center_agg_at_date'; $qb->leftJoin('person.centerHistory', $alias); - $qb - ->andWhere( - $qb->expr()->lte($alias.'.startDate', ':'.$atDate), - )->andWhere( - $qb->expr()->orX( - $qb->expr()->isNull($alias.'.endDate'), - $qb->expr()->gt($alias.'.endDate', ':'.$atDate) + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->isNull($alias.'.id'), + $qb->expr()->andX( + $qb->expr()->lte($alias.'.startDate', ':'.$atDate), + $qb->expr()->orX( + $qb->expr()->isNull($alias.'.endDate'), + $qb->expr()->gt($alias.'.endDate', ':'.$atDate) + ) ) - ); + ) + ); $qb->setParameter($atDate, $this->rollingDateConverter->convert($data['at_date'])); $qb->addSelect("IDENTITY({$alias}.center) AS ".self::COLUMN_NAME); diff --git a/src/Bundle/ChillPersonBundle/Export/Helper/FilterListAccompanyingPeriodHelper.php b/src/Bundle/ChillPersonBundle/Export/Helper/FilterListAccompanyingPeriodHelper.php index e631051a0..a62c7ac89 100644 --- a/src/Bundle/ChillPersonBundle/Export/Helper/FilterListAccompanyingPeriodHelper.php +++ b/src/Bundle/ChillPersonBundle/Export/Helper/FilterListAccompanyingPeriodHelper.php @@ -44,12 +44,10 @@ final readonly class FilterListAccompanyingPeriodHelper implements FilterListAcc }; // add filtering on confidential accompanying period. The confidential is applyed on the current status of - // the accompanying period (we do not use the 'calc_date' here - $aclConditionsOrX = $qb->expr()->orX( - // either the current user is the refferer for the course - 'acp.user = :list_acp_current_user', - ); - $qb->setParameter('list_acp_current_user', $user); + // the accompanying period (we do not use the 'calc_date' here) + // + // IMPORTANT: we must NOT bypass selected centers just because the current user is the referrer. + $aclConditionsOrX = $qb->expr()->orX(); $i = 0; foreach ($centers as $center) { @@ -93,6 +91,12 @@ final readonly class FilterListAccompanyingPeriodHelper implements FilterListAcc ++$i; } - $qb->andWhere($aclConditionsOrX); + // Prevent invalid/empty WHERE when no conditions were added (e.g., no centers available) + if (0 === count($aclConditionsOrX->getParts())) { + // No allowed conditions => return no rows + $qb->andWhere('1 = 0'); + } else { + $qb->andWhere($aclConditionsOrX); + } } } diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php index df5ee6c66..d72084bdd 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php @@ -100,9 +100,9 @@ class AccompanyingPeriodWorkRepository implements ObjectRepository $rsm->addRootEntityFromClassMetadata(AccompanyingPeriodWork::class, 'w'); $sql = "SELECT {$rsm} FROM chill_person_accompanying_period_work w - LEFT JOIN chill_person_accompanying_period_work_referrer AS rw ON accompanyingperiodwork_id = w.id - AND (rw.enddate IS NULL OR rw.enddate > CURRENT_DATE) - WHERE accompanyingPeriod_id = :periodId"; + LEFT JOIN chill_person_accompanying_period_work_referrer AS rw ON accompanyingperiodwork_id = w.id + AND (rw.enddate IS NULL OR rw.enddate > CURRENT_DATE) + WHERE accompanyingPeriod_id = :periodId"; // implement filters @@ -136,11 +136,14 @@ class AccompanyingPeriodWorkRepository implements ObjectRepository } // set limit and offset - $sql .= " ORDER BY - CASE WHEN w.enddate IS NULL THEN '-infinity'::timestamp ELSE 'infinity'::timestamp END ASC, - w.startdate DESC, - w.enddate DESC, - w.id DESC"; + $sql .= ' ORDER BY + CASE + WHEN w.enddate IS NULL OR w.enddate > CURRENT_DATE THEN 0 + ELSE 1 + END ASC, + w.startdate DESC, + w.enddate DESC, + w.id DESC'; $sql .= ' LIMIT :limit OFFSET :offset'; diff --git a/src/Bundle/ChillPersonBundle/Repository/SocialWork/EvaluationRepository.php b/src/Bundle/ChillPersonBundle/Repository/SocialWork/EvaluationRepository.php index 1c89a98f6..e78fdb590 100644 --- a/src/Bundle/ChillPersonBundle/Repository/SocialWork/EvaluationRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/SocialWork/EvaluationRepository.php @@ -14,12 +14,16 @@ namespace Chill\PersonBundle\Repository\SocialWork; use Chill\PersonBundle\Entity\SocialWork\Evaluation; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; +use Doctrine\ORM\QueryBuilder; +use Symfony\Component\HttpFoundation\RequestStack; final readonly class EvaluationRepository implements EvaluationRepositoryInterface { private EntityRepository $repository; - public function __construct(EntityManagerInterface $entityManager) + public function __construct(private EntityManagerInterface $entityManager, private RequestStack $requestStack) { $this->repository = $entityManager->getRepository(Evaluation::class); } @@ -65,4 +69,86 @@ final readonly class EvaluationRepository implements EvaluationRepositoryInterfa { return Evaluation::class; } + + private function getLang(): string + { + return $this->requestStack->getCurrentRequest()?->getLocale() ?? 'fr'; + } + + public function getResult( + QueryBuilder $qb, + ?int $start = 0, + ?int $limit = 50, + ?array $orderBy = [], + ): array { + $qb->select('e'); + + $qb + ->setFirstResult($start) + ->setMaxResults($limit); + + foreach ($orderBy as $field => $direction) { + $qb->addOrderBy('e.'.$field, $direction); + } + + return $qb->getQuery()->getResult(); + } + + private function queryByTitle(string $pattern): QueryBuilder + { + $qb = $this->entityManager->createQueryBuilder()->from(Evaluation::class, 'e'); + + // Extract the current locale's value from the JSON `title` and search on it + $qb + ->where($qb->expr()->like('LOWER(UNACCENT(JSON_EXTRACT(e.title, :lang)))', "CONCAT('%', LOWER(UNACCENT(:pattern)), '%')")) + ->setParameter('pattern', $pattern) + ->setParameter('lang', $this->getLang()); + + return $qb; + } + + public function buildFilterBaseQuery(?string $queryString, array $isActive): QueryBuilder + { + if (null !== $queryString) { + $qb = $this->queryByTitle($queryString); + } else { + $qb = $this->entityManager->createQueryBuilder()->from(Evaluation::class, 'e'); + } + + // Add condition based on active/inactive status + if (in_array('Active', $isActive, true) && !in_array('Inactive', $isActive, true)) { + $qb->andWhere('e.active = true'); + } elseif (in_array('Inactive', $isActive, true) && !in_array('Active', $isActive, true)) { + $qb->andWhere('e.active = false'); + } + + return $qb; + } + + public function findFilteredEvaluations( + ?string $queryString = null, + array $isActive = ['active'], + ?int $start = 0, + ?int $limit = 50, + ?array $orderBy = ['title' => 'ASC'], + ): array { + $qb = $this->buildFilterBaseQuery($queryString, $isActive); + + return $this->getResult($qb, $start, $limit, $orderBy); + } + + public function countFilteredEvaluations( + ?string $queryString = null, + array $isActive = ['active'], + ): int { + $qb = $this->buildFilterBaseQuery($queryString, $isActive); + + try { + return $qb + ->select('COUNT(e)') + ->getQuery()->getSingleScalarResult(); + } catch (NoResultException|NonUniqueResultException $e) { + throw new \LogicException('a count query should return one result', previous: $e); + } + } } diff --git a/src/Bundle/ChillPersonBundle/Repository/SocialWork/GoalRepository.php b/src/Bundle/ChillPersonBundle/Repository/SocialWork/GoalRepository.php index ec89c597d..13cfed9a7 100644 --- a/src/Bundle/ChillPersonBundle/Repository/SocialWork/GoalRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/SocialWork/GoalRepository.php @@ -15,21 +15,28 @@ use Chill\PersonBundle\Entity\SocialWork\Goal; use Chill\PersonBundle\Entity\SocialWork\SocialAction; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Clock\ClockInterface; final readonly class GoalRepository implements ObjectRepository { private EntityRepository $repository; - public function __construct(EntityManagerInterface $entityManager) - { + public function __construct( + private EntityManagerInterface $entityManager, + private ClockInterface $clock, + private RequestStack $requestStack, + ) { $this->repository = $entityManager->getRepository(Goal::class); } - public function countBySocialActionWithDescendants(SocialAction $action): int + public function countBySocialActionWithDescendants(SocialAction $action, bool $onlyActive = false): int { - $qb = $this->buildQueryBySocialActionWithDescendants($action); + $qb = $this->buildQueryBySocialActionWithDescendants($action, $onlyActive); $qb->select('COUNT(g)'); return $qb @@ -64,9 +71,9 @@ final readonly class GoalRepository implements ObjectRepository /** * @return Goal[] */ - public function findBySocialActionWithDescendants(SocialAction $action, array $orderBy = [], ?int $limit = null, ?int $offset = null): array + public function findBySocialActionWithDescendants(SocialAction $action, array $orderBy = [], ?int $limit = null, ?int $offset = null, bool $onlyActive = false): array { - $qb = $this->buildQueryBySocialActionWithDescendants($action); + $qb = $this->buildQueryBySocialActionWithDescendants($action, $onlyActive); $qb->select('g'); $qb->andWhere( @@ -101,7 +108,103 @@ final readonly class GoalRepository implements ObjectRepository return Goal::class; } - private function buildQueryBySocialActionWithDescendants(SocialAction $action): QueryBuilder + private function getLang(): string + { + return $this->requestStack->getCurrentRequest()?->getLocale() ?? 'fr'; + } + + public function getResult( + QueryBuilder $qb, + ?int $start = 0, + ?int $limit = 50, + ?array $orderBy = [], + ): array { + $qb->select('g'); + + $qb + ->setFirstResult($start) + ->setMaxResults($limit); + + foreach ($orderBy as $field => $direction) { + $qb->addOrderBy('g.'.$field, $direction); + } + + return $qb->getQuery()->getResult(); + } + + private function queryByTitle(string $pattern): QueryBuilder + { + $qb = $this->entityManager->createQueryBuilder()->from(Goal::class, 'g'); + + // search across locales by extracting the localized value + $qb + ->where($qb->expr()->like('LOWER(UNACCENT(JSON_EXTRACT(g.title, :lang)))', "CONCAT('%', LOWER(UNACCENT(:pattern)), '%')")) + ->setParameter('pattern', $pattern) + ->setParameter('lang', $this->getLang()); + + return $qb; + } + + public function buildFilterBaseQuery(?string $queryString, array $isActive): QueryBuilder + { + if (null !== $queryString) { + $qb = $this->queryByTitle($queryString); + } else { + $qb = $this->entityManager->createQueryBuilder()->from(Goal::class, 'g'); + } + + // Active when desactivationDate is null or in the future + $now = new \DateTime('now'); + if (in_array('Active', $isActive, true) && !in_array('Inactive', $isActive, true)) { + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->isNull('g.desactivationDate'), + $qb->expr()->gt('g.desactivationDate', ':now') + ) + )->setParameter('now', $now); + } elseif (in_array('Inactive', $isActive, true) && !in_array('Active', $isActive, true)) { + $qb->andWhere( + $qb->expr()->andX( + $qb->expr()->isNotNull('g.desactivationDate'), + $qb->expr()->lte('g.desactivationDate', ':now') + ) + )->setParameter('now', $now); + } + + return $qb; + } + + /** + * @return array + */ + public function findFilteredGoals( + ?string $queryString = null, + array $isActive = ['active'], + ?int $start = 0, + ?int $limit = 50, + ?array $orderBy = ['id' => 'ASC'], + ): array { + $qb = $this->buildFilterBaseQuery($queryString, $isActive); + + return $this->getResult($qb, $start, $limit, $orderBy); + } + + public function countFilteredGoals( + ?string $queryString = null, + array $isActive = ['active'], + ): int { + $qb = $this->buildFilterBaseQuery($queryString, $isActive); + + try { + return $qb + ->select('COUNT(g)') + ->getQuery()->getSingleScalarResult(); + } catch (NoResultException|NonUniqueResultException $e) { + throw new \LogicException('a count query should return one result', previous: $e); + } + } + + private function buildQueryBySocialActionWithDescendants(SocialAction $action, bool $onlyActive): QueryBuilder { $actions = $action->getDescendantsWithThis(); @@ -116,6 +219,11 @@ final readonly class GoalRepository implements ObjectRepository } $qb->where($orx); + if ($onlyActive) { + $qb->andWhere('g.desactivationDate > :now OR g.desactivationDate IS NULL') + ->setParameter('now', $this->clock->now()); + } + return $qb; } } diff --git a/src/Bundle/ChillPersonBundle/Repository/SocialWork/ResultRepository.php b/src/Bundle/ChillPersonBundle/Repository/SocialWork/ResultRepository.php index 0d470d2d7..4f0be3977 100644 --- a/src/Bundle/ChillPersonBundle/Repository/SocialWork/ResultRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/SocialWork/ResultRepository.php @@ -16,21 +16,28 @@ use Chill\PersonBundle\Entity\SocialWork\Result; use Chill\PersonBundle\Entity\SocialWork\SocialAction; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Clock\ClockInterface; final readonly class ResultRepository implements ObjectRepository { private EntityRepository $repository; - public function __construct(EntityManagerInterface $entityManager) - { + public function __construct( + private EntityManagerInterface $entityManager, + private ClockInterface $clock, + private RequestStack $requestStack, + ) { $this->repository = $entityManager->getRepository(Result::class); } - public function countByGoal(Goal $goal): int + public function countByGoal(Goal $goal, bool $onlyActive = false): int { - $qb = $this->buildQueryByGoal($goal); + $qb = $this->buildQueryByGoal($goal, $onlyActive); $qb->select('COUNT(r)'); return $qb @@ -38,9 +45,9 @@ final readonly class ResultRepository implements ObjectRepository ->getSingleScalarResult(); } - public function countBySocialActionWithDescendants(SocialAction $action): int + public function countBySocialActionWithDescendants(SocialAction $action, bool $onlyActive = false): int { - $qb = $this->buildQueryBySocialActionWithDescendants($action); + $qb = $this->buildQueryBySocialActionWithDescendants($action, $onlyActive); $qb->select('COUNT(r)'); return $qb @@ -75,9 +82,9 @@ final readonly class ResultRepository implements ObjectRepository /** * @return array */ - public function findByGoal(Goal $goal, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array + public function findByGoal(Goal $goal, ?array $orderBy = null, ?int $limit = null, ?int $offset = null, bool $onlyActive = false): array { - $qb = $this->buildQueryByGoal($goal); + $qb = $this->buildQueryByGoal($goal, $onlyActive); if (null !== $orderBy) { foreach ($orderBy as $sort => $order) { @@ -96,9 +103,9 @@ final readonly class ResultRepository implements ObjectRepository /** * @return Result[] */ - public function findBySocialActionWithDescendants(SocialAction $action, array $orderBy = [], ?int $limit = null, ?int $offset = null): array + public function findBySocialActionWithDescendants(SocialAction $action, array $orderBy = [], ?int $limit = null, ?int $offset = null, bool $onlyActive = false): array { - $qb = $this->buildQueryBySocialActionWithDescendants($action); + $qb = $this->buildQueryBySocialActionWithDescendants($action, $onlyActive); $qb->select('r'); foreach ($orderBy as $sort => $order) { @@ -125,17 +132,116 @@ final readonly class ResultRepository implements ObjectRepository return Result::class; } - private function buildQueryByGoal(Goal $goal): QueryBuilder + private function getLang(): string + { + return $this->requestStack->getCurrentRequest()?->getLocale() ?? 'fr'; + } + + public function getResult( + QueryBuilder $qb, + ?int $start = 0, + ?int $limit = 50, + ?array $orderBy = [], + ): array { + $qb->select('r'); + + $qb + ->setFirstResult($start) + ->setMaxResults($limit); + + foreach ($orderBy as $field => $direction) { + $qb->addOrderBy('r.'.$field, $direction); + } + + return $qb->getQuery()->getResult(); + } + + private function queryByTitle(string $pattern): QueryBuilder + { + $qb = $this->entityManager->createQueryBuilder()->from(Result::class, 'r'); + + $qb + ->where($qb->expr()->like('LOWER(UNACCENT(JSON_EXTRACT(r.title, :lang)))', "CONCAT('%', LOWER(UNACCENT(:pattern)), '%')")) + ->setParameter('pattern', $pattern) + ->setParameter('lang', $this->getLang()); + + return $qb; + } + + public function buildFilterBaseQuery(?string $queryString, array $isActive): QueryBuilder + { + if (null !== $queryString) { + $qb = $this->queryByTitle($queryString); + } else { + $qb = $this->entityManager->createQueryBuilder()->from(Result::class, 'r'); + } + + $now = new \DateTime('now'); + if (in_array('Active', $isActive, true) && !in_array('Inactive', $isActive, true)) { + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->isNull('r.desactivationDate'), + $qb->expr()->gt('r.desactivationDate', ':now') + ) + )->setParameter('now', $now); + } elseif (in_array('Inactive', $isActive, true) && !in_array('Active', $isActive, true)) { + $qb->andWhere( + $qb->expr()->andX( + $qb->expr()->isNotNull('r.desactivationDate'), + $qb->expr()->lte('r.desactivationDate', ':now') + ) + )->setParameter('now', $now); + } + + return $qb; + } + + /** + * @return array + */ + public function findFilteredResults( + ?string $queryString = null, + array $isActive = ['active'], + ?int $start = 0, + ?int $limit = 50, + ?array $orderBy = ['id' => 'ASC'], + ): array { + $qb = $this->buildFilterBaseQuery($queryString, $isActive); + + return $this->getResult($qb, $start, $limit, $orderBy); + } + + public function countFilteredResults( + ?string $queryString = null, + array $isActive = ['active'], + ): int { + $qb = $this->buildFilterBaseQuery($queryString, $isActive); + + try { + return $qb + ->select('COUNT(r)') + ->getQuery()->getSingleScalarResult(); + } catch (NoResultException|NonUniqueResultException $e) { + throw new \LogicException('a count query should return one result', previous: $e); + } + } + + private function buildQueryByGoal(Goal $goal, bool $onlyActive): QueryBuilder { $qb = $this->repository->createQueryBuilder('r'); $qb->where(':goal MEMBER OF r.goals') ->setParameter('goal', $goal); + if ($onlyActive) { + $qb->andWhere('r.desactivationDate > :now OR r.desactivationDate IS NULL') + ->setParameter('now', $this->clock->now()); + } + return $qb; } - private function buildQueryBySocialActionWithDescendants(SocialAction $action): QueryBuilder + private function buildQueryBySocialActionWithDescendants(SocialAction $action, bool $onlyActive = false): QueryBuilder { $actions = $action->getDescendantsWithThis(); @@ -150,6 +256,11 @@ final readonly class ResultRepository implements ObjectRepository } $qb->where($orx); + if ($onlyActive) { + $qb->andWhere('r.desactivationDate > :now OR r.desactivationDate IS NULL') + ->setParameter('now', $this->clock->now()); + } + return $qb; } } diff --git a/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialActionRepository.php b/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialActionRepository.php index 4d37cb3d4..81da2c60c 100644 --- a/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialActionRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialActionRepository.php @@ -14,14 +14,17 @@ namespace Chill\PersonBundle\Repository\SocialWork; use Chill\PersonBundle\Entity\SocialWork\SocialAction; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; +use Symfony\Component\HttpFoundation\RequestStack; final readonly class SocialActionRepository implements ObjectRepository { private EntityRepository $repository; - public function __construct(EntityManagerInterface $entityManager) + public function __construct(private EntityManagerInterface $entityManager, private RequestStack $requestStack) { $this->repository = $entityManager->getRepository(SocialAction::class); } @@ -84,6 +87,100 @@ final readonly class SocialActionRepository implements ObjectRepository return SocialAction::class; } + private function getLang(): string + { + return $this->requestStack->getCurrentRequest()?->getLocale() ?? 'fr'; + } + + public function getResult( + QueryBuilder $qb, + ?int $start = 0, + ?int $limit = 50, + ?array $orderBy = [], + ): array { + $qb->select('sa'); + + $qb + ->setFirstResult($start) + ->setMaxResults($limit); + + foreach ($orderBy as $field => $direction) { + $qb->addOrderBy('sa.'.$field, $direction); + } + + return $qb->getQuery()->getResult(); + } + + private function queryByTitle(string $pattern): QueryBuilder + { + $qb = $this->entityManager->createQueryBuilder()->from(SocialAction::class, 'sa'); + + $qb + ->where($qb->expr()->like('LOWER(UNACCENT(JSON_EXTRACT(sa.title, :lang)))', "CONCAT('%', LOWER(UNACCENT(:pattern)), '%')")) + ->setParameter('pattern', $pattern) + ->setParameter('lang', $this->getLang()); + + return $qb; + } + + public function buildFilterBaseQuery(?string $queryString, array $isActive): QueryBuilder + { + if (null !== $queryString) { + $qb = $this->queryByTitle($queryString); + } else { + $qb = $this->entityManager->createQueryBuilder()->from(SocialAction::class, 'sa'); + } + + $now = new \DateTime('now'); + if (in_array('Active', $isActive, true) && !in_array('Inactive', $isActive, true)) { + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->isNull('sa.desactivationDate'), + $qb->expr()->gt('sa.desactivationDate', ':now') + ) + )->setParameter('now', $now); + } elseif (in_array('Inactive', $isActive, true) && !in_array('Active', $isActive, true)) { + $qb->andWhere( + $qb->expr()->andX( + $qb->expr()->isNotNull('sa.desactivationDate'), + $qb->expr()->lte('sa.desactivationDate', ':now') + ) + )->setParameter('now', $now); + } + + return $qb; + } + + /** + * @return array + */ + public function findFilteredSocialActions( + ?string $queryString = null, + array $isActive = ['active'], + ?int $start = 0, + ?int $limit = 50, + ?array $orderBy = ['ordering' => 'ASC'], + ): array { + $qb = $this->buildFilterBaseQuery($queryString, $isActive); + + return $this->getResult($qb, $start, $limit, $orderBy); + } + + public function countFilteredSocialActions( + ?string $queryString = null, + array $isActive = ['active'], + ): int { + $qb = $this->buildFilterBaseQuery($queryString, $isActive); + + try { + return $qb + ->select('COUNT(sa)') + ->getQuery()->getSingleScalarResult(); + } catch (NoResultException|NonUniqueResultException $e) { + throw new \LogicException('a count query should return one result', previous: $e); + } + } + private function buildQueryWithDesactivatedDateCriteria(): QueryBuilder { $qb = $this->repository->createQueryBuilder('sa'); diff --git a/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialIssueRepository.php b/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialIssueRepository.php index 40ec35855..638e1a869 100644 --- a/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialIssueRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialIssueRepository.php @@ -14,14 +14,17 @@ namespace Chill\PersonBundle\Repository\SocialWork; use Chill\PersonBundle\Entity\SocialWork\SocialIssue; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; +use Symfony\Component\HttpFoundation\RequestStack; final readonly class SocialIssueRepository implements ObjectRepository { private EntityRepository $repository; - public function __construct(EntityManagerInterface $entityManager) + public function __construct(private EntityManagerInterface $entityManager, private RequestStack $requestStack) { $this->repository = $entityManager->getRepository(SocialIssue::class); } @@ -79,6 +82,100 @@ final readonly class SocialIssueRepository implements ObjectRepository return SocialIssue::class; } + public function getResult( + QueryBuilder $qb, + ?int $start = 0, + ?int $limit = 50, + ?array $orderBy = [], + ): array { + $qb->select('si'); + + $qb + ->setFirstResult($start) + ->setMaxResults($limit); + + foreach ($orderBy as $field => $direction) { + $qb->addOrderBy('si.'.$field, $direction); + } + + return $qb->getQuery()->getResult(); + } + + private function getLang(): string + { + return $this->requestStack->getCurrentRequest()?->getLocale() ?? 'fr'; + } + + private function queryByTitle(string $pattern): QueryBuilder + { + $qb = $this->entityManager->createQueryBuilder()->from(SocialIssue::class, 'si'); + + $qb + ->where($qb->expr()->like('LOWER(UNACCENT(JSON_EXTRACT(si.title, :lang)))', "CONCAT('%', LOWER(UNACCENT(:pattern)), '%')")) + ->setParameter('pattern', $pattern) + ->setParameter('lang', $this->getLang()); + + return $qb; + } + + public function buildFilterBaseQuery(?string $queryString, array $isActive): QueryBuilder + { + if (null !== $queryString) { + $qb = $this->queryByTitle($queryString); + } else { + $qb = $this->entityManager->createQueryBuilder()->from(SocialIssue::class, 'si'); + } + + $now = new \DateTime('now'); + if (in_array('Active', $isActive, true) && !in_array('Inactive', $isActive, true)) { + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->isNull('si.desactivationDate'), + $qb->expr()->gt('si.desactivationDate', ':now') + ) + )->setParameter('now', $now); + } elseif (in_array('Inactive', $isActive, true) && !in_array('Active', $isActive, true)) { + $qb->andWhere( + $qb->expr()->andX( + $qb->expr()->isNotNull('si.desactivationDate'), + $qb->expr()->lte('si.desactivationDate', ':now') + ) + )->setParameter('now', $now); + } + + return $qb; + } + + /** + * @return array + */ + public function findFilteredSocialIssues( + ?string $queryString = null, + array $isActive = ['active'], + ?int $start = 0, + ?int $limit = 50, + ?array $orderBy = ['ordering' => 'ASC'], + ): array { + $qb = $this->buildFilterBaseQuery($queryString, $isActive); + + return $this->getResult($qb, $start, $limit, $orderBy); + } + + public function countFilteredSocialIssues( + ?string $queryString = null, + array $isActive = ['active'], + ): int { + $qb = $this->buildFilterBaseQuery($queryString, $isActive); + + try { + return $qb + ->select('COUNT(si)') + ->getQuery()->getSingleScalarResult(); + } catch (NoResultException|NonUniqueResultException $e) { + throw new \LogicException('a count query should return one result', previous: $e); + } + } + private function buildQueryWithDesactivatedDateCriteria(): QueryBuilder { $qb = $this->repository->createQueryBuilder('si'); diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/CommentInput.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/CommentInput.vue index 62b46d2da..9dd550a4b 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/CommentInput.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/CommentInput.vue @@ -8,8 +8,8 @@ :editor="ClassicEditor" :config="classicEditorConfig" :placeholder="trans(EVALUATION_COMMENT_PLACEHOLDER)" - :value="comment" - @input="$emit('update:comment', $event)" + :model-value="comment" + @update:model-value="$emit('update:comment', $event)" tag-name="textarea" >

diff --git a/src/Bundle/ChillPersonBundle/Resources/views/SocialWork/Evaluation/index.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/SocialWork/Evaluation/index.html.twig index 556065e42..af7989b8f 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/SocialWork/Evaluation/index.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/SocialWork/Evaluation/index.html.twig @@ -2,6 +2,9 @@ {% block admin_content %} {% embed '@ChillMain/CRUD/_index.html.twig' %} + + {% block filter_order %}{{ filter_order|chill_render_filter_order_helper }}{% endblock %} + {% block table_entities_thead_tr %} {{ 'Id'|trans }} {{ 'Title'|trans }} diff --git a/src/Bundle/ChillPersonBundle/Resources/views/SocialWork/Goal/index.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/SocialWork/Goal/index.html.twig index 14e9bab73..f6f9a84c5 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/SocialWork/Goal/index.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/SocialWork/Goal/index.html.twig @@ -2,6 +2,9 @@ {% block admin_content %} {% embed '@ChillMain/CRUD/_index.html.twig' %} + + {% block filter_order %}{{ filter_order|chill_render_filter_order_helper }}{% endblock %} + {% block table_entities_thead_tr %} {{ 'Id'|trans }} {{ 'Title'|trans }} @@ -22,7 +25,7 @@ {% if entity.desactivationDate is not null %} - {{ entity.desactivationDate|date('Y-m-d') }} + {{ entity.desactivationDate|format_date('medium') }} {% endif %} diff --git a/src/Bundle/ChillPersonBundle/Resources/views/SocialWork/Result/index.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/SocialWork/Result/index.html.twig index da9e71bc5..cd4f24b5a 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/SocialWork/Result/index.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/SocialWork/Result/index.html.twig @@ -2,6 +2,9 @@ {% block admin_content %} {% embed '@ChillMain/CRUD/_index.html.twig' %} + + {% block filter_order %}{{ filter_order|chill_render_filter_order_helper }}{% endblock %} + {% block table_entities_thead_tr %} {{ 'Id'|trans }} {{ 'Title'|trans }} @@ -16,7 +19,7 @@ {{ entity.title|localize_translatable_string }} {% if entity.desactivationDate is not null %} - {{ entity.desactivationDate|date('Y-m-d') }} + {{ entity.desactivationDate|format_date('medium') }} {% endif %} diff --git a/src/Bundle/ChillPersonBundle/Resources/views/SocialWork/SocialAction/index.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/SocialWork/SocialAction/index.html.twig index 152b8981c..003965667 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/SocialWork/SocialAction/index.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/SocialWork/SocialAction/index.html.twig @@ -2,6 +2,9 @@ {% block admin_content %} {% embed '@ChillMain/CRUD/_index.html.twig' %} + + {% block filter_order %}{{ filter_order|chill_render_filter_order_helper }}{% endblock %} + {% block table_entities_thead_tr %} {{ 'Id' }} {{ 'Title'|trans }} diff --git a/src/Bundle/ChillPersonBundle/Resources/views/SocialWork/SocialIssue/index.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/SocialWork/SocialIssue/index.html.twig index 7ca3c6636..9eb786b22 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/SocialWork/SocialIssue/index.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/SocialWork/SocialIssue/index.html.twig @@ -2,6 +2,9 @@ {% block admin_content %} {% embed '@ChillMain/CRUD/_index.html.twig' %} + + {% block filter_order %}{{ filter_order|chill_render_filter_order_helper }}{% endblock %} + {% block table_entities_thead_tr %} {{ 'Id'|trans }} {{ 'Title'|trans }} diff --git a/src/Bundle/ChillPersonBundle/Search/PersonSearch.php b/src/Bundle/ChillPersonBundle/Search/PersonSearch.php index c5c15e125..e891e6e6b 100644 --- a/src/Bundle/ChillPersonBundle/Search/PersonSearch.php +++ b/src/Bundle/ChillPersonBundle/Search/PersonSearch.php @@ -167,7 +167,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf /** * (non-PHPdoc). * - * @see \Chill\MainBundle\Search\SearchInterface::getOrder() + * @see SearchInterface::getOrder() */ public function getOrder(): int { @@ -177,7 +177,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf /** * (non-PHPdoc). * - * @see \Chill\MainBundle\Search\SearchInterface::isActiveByDefault() + * @see SearchInterface::isActiveByDefault() */ public function isActiveByDefault() { diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php index 4137cb077..0b6ac365f 100644 --- a/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php @@ -14,6 +14,7 @@ namespace Chill\PersonBundle\Security\Authorization\StoredObjectVoter; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; +use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository; use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository; @@ -26,8 +27,9 @@ class AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter extends Abstract private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository, Security $security, WorkflowRelatedEntityPermissionHelper $workflowDocumentService, + EntityWorkflowAttachmentRepository $attachmentRepository, ) { - parent::__construct($security, $workflowDocumentService); + parent::__construct($security, $attachmentRepository, $workflowDocumentService); } protected function getRepository(): AssociatedEntityToStoredObjectInterface diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 2159f32db..9105e9c62 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -103,6 +103,11 @@ Employment status: Situation professionelle Administrative status: Situation administrative person: + # trans key according to new conventions + export: + aggregator: + by_center: + no_center: Sans territoire Identifiers: Identifiants person_edit: