Compare commits

...

28 Commits

Author SHA1 Message Date
3402e4863f Release v4.12.1 2026-02-01 18:52:21 +01:00
1f0974ea68 Merge branch 'cs/update-cs-fixer-3.93' into 'master'
Update PHP-CS-Fixer to version 3.93.0 in composer dependencies

See merge request Chill-Projet/chill-bundles!955
2026-01-29 12:27:56 +00:00
9997fb287a Update PHP-CS-Fixer to version 3.93.0 in composer dependencies 2026-01-29 12:27:56 +00:00
f9a9de1148 Merge branch '496-allow-remove-double-refid-ban-address-importer' into 'master'
Adding the option to deal with duplicate addresses in the BAN importer

Closes #496

See merge request Chill-Projet/chill-bundles!954
2026-01-27 10:26:57 +00:00
juminet
c34f720f94 Adding the option to deal with duplicate addresses in the BAN importer 2026-01-27 10:26:57 +00:00
e1b1f592fa Merge branch 'zimbra/use-delegated-admin' into 'master'
[Zimbra] Use admin delegated account for authenticating users against Zimbra

See merge request Chill-Projet/chill-bundles!952
2026-01-22 14:39:46 +00:00
8546f4dadc [Zimbra] Use admin delegated account for authenticating users against Zimbra 2026-01-22 14:39:46 +00:00
4028c020ee Release v4.12.0 2026-01-15 18:02:12 +01:00
0d4eef6a0c Merge branch '493-fix-stored-object-workflow-permission' into 'master'
Fix issues with permission for stored objects associated with workflows

Closes #493

See merge request Chill-Projet/chill-bundles!951
2026-01-15 16:54:37 +00:00
b6152d5356 Fix issues with permission for stored objects associated with workflows 2026-01-15 16:54:37 +00:00
8b708f8c73 fix CommentInput: replace deprecated value binding with model-value 2026-01-15 14:53:40 +01:00
8d5b200107 Restrict ux-translator version to 2.31.0 2026-01-15 14:44:05 +01:00
a9e9207d5a Update php-cs-fixer version 2026-01-15 13:41:00 +01:00
3915574ed4 phpstan error fix 2026-01-15 13:40:46 +01:00
f3217d22ef 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 2026-01-15 13:25:54 +01:00
06c5affbe7 Increase delay for removing stale workflows from 90 to 180 days
- Updated `KEEP_INTERVAL` in `CancelStaleWorkflowCronJob` to `P180D`.
2026-01-15 10:08:40 +01:00
bf461a1211 Merge branch '473-display-bundles-version' into 'master'
Resolve "Afficher le numéro de version de Chill dans l'UX"

Closes #473

See merge request Chill-Projet/chill-bundles!947
2026-01-13 15:35:26 +00:00
3f0ad51114 Resolve "Afficher le numéro de version de Chill dans l'UX" 2026-01-13 15:35:26 +00:00
a4de8eaab3 Merge branch '489-fix-desactivation-date-goarls-results' into 'master'
Fix issue with goal/result deactivation date handling and improve formatting

Closes #489

See merge request Chill-Projet/chill-bundles!949
2026-01-13 15:32:08 +00:00
2feb137ac2 Fix issue with goal/result deactivation date handling and improve formatting 2026-01-13 15:32:07 +00:00
5ea74d118b Merge branch '490-fix-double-notification' into 'master'
Prevent notifications from being sent when the user signs a document he asked to himself

Closes #490

See merge request Chill-Projet/chill-bundles!950
2026-01-13 15:31:50 +00:00
8eb7a55ef5 Prevent notifications from being sent when the user signs a document he asked to himself 2026-01-13 15:31:49 +00:00
281887355f Fix calculation of budget balance 2026-01-12 10:34:30 +01:00
47b285b584 Fix export group by center for persons without a center in CenterAggregator.php 2025-12-30 13:01:56 +01:00
7c9b4d02f6 Fix ordering of social actions
Actions with a closing date in the future should be considered as 'still open'.
2025-12-18 11:08:18 +01:00
3ff9bba4de Fix the condition to display concerned persons in calendar list items. 2025-12-18 10:24:24 +01:00
c0f9e953fb Update to v4.11.0 2025-12-17 16:56:35 +01:00
a49ea2b6b9 Fix translation syntax
Cannot start with %, wrap translation value in double quotes
2025-12-17 16:54:33 +01:00
60 changed files with 848 additions and 494 deletions

View File

@@ -1,6 +0,0 @@
kind: Feature
body: 'Add filtering to admin lists: social actions, social issues, goals, results, and evaluations'
time: 2025-12-10T03:20:45.135973502+01:00
custom:
Issue: "478"
SchemaChange: No schema change

View File

@@ -1,7 +0,0 @@
kind: Fixed
body: |
Fix migration query after previous fix
time: 2025-12-11T21:49:08.899926492+01:00
custom:
Issue: "466"
SchemaChange: No schema change

9
.changes/v4.11.0.md Normal file
View File

@@ -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 "".

16
.changes/v4.12.0.md Normal file
View File

@@ -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.

4
.changes/v4.12.1.md Normal file
View File

@@ -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

View File

@@ -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). 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 ## v4.10.1 - 2025-12-11
### Fixed ### Fixed
* Fix missing translation variable in NewLocation component * Fix missing translation variable in NewLocation component

View File

@@ -21,6 +21,7 @@
"ext-openssl": "*", "ext-openssl": "*",
"ext-redis": "*", "ext-redis": "*",
"ext-zlib": "*", "ext-zlib": "*",
"composer-runtime-api": "*",
"champs-libres/wopi-bundle": "dev-symfony-v5@dev", "champs-libres/wopi-bundle": "dev-symfony-v5@dev",
"champs-libres/wopi-lib": "dev-master@dev", "champs-libres/wopi-lib": "dev-master@dev",
"doctrine/data-fixtures": "^1.8", "doctrine/data-fixtures": "^1.8",
@@ -82,7 +83,7 @@
"symfony/templating": "^5.4", "symfony/templating": "^5.4",
"symfony/translation": "^5.4", "symfony/translation": "^5.4",
"symfony/twig-bundle": "^5.4", "symfony/twig-bundle": "^5.4",
"symfony/ux-translator": "^2.22", "symfony/ux-translator": "2.31.0",
"symfony/validator": "^5.4", "symfony/validator": "^5.4",
"symfony/webpack-encore-bundle": "^1.11", "symfony/webpack-encore-bundle": "^1.11",
"symfony/workflow": "^5.4", "symfony/workflow": "^5.4",
@@ -97,7 +98,7 @@
"require-dev": { "require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3", "doctrine/doctrine-fixtures-bundle": "^3.3",
"fakerphp/faker": "^1.13", "fakerphp/faker": "^1.13",
"friendsofphp/php-cs-fixer": "3.65.0", "friendsofphp/php-cs-fixer": "3.93.0",
"jangregor/phpstan-prophecy": "^1.0", "jangregor/phpstan-prophecy": "^1.0",
"nelmio/alice": "^3.8", "nelmio/alice": "^3.8",
"nikic/php-parser": "^4.15", "nikic/php-parser": "^4.15",

View File

@@ -0,0 +1,3 @@
kind: Added
body: Use admin delegated account for handling authentication
time: 2026-01-22T15:32:23.932994899+01:00

View File

@@ -80,12 +80,19 @@ final readonly class CreateZimbraComponent
$location = $calendar->getCalendar()->getLocation(); $location = $calendar->getCalendar()->getLocation();
$hasLocation = $calendar->getCalendar()->hasLocation(); $hasLocation = $calendar->getCalendar()->hasLocation();
$isPrivate = $calendar->getCalendar()->getAccompanyingPeriod()?->isConfidential() ?? false; $isPrivate = $calendar->getCalendar()->getAccompanyingPeriod()?->isConfidential() ?? false;
} else { } elseif ($calendar instanceof Calendar) {
$startDate = $calendar->getStartDate(); $startDate = $calendar->getStartDate();
$endDate = $calendar->getEndDate(); $endDate = $calendar->getEndDate();
$location = $calendar->getLocation(); $location = $calendar->getLocation();
$hasLocation = $calendar->hasLocation(); $hasLocation = $calendar->hasLocation();
$isPrivate = $calendar->getAccompanyingPeriod()?->isConfidential() ?? false; $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(); $comp = new InviteComponent();

View File

@@ -11,48 +11,84 @@ declare(strict_types=1);
namespace Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector; namespace Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpClient\Psr18Client; use Symfony\Component\HttpClient\Psr18Client;
use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\HttpClientInterface;
use Zimbra\Admin\AdminApi;
use Zimbra\Common\Enum\AccountBy; use Zimbra\Common\Enum\AccountBy;
use Zimbra\Common\Soap\ClientFactory; use Zimbra\Common\Soap\ClientFactory;
use Zimbra\Common\Struct\AccountSelector;
use Zimbra\Common\Struct\Header\AccountInfo; use Zimbra\Common\Struct\Header\AccountInfo;
use Zimbra\Mail\MailApi; 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<string, array{token: string, expirationTime: \DateTimeImmutable}>
*/
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'); $dsn = $this->parameterBag->get('chill_calendar.remote_calendar_dsn');
$url = parse_url($dsn); $url = parse_url($dsn);
$this->username = urldecode($url['user']); $this->username = urldecode($url['user']);
$this->password = urldecode($url['pass']); $this->password = urldecode($url['pass']);
if ('zimbra+http' === $url['scheme']) { if ('zimbra+http' === $url['scheme']) {
$scheme = 'http://'; $scheme = 'http';
$port = $url['port'] ?? 80; $port = $url['port'] ?? 80;
} elseif ('zimbra+https' === $url['scheme']) { } elseif ('zimbra+https' === $url['scheme']) {
$scheme = 'https://'; $scheme = 'https';
$port = $url['port'] ?? 443; $port = $url['port'] ?? 443;
} else { } else {
throw new \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException('Unsupported remote calendar scheme: '.$url['scheme']); 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 private function buildApi(): MailApi
{ {
$baseClient = $this->client->withOptions([ $baseClient = $this->client->withOptions([
'base_uri' => $location = $this->url.'/service/soap', 'base_uri' => $location = $this->url.'/service/soap',
'verify_host' => false, 'verify_host' => $this->verifyHost,
'verify_peer' => false, 'verify_peer' => $this->verifyPeer,
]); ]);
$psr18Client = new Psr18Client($baseClient); $psr18Client = new Psr18Client($baseClient);
$api = new MailApi(); $api = new MailApi();
@@ -62,12 +98,36 @@ final readonly class SoapClientBuilder
return $api; 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 public function getApiForAccount(string $accountName): MailApi
{ {
$api = $this->buildApi(); ['token' => $token, 'expirationTime' => $expirationTime] = $this->tokenCache[$accountName]
$response = $api->authByAccountName($this->username, $this->password); ?? ['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 = $this->buildApi();
$apiBy->setAuthToken($token); $apiBy->setAuthToken($token);

View File

@@ -69,7 +69,7 @@ class ChillActivityExtension extends Extension implements PrependExtensionInterf
} }
/** (non-PHPdoc). /** (non-PHPdoc).
* @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend() * @see PrependExtensionInterface::prepend()
*/ */
public function prependRoutes(ContainerBuilder $container) public function prependRoutes(ContainerBuilder $container)
{ {

View File

@@ -16,7 +16,8 @@ use Chill\ActivityBundle\Repository\ActivityRepository;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; 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; use Symfony\Component\Security\Core\Security;
class ActivityStoredObjectVoter extends AbstractStoredObjectVoter class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
@@ -24,9 +25,10 @@ class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
public function __construct( public function __construct(
private readonly ActivityRepository $repository, private readonly ActivityRepository $repository,
Security $security, Security $security,
WorkflowRelatedEntityPermissionHelper $workflowDocumentService, WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService,
EntityWorkflowAttachmentRepository $attachmentRepository,
) { ) {
parent::__construct($security, $workflowDocumentService); parent::__construct($security, $attachmentRepository, $workflowDocumentService);
} }
protected function getRepository(): AssociatedEntityToStoredObjectInterface protected function getRepository(): AssociatedEntityToStoredObjectInterface

View File

@@ -56,7 +56,7 @@ class ChillBudgetExtension extends Extension implements PrependExtensionInterfac
} }
/** (non-PHPdoc). /** (non-PHPdoc).
* @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend() * @see PrependExtensionInterface::prepend()
*/ */
public function prependRoutes(ContainerBuilder $container) public function prependRoutes(ContainerBuilder $container)
{ {

View File

@@ -72,14 +72,20 @@
{% macro table_results(actualCharges, actualResources, results) %} {% macro table_results(actualCharges, actualResources, results) %}
{% set now = date() %}
{% set totalCharges = 0 %} {% set totalCharges = 0 %}
{% for c in actualCharges %} {% 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 %} {% endfor %}
{% set totalResources = 0 %} {% set totalResources = 0 %}
{% for r in actualResources %} {% 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 %} {% endfor %}
{% set result = (totalResources - totalCharges) %} {% set result = (totalResources - totalCharges) %}

View File

@@ -75,7 +75,7 @@
</div> </div>
{% if calendar.comment.comment is not empty {% if calendar.comment.comment is not empty
or calendar.users|length > 0 or calendar.persons|length > 0
or calendar.thirdParties|length > 0 or calendar.thirdParties|length > 0
or calendar.users|length > 0 %} or calendar.users|length > 0 %}
<div class="item-row details separator"> <div class="item-row details separator">

View File

@@ -52,7 +52,7 @@ class ChillCustomFieldsExtension extends Extension implements PrependExtensionIn
} }
/** (non-PHPdoc). /** (non-PHPdoc).
* @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend() * @see PrependExtensionInterface::prepend()
*/ */
public function prepend(ContainerBuilder $container) public function prepend(ContainerBuilder $container)
{ {

View File

@@ -25,7 +25,7 @@ class ChoiceWithOtherType extends AbstractType
private string $otherValueLabel = 'Other value'; private string $otherValueLabel = 'Other value';
/** (non-PHPdoc). /** (non-PHPdoc).
* @see \Symfony\Component\Form\AbstractType::buildForm() * @see AbstractType::buildForm()
*/ */
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options)
{ {
@@ -42,7 +42,7 @@ class ChoiceWithOtherType extends AbstractType
} }
/** (non-PHPdoc). /** (non-PHPdoc).
* @see \Symfony\Component\Form\AbstractType::configureOptions() * @see AbstractType::configureOptions()
*/ */
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver)
{ {

View File

@@ -22,7 +22,7 @@ use Symfony\Component\Form\FormEvents;
class ChoicesListType extends AbstractType class ChoicesListType extends AbstractType
{ {
/** (non-PHPdoc). /** (non-PHPdoc).
* @see \Symfony\Component\Form\AbstractType::buildForm() * @see AbstractType::buildForm()
*/ */
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options)
{ {

View File

@@ -82,7 +82,7 @@ class CustomFieldProvider implements ContainerAwareInterface
/** /**
* (non-PHPdoc). * (non-PHPdoc).
* *
* @see \Symfony\Component\DependencyInjection\ContainerAwareInterface::setContainer() * @see ContainerAwareInterface::setContainer()
*/ */
public function setContainer(?ContainerInterface $container = null) public function setContainer(?ContainerInterface $container = null)
{ {

View File

@@ -15,7 +15,10 @@ use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface; 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\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
@@ -34,7 +37,8 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
public function __construct( public function __construct(
private readonly Security $security, 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 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 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 // Retrieve the related entity
$entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject); $entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
@@ -65,7 +59,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
$regularPermission = $this->security->isGranted($voterAttribute, $entity); $regularPermission = $this->security->isGranted($voterAttribute, $entity);
if (!$this->canBeAssociatedWithWorkflow()) { if (!$this->canBeAssociatedWithWorkflow()) {
return $regularPermission; return $this->voteOnStoredObjectAsAttachementOfAWorkflow($attribute, $regularPermission, $subject);
} }
$workflowPermission = match ($attribute) { $workflowPermission = match ($attribute) {
@@ -74,9 +68,41 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
}; };
return match ($workflowPermission) { return match ($workflowPermission) {
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT => true, WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT => true,
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED => false, WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED => false,
WorkflowRelatedEntityPermissionHelper::ABSTAIN => WorkflowRelatedEntityPermissionHelper::FORCE_GRANT === $workflowPermissionAsAttachment || $regularPermission, 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;
}
} }

View File

@@ -16,6 +16,7 @@ use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
@@ -25,8 +26,9 @@ final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredOb
private readonly AccompanyingCourseDocumentRepository $repository, private readonly AccompanyingCourseDocumentRepository $repository,
Security $security, Security $security,
WorkflowRelatedEntityPermissionHelper $workflowDocumentService, WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
EntityWorkflowAttachmentRepository $attachmentRepository,
) { ) {
parent::__construct($security, $workflowDocumentService); parent::__construct($security, $attachmentRepository, $workflowDocumentService);
} }
protected function getRepository(): AssociatedEntityToStoredObjectInterface protected function getRepository(): AssociatedEntityToStoredObjectInterface

View File

@@ -16,6 +16,7 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Repository\PersonDocumentRepository; use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
@@ -25,8 +26,9 @@ class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter
private readonly PersonDocumentRepository $repository, private readonly PersonDocumentRepository $repository,
Security $security, Security $security,
WorkflowRelatedEntityPermissionHelper $workflowDocumentService, WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
EntityWorkflowAttachmentRepository $attachmentRepository,
) { ) {
parent::__construct($security, $workflowDocumentService); parent::__construct($security, $attachmentRepository, $workflowDocumentService);
} }
protected function getRepository(): AssociatedEntityToStoredObjectInterface protected function getRepository(): AssociatedEntityToStoredObjectInterface

View File

@@ -16,8 +16,11 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
use Chill\MainBundle\Entity\User; 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 PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
@@ -31,21 +34,31 @@ class AbstractStoredObjectVoterTest extends TestCase
{ {
use ProphecyTrait; use ProphecyTrait;
/**
* @param array<int, EntityWorkflowAttachment> $attachments
*
* @return void
*/
private function buildStoredObjectVoter( private function buildStoredObjectVoter(
bool $canBeAssociatedWithWorkflow, bool $canBeAssociatedWithWorkflow,
AssociatedEntityToStoredObjectInterface $repository, AssociatedEntityToStoredObjectInterface $repository,
Security $security, Security $security,
?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null, ?WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService = null,
array $attachments = [],
): AbstractStoredObjectVoter { ): AbstractStoredObjectVoter {
$attachmentsRepository = $this->prophesize(EntityWorkflowAttachmentRepository::class);
$attachmentsRepository->findByStoredObject(Argument::type(StoredObject::class))->willReturn($attachments);
// Anonymous class extending the abstract class // 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( public function __construct(
private readonly bool $canBeAssociatedWithWorkflow, private readonly bool $canBeAssociatedWithWorkflow,
private readonly AssociatedEntityToStoredObjectInterface $repository, private readonly AssociatedEntityToStoredObjectInterface $repository,
Security $security, Security $security,
?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null, EntityWorkflowAttachmentRepository $attachmentRepository,
WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService,
) { ) {
parent::__construct($security, $workflowDocumentService); parent::__construct($security, $attachmentRepository, $workflowDocumentService);
} }
protected function attributeToRole($attribute): string protected function attributeToRole($attribute): string
@@ -72,28 +85,29 @@ class AbstractStoredObjectVoterTest extends TestCase
public function testSupportsOnAttribute(): void 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())); 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())); 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())); self::assertFalse($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject()));
} }
/** /**
* @dataProvider dataProviderVoteOnAttributeWithStoredObjectPermission * @dataProvider dataProviderVoteOnAttributeWithWorkflow
*/ */
public function testVoteOnAttributeWithStoredObjectPermission( public function testVoteOnAttributeWithWorkflow(
StoredObjectRoleEnum $attribute, StoredObjectRoleEnum $attribute,
bool $expected, bool $expected,
bool $isGrantedRegularPermission, bool $isGrantedRegularPermission,
string $isGrantedWorkflowPermission, string $isGrantedWorkflowPermission,
string $isGrantedStoredObjectAttachment,
): void { ): void {
$storedObject = new StoredObject(); $storedObject = new StoredObject();
$repository = new DummyRepository($related = new \stdClass()); $repository = new DummyRepository($related = new \stdClass());
@@ -102,31 +116,28 @@ class AbstractStoredObjectVoterTest extends TestCase
$security = $this->prophesize(Security::class); $security = $this->prophesize(Security::class);
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission); $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class); $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class);
$security = $this->prophesize(Security::class); $security = $this->prophesize(Security::class);
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission); $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
$attachementRepository = $this->prophesize(EntityWorkflowAttachmentRepository::class);
$attachementRepository->findByStoredObject($storedObject)->willReturn([]);
if (StoredObjectRoleEnum::SEE === $attribute) { if (StoredObjectRoleEnum::SEE === $attribute) {
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)
->shouldBeCalled()
->willReturn($isGrantedStoredObjectAttachment);
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related) $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
->willReturn($isGrantedWorkflowPermission); ->willReturn($isGrantedWorkflowPermission);
} elseif (StoredObjectRoleEnum::EDIT === $attribute) { } elseif (StoredObjectRoleEnum::EDIT === $attribute) {
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)
->shouldBeCalled()
->willReturn($isGrantedStoredObjectAttachment);
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related) $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related)
->willReturn($isGrantedWorkflowPermission); ->willReturn($isGrantedWorkflowPermission);
} else { } else {
throw new \LogicException('Invalid attribute for StoredObjectVoter'); throw new \LogicException('Invalid attribute for StoredObjectVoter');
} }
$storedObjectVoter = new class ($repository, $workflowRelatedEntityPermissionHelper->reveal(), $security->reveal()) extends AbstractStoredObjectVoter { $storedObjectVoter = new class ($repository, $workflowRelatedEntityPermissionHelper->reveal(), $security->reveal(), $attachementRepository->reveal()) extends AbstractStoredObjectVoter {
public function __construct(private $repository, $helper, $security) public function __construct(private $repository, $helper, $security, EntityWorkflowAttachmentRepository $attachmentRepository)
{ {
parent::__construct($security, $helper); parent::__construct($security, $attachmentRepository, $helper);
} }
protected function getRepository(): AssociatedEntityToStoredObjectInterface protected function getRepository(): AssociatedEntityToStoredObjectInterface
@@ -155,96 +166,64 @@ class AbstractStoredObjectVoterTest extends TestCase
self::assertEquals($expected, $actual); self::assertEquals($expected, $actual);
} }
public static function dataProviderVoteOnAttributeWithStoredObjectPermission(): iterable public static function dataProviderVoteOnAttributeWithWorkflow(): iterable
{ {
foreach (['read' => StoredObjectRoleEnum::SEE, 'write' => StoredObjectRoleEnum::EDIT] as $action => $attribute) { foreach (['read' => StoredObjectRoleEnum::SEE, 'write' => StoredObjectRoleEnum::EDIT] as $action => $attribute) {
yield 'Not related to any workflow nor attachment ('.$action.')' => [ yield 'Not related to any workflow nor attachment ('.$action.')' => [
$attribute, $attribute,
true, true,
true, true,
WorkflowRelatedEntityPermissionHelper::ABSTAIN, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
]; ];
yield 'Not related to any workflow nor attachment (refuse) ('.$action.')' => [ yield 'Not related to any workflow nor attachment (refuse) ('.$action.')' => [
$attribute, $attribute,
false, false,
false, false,
WorkflowRelatedEntityPermissionHelper::ABSTAIN, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
]; ];
yield 'Is granted by a workflow takes precedence (workflow) ('.$action.')' => [ yield 'Is granted by a workflow takes precedence (workflow) ('.$action.')' => [
$attribute, $attribute,
false, false,
true, true,
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
]; ];
yield 'Is granted by a workflow takes precedence (stored object) ('.$action.')' => [ yield 'Is granted by a workflow takes precedence (stored object) ('.$action.')' => [
$attribute, $attribute,
false, false,
true,
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
];
yield 'Is granted by a workflow takes precedence (workflow) although grant ('.$action.')' => [
$attribute,
false, false,
true, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
]; ];
yield 'Is granted by a workflow takes precedence (stored object) although grant ('.$action.')' => [ yield 'Is granted by a workflow takes precedence (stored object) although grant ('.$action.')' => [
$attribute, $attribute,
false,
true, true,
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, true,
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
]; ];
yield 'Is granted by a workflow takes precedence (initially refused) (workflow) although grant ('.$action.')' => [ yield 'Is granted by a workflow takes precedence (initially refused) (workflow) although grant ('.$action.')' => [
$attribute, $attribute,
false, false,
false, false,
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
]; ];
yield 'Is granted by a workflow takes precedence (initially refused) (stored object) although grant ('.$action.')' => [ 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, $attribute,
true, true,
false, false,
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
]; ];
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, StoredObjectRoleEnum $attribute,
bool $expected, bool $expected,
bool $canBeAssociatedWithWorkflow, bool $canBeAssociatedWithWorkflow,
@@ -260,10 +239,7 @@ class AbstractStoredObjectVoterTest extends TestCase
$security = $this->prophesize(Security::class); $security = $this->prophesize(Security::class);
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission); $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class); $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class);
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN);
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN);
if (null !== $isGrantedWorkflowPermissionRead) { if (null !== $isGrantedWorkflowPermissionRead) {
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related) $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
@@ -283,27 +259,155 @@ class AbstractStoredObjectVoterTest extends TestCase
self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message); self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message);
} }
public static function dataProviderVoteOnAttributeWithoutStoredObjectPermission(): iterable public static function dataProviderVoteOnAttribute(): iterable
{ {
// not associated on a workflow // 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, 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']; 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 // 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, 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, WorkflowRelatedEntityPermissionHelper::ABSTAIN, null, 'associated on a workflow, read by regular, abstain 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, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be denied']; 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, 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, 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, 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, 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, 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, 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 // 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, 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, WorkflowRelatedEntityPermissionHelper::ABSTAIN, 'associated on a workflow, write by regular, abstain 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, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be denied']; 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, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be granted']; 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, 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, 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, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be denied']; 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'],
];
}
}
} }
} }

View File

@@ -52,7 +52,7 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
} }
/** (non-PHPdoc). /** (non-PHPdoc).
* @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend() * @see PrependExtensionInterface::prepend()
*/ */
public function prepend(ContainerBuilder $container): void public function prepend(ContainerBuilder $container): void
{ {

View File

@@ -14,6 +14,7 @@ namespace Chill\EventBundle\Security\Authorization;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Chill\EventBundle\Entity\Event; use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Repository\EventRepository; use Chill\EventBundle\Repository\EventRepository;
@@ -26,8 +27,9 @@ class EventStoredObjectVoter extends AbstractStoredObjectVoter
private readonly EventRepository $repository, private readonly EventRepository $repository,
Security $security, Security $security,
WorkflowRelatedEntityPermissionHelper $workflowDocumentService, WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
EntityWorkflowAttachmentRepository $attachmentRepository,
) { ) {
parent::__construct($security, $workflowDocumentService); parent::__construct($security, $attachmentRepository, $workflowDocumentService);
} }
protected function getRepository(): AssociatedEntityToStoredObjectInterface protected function getRepository(): AssociatedEntityToStoredObjectInterface

View File

@@ -189,14 +189,14 @@ crud:
title_edit: Rapport "belemmering" bewerken title_edit: Rapport "belemmering" bewerken
title_delete: Belemmering verwijderen title_delete: Belemmering verwijderen
button_delete: Verwijderen button_delete: Verwijderen
confirm_message_delete: %as_string% verwijderen? confirm_message_delete: "%as_string% verwijderen?"
cscv: cscv:
title_new: Nieuw CV voor %person% title_new: Nieuw CV voor %person%
title_view: CV voor %person% title_view: CV voor %person%
title_edit: CV bewerken title_edit: CV bewerken
title_delete: CV verwijderen title_delete: CV verwijderen
button_delete: Verwijderen button_delete: Verwijderen
confirm_message_delete: %as_string% verwijderen? confirm_message_delete: "%as_string% verwijderen?"
no_date: Geen datum aangegeven no_date: Geen datum aangegeven
no_end_date: einddatum onbekend no_end_date: einddatum onbekend
no_start_date: startdatum onbekend no_start_date: startdatum onbekend
@@ -206,7 +206,7 @@ crud:
title_edit: Immersie bewerken title_edit: Immersie bewerken
title_delete: Immersie verwijderen title_delete: Immersie verwijderen
button_delete: Verwijderen button_delete: Verwijderen
confirm_message_delete: %as_string% verwijderen? confirm_message_delete: "%as_string% verwijderen?"
projet_prof: projet_prof:
title_new: Nieuw professioneel project voor %person% title_new: Nieuw professioneel project voor %person%
title_view: Professioneel project voor %person% title_view: Professioneel project voor %person%

View File

@@ -31,7 +31,8 @@ class LoadAddressesFRFromBANCommand extends Command
{ {
$this->setName('chill:main:address-ref-from-ban') $this->setName('chill:main:address-ref-from-ban')
->addArgument('departementNo', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'a list of departement numbers') ->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 protected function execute(InputInterface $input, OutputInterface $output): int
@@ -40,7 +41,7 @@ class LoadAddressesFRFromBANCommand extends Command
foreach ($input->getArgument('departementNo') as $departementNo) { foreach ($input->getArgument('departementNo') as $departementNo) {
$output->writeln('Import addresses for '.$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; return Command::SUCCESS;

View File

@@ -48,7 +48,7 @@ class LoadAndUpdateLanguagesCommand extends Command
/** /**
* (non-PHPdoc). * (non-PHPdoc).
* *
* @see \Symfony\Component\Console\Command\Command::configure() * @see Command::configure()
*/ */
protected function configure() protected function configure()
{ {
@@ -73,7 +73,7 @@ class LoadAndUpdateLanguagesCommand extends Command
/** /**
* (non-PHPdoc). * (non-PHPdoc).
* *
* @see \Symfony\Component\Console\Command\Command::execute() * @see Command::execute()
*/ */
protected function execute(InputInterface $input, OutputInterface $output): int protected function execute(InputInterface $input, OutputInterface $output): int
{ {

View File

@@ -51,7 +51,7 @@ class LoadCountriesCommand extends Command
/** /**
* (non-PHPdoc). * (non-PHPdoc).
* *
* @see \Symfony\Component\Console\Command\Command::configure() * @see Command::configure()
*/ */
protected function configure() protected function configure()
{ {
@@ -61,7 +61,7 @@ class LoadCountriesCommand extends Command
/** /**
* (non-PHPdoc). * (non-PHPdoc).
* *
* @see \Symfony\Component\Console\Command\Command::execute() * @see Command::execute()
*/ */
protected function execute(InputInterface $input, OutputInterface $output): int protected function execute(InputInterface $input, OutputInterface $output): int
{ {

View File

@@ -20,7 +20,7 @@ class SearchableServicesCompilerPass implements CompilerPassInterface
/** /**
* (non-PHPdoc). * (non-PHPdoc).
* *
* @see \Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface::process() * @see CompilerPassInterface::process()
*/ */
public function process(ContainerBuilder $container) public function process(ContainerBuilder $container)
{ {

View File

@@ -32,7 +32,7 @@ abstract class AbstractWidgetFactory implements WidgetFactoryInterface
* Will create the definition by returning the definition from the `services.yml` * Will create the definition by returning the definition from the `services.yml`
* file (or `services.xml` or `what-you-want.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) public function createDefinition(ContainerBuilder $containerBuilder, $place, $order, array $config)
{ {

View File

@@ -2,6 +2,10 @@
<p> <p>
{{ 'This program is free software: you can redistribute it and/or modify it under the terms of the <strong>GNU Affero General Public License</strong>'|trans|raw }} {{ 'This program is free software: you can redistribute it and/or modify it under the terms of the <strong>GNU Affero General Public License</strong>'|trans|raw }}
<br/> <br/>
{% if get_chill_version() %}
{{ 'footer.Running chill version %version%'|trans({ '%version%': get_chill_version() }) }}
{% endif %}
<br/>
<a name="bottom" class="btn text-white" href="https://gitea.champs-libres.be/Chill-project/manuals/releases" target="_blank"> <a name="bottom" class="btn text-white" href="https://gitea.champs-libres.be/Chill-project/manuals/releases" target="_blank">
{{ 'User manual'|trans }} {{ 'User manual'|trans }}
</a> </a>

View File

@@ -33,7 +33,7 @@ final readonly class ScopeResolverDispatcher
} }
/** /**
* @return Scope|iterable<Scope>|Scope|null * @return Scope|iterable<Scope>|null
*/ */
public function resolveScope(mixed $entity, ?array $options = []): iterable|Scope|null public function resolveScope(mixed $entity, ?array $options = []): iterable|Scope|null
{ {

View File

@@ -23,7 +23,7 @@ class AddressReferenceFromBAN
private readonly AddressToReferenceMatcher $addressToReferenceMatcher, 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)) { if (!is_numeric($departementNo)) {
throw new \UnexpectedValueException('Could not parse this department number'); 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(); $this->addressToReferenceMatcher->checkAddressesMatchingReferences();

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Service;
use Composer\InstalledVersions;
readonly class VersionProvider
{
public function __construct(private string $packageName) {}
public function getVersion(): string
{
try {
$version = InstalledVersions::getPrettyVersion($this->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;
}
}

View File

@@ -23,7 +23,7 @@ class CancelStaleWorkflowCronJob implements CronJobInterface
{ {
public const KEY = 'remove-stale-workflow'; 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'; private const LAST_CANCELED_WORKFLOW = 'last-canceled-workflow-id';

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Templating;
use Chill\MainBundle\Service\VersionProvider;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class VersionRenderExtension extends AbstractExtension
{
public function __construct(
private readonly VersionProvider $versionProvider,
) {}
public function getFunctions(): array
{
return [
new TwigFunction('get_chill_version', $this->getChillVersion(...)),
];
}
public function getChillVersion(): string
{
return $this->versionProvider->getFormattedVersion();
}
}

View File

@@ -42,14 +42,14 @@ class CancelStaleWorkflowHandlerTest extends TestCase
{ {
use ProphecyTrait; use ProphecyTrait;
public function testWorkflowWithOneStepOlderThan90DaysIsCanceled(): void public function testWorkflowWithOneStepOlderThan180DaysIsCanceled(): void
{ {
$clock = new MockClock('2024-01-01'); $clock = new MockClock('2024-01-01');
$daysAgos = new \DateTimeImmutable('2023-09-01'); $daysAgos = new \DateTimeImmutable('2023-06-01');
$workflow = new EntityWorkflow(); $workflow = new EntityWorkflow();
$workflow->setWorkflowName('dummy_workflow'); $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()); $workflow->setStep('step1', new WorkflowTransitionContextDTO($workflow), 'to_step1', $daysAgos, new User());
$em = $this->prophesize(EntityManagerInterface::class); $em = $this->prophesize(EntityManagerInterface::class);
@@ -94,7 +94,7 @@ class CancelStaleWorkflowHandlerTest extends TestCase
$workflow = new EntityWorkflow(); $workflow = new EntityWorkflow();
$workflow->setWorkflowName('dummy_workflow'); $workflow->setWorkflowName('dummy_workflow');
$workflow->setCreatedAt(new \DateTimeImmutable('2023-09-01')); $workflow->setCreatedAt(new \DateTimeImmutable('2023-06-01'));
$em = $this->prophesize(EntityManagerInterface::class); $em = $this->prophesize(EntityManagerInterface::class);
$em->flush()->shouldBeCalled(); $em->flush()->shouldBeCalled();

View File

@@ -15,7 +15,9 @@ use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup; use Chill\MainBundle\Entity\UserGroup;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep; use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface; use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
use Chill\MainBundle\Workflow\EntityWorkflowManager; use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\EventSubscriber\NotificationOnTransition; use Chill\MainBundle\Workflow\EventSubscriber\NotificationOnTransition;
@@ -87,7 +89,7 @@ final class NotificationOnTransitionTest extends TestCase
->willReturn([]); ->willReturn([]);
$registry = $this->prophesize(Registry::class); $registry = $this->prophesize(Registry::class);
$registry->get(Argument::type(EntityWorkflow::class), Argument::type('string')) $registry->get(Argument::type(EntityWorkflow::class), Argument::any())
->willReturn($workflow); ->willReturn($workflow);
$security = $this->prophesize(Security::class); $security = $this->prophesize(Security::class);
@@ -111,4 +113,74 @@ final class NotificationOnTransitionTest extends TestCase
$notificationOnTransition->onCompletedSendNotification($event); $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);
}
} }

View File

@@ -11,9 +11,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Tests\Workflow\Helper; 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\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
@@ -269,217 +266,7 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class); $entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
$entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn($entityWorkflows); $entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn($entityWorkflows);
$repository = $this->prophesize(EntityWorkflowAttachmentRepository::class); return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable()));
$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<EntityWorkflow> $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<EntityWorkflow> $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<EntityWorkflow> $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()));
} }
private static function buildRegistry(): Registry private static function buildRegistry(): Registry

View File

@@ -103,7 +103,10 @@ class NotificationOnTransition implements EventSubscriberInterface
foreach ($dests as $subscriber) { foreach ($dests as $subscriber) {
if ( if (
// prevent to send a notification to the one who created the step
$this->security->getUser() === $subscriber $this->security->getUser() === $subscriber
// prevent to send a notification if the user applyied a signature on the previous step
|| $this->isStepCreatedByPreviousSignature($entityWorkflow, $subscriber)
) { ) {
continue; continue;
} }
@@ -131,4 +134,31 @@ class NotificationOnTransition implements EventSubscriberInterface
$this->entityManager->persist($notification); $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;
}
} }

View File

@@ -11,12 +11,9 @@ declare(strict_types=1);
namespace Chill\MainBundle\Workflow\Helper; namespace Chill\MainBundle\Workflow\Helper;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum; use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
use Chill\MainBundle\Workflow\EntityWorkflowManager; use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Symfony\Component\Clock\ClockInterface; use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
@@ -52,48 +49,28 @@ use Symfony\Component\Workflow\Registry;
* the workflow denys write operations; * 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; * - 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( public function __construct(
private readonly Security $security, private Security $security,
private readonly EntityWorkflowManager $entityWorkflowManager, private EntityWorkflowManager $entityWorkflowManager,
private readonly EntityWorkflowAttachmentRepository $entityWorkflowAttachmentRepository, private Registry $registry,
private readonly Registry $registry, private ClockInterface $clock,
private readonly 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' * @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN'
*/ */
public function isAllowedByWorkflowForReadOperation(object $entity): string public function isAllowedByWorkflowForReadOperation(object $entity): string
{ {
if ($entity instanceof StoredObject) { $entityWorkflows = $entity instanceof EntityWorkflow ? [$entity] : $this->entityWorkflowManager->findByRelatedEntity($entity);
$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;
}
if ($this->isUserInvolvedInAWorkflow($entityWorkflows)) { if ($this->isUserInvolvedInAWorkflow($entityWorkflows)) {
return self::FORCE_GRANT; 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 // give a view permission if there is a Person signature pending, or in the 12 hours following
// the signature last state // the signature last state
foreach ($entityWorkflows as $workflow) { 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' * @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN'
*/ */
public function isAllowedByWorkflowForWriteOperation(object $entity): string public function isAllowedByWorkflowForWriteOperation(object $entity): string
{ {
if ($entity instanceof StoredObject) { $entityWorkflows = $entity instanceof EntityWorkflow ? [$entity] : $this->entityWorkflowManager->findByRelatedEntity($entity);
$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) { if ([] === $entityWorkflows) {
return self::ABSTAIN; 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) { foreach ($entityWorkflows as $entityWorkflow) {
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$marking = $workflow->getMarkingStore()->getMarking($entityWorkflow); $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. // the workflow is final, and final positive, or is sentExternal, so we stop here.
return self::FORCE_DENIED; 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 foreach ($entityWorkflow->getSteps() as $step) {
if (!$isAttached) { foreach ($step->getSignatures() as $signature) {
foreach ($runningWorkflows as $entityWorkflow) { if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) {
foreach ($entityWorkflow->getSteps() as $step) { return self::FORCE_DENIED;
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 // allow only the users involved
if ($this->isUserInvolvedInAWorkflow($runningWorkflows)) { if ($this->isUserInvolvedInAWorkflow($runningWorkflows)) {
return self::FORCE_GRANT; return self::FORCE_GRANT;
} }
if ($isAttached) {
return self::ABSTAIN;
}
return self::FORCE_DENIED; return self::FORCE_DENIED;
} }

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Workflow\Helper;
/**
* Helper to give supplementary permissions to a related entity.
*
* If a related entity is associated within a workflow, the logic of the workflow can give more permissions, or
* remove some permissions.
*
* The methods of this helper return either:
*
* - FORCE_GRANT, which means that a permission can be given, even if it would be denied when the related
* entity is not associated with a workflow;
* - FORCE_DENIED, which means that a permission should be denied, even if it would be granted when the related entity
* is not associated with a workflow
* - ABSTAIN, if there is no workflow logic to add or remove permission
*
* For read operations:
*
* - if the user is involved in the workflow (is part of the current step, of a step before), the user is granted read
* operation;
* - if there is a pending signature for a person, the workflow grant access to the related entity;
* - if there a signature applyied in less than 12 hours, the workflow grant access to the related entity. This allow to
* show the related entity to the person during this time frame.
*
*
* For write operation:
*
* - if the workflow is finalized "positive" (means "not canceled"), the workflow denys write operations;
* - if there isn't any finalized "positive" workflow, and if there is a signature appliyed for a running workflow (not finalized nor canceled),
* 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;
*/
interface WorkflowRelatedEntityPermissionHelperInterface
{
public const FORCE_GRANT = 'FORCE_GRANT';
public const FORCE_DENIED = 'FORCE_DENIED';
public const ABSTAIN = 'ABSTAIN';
/**
* @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;
/**
* @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;
}

View File

@@ -115,3 +115,7 @@ services:
$vienEntityInfoProviders: !tagged_iterator chill_main.entity_info_provider $vienEntityInfoProviders: !tagged_iterator chill_main.entity_info_provider
Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommandHandler: ~ Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommandHandler: ~
Chill\MainBundle\Service\VersionProvider:
arguments:
$packageName: 'chill-project/chill-bundles'

View File

@@ -66,3 +66,7 @@ services:
resource: './../../Templating/Listing' resource: './../../Templating/Listing'
Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface: '@Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory' Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface: '@Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory'
Chill\MainBundle\Templating\VersionRenderExtension:
tags:
- { name: twig.extension }

View File

@@ -48,6 +48,9 @@ See: Voir
Name: Nom Name: Nom
Label: Nom Label: Nom
footer:
Running chill version %version%: "Version de Chill: %version%"
user: user:
current_user: Utilisateur courant current_user: Utilisateur courant
profile: profile:

View File

@@ -38,7 +38,7 @@ class SocialWorkEvaluationApiController extends AbstractController
$pagination->getCurrentPageFirstItemNumber(), $pagination->getCurrentPageFirstItemNumber(),
$pagination->getItemsPerPage() $pagination->getItemsPerPage()
); );
$collection = new Collection($evaluations, $pagination); $collection = new Collection(array_values($evaluations), $pagination);
return $this->json($collection, Response::HTTP_OK, [], ['groups' => ['read']]); return $this->json($collection, Response::HTTP_OK, [], ['groups' => ['read']]);
} }

View File

@@ -25,14 +25,15 @@ class SocialWorkGoalApiController extends ApiController
public function listBySocialAction(Request $request, SocialAction $action): Response public function listBySocialAction(Request $request, SocialAction $action): Response
{ {
$totalItems = $this->goalRepository->countBySocialActionWithDescendants($action); $totalItems = $this->goalRepository->countBySocialActionWithDescendants($action, true);
$paginator = $this->getPaginatorFactory()->create($totalItems); $paginator = $this->paginator->create($totalItems);
$entities = $this->goalRepository->findBySocialActionWithDescendants( $entities = $this->goalRepository->findBySocialActionWithDescendants(
$action, $action,
['id' => 'ASC'], ['id' => 'ASC'],
$paginator->getItemsPerPage(), $paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber() $paginator->getCurrentPageFirstItemNumber(),
onlyActive: true
); );
$model = new Collection($entities, $paginator); $model = new Collection($entities, $paginator);

View File

@@ -25,14 +25,15 @@ class SocialWorkResultApiController extends ApiController
public function listByGoal(Request $request, Goal $goal): Response public function listByGoal(Request $request, Goal $goal): Response
{ {
$totalItems = $this->resultRepository->countByGoal($goal); $totalItems = $this->resultRepository->countByGoal($goal, true);
$paginator = $this->getPaginatorFactory()->create($totalItems); $paginator = $this->getPaginatorFactory()->create($totalItems);
$entities = $this->resultRepository->findByGoal( $entities = $this->resultRepository->findByGoal(
$goal, $goal,
['id' => 'ASC'], ['id' => 'ASC'],
$paginator->getItemsPerPage(), $paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber() $paginator->getCurrentPageFirstItemNumber(),
onlyActive: true,
); );
$model = new Collection($entities, $paginator); $model = new Collection($entities, $paginator);
@@ -42,14 +43,15 @@ class SocialWorkResultApiController extends ApiController
public function listBySocialAction(Request $request, SocialAction $action): Response public function listBySocialAction(Request $request, SocialAction $action): Response
{ {
$totalItems = $this->resultRepository->countBySocialActionWithDescendants($action); $totalItems = $this->resultRepository->countBySocialActionWithDescendants($action, true);
$paginator = $this->getPaginatorFactory()->create($totalItems); $paginator = $this->getPaginatorFactory()->create($totalItems);
$entities = $this->resultRepository->findBySocialActionWithDescendants( $entities = $this->resultRepository->findBySocialActionWithDescendants(
$action, $action,
['id' => 'ASC'], ['id' => 'ASC'],
$paginator->getItemsPerPage(), $paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber() $paginator->getCurrentPageFirstItemNumber(),
onlyActive: true
); );
$model = new Collection($entities, $paginator); $model = new Collection($entities, $paginator);

View File

@@ -19,6 +19,7 @@ use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\PersonBundle\Export\Declarations; use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
final readonly class CenterAggregator implements AggregatorInterface final readonly class CenterAggregator implements AggregatorInterface
{ {
@@ -27,6 +28,7 @@ final readonly class CenterAggregator implements AggregatorInterface
public function __construct( public function __construct(
private CenterRepositoryInterface $centerRepository, private CenterRepositoryInterface $centerRepository,
private RollingDateConverterInterface $rollingDateConverter, private RollingDateConverterInterface $rollingDateConverter,
private TranslatorInterface $translator,
) {} ) {}
public function buildForm(FormBuilderInterface $builder): void public function buildForm(FormBuilderInterface $builder): void
@@ -62,7 +64,7 @@ final readonly class CenterAggregator implements AggregatorInterface
{ {
return function (int|string|null $value) { return function (int|string|null $value) {
if (null === $value || '' === $value) { if (null === $value || '' === $value) {
return ''; return $this->translator->trans('person.export.aggregator.by_center.no_center');
} }
if ('_header' === $value) { if ('_header' === $value) {
@@ -94,15 +96,18 @@ final readonly class CenterAggregator implements AggregatorInterface
$atDate = 'pers_center_agg_at_date'; $atDate = 'pers_center_agg_at_date';
$qb->leftJoin('person.centerHistory', $alias); $qb->leftJoin('person.centerHistory', $alias);
$qb $qb->andWhere(
->andWhere( $qb->expr()->orX(
$qb->expr()->lte($alias.'.startDate', ':'.$atDate), $qb->expr()->isNull($alias.'.id'),
)->andWhere( $qb->expr()->andX(
$qb->expr()->orX( $qb->expr()->lte($alias.'.startDate', ':'.$atDate),
$qb->expr()->isNull($alias.'.endDate'), $qb->expr()->orX(
$qb->expr()->gt($alias.'.endDate', ':'.$atDate) $qb->expr()->isNull($alias.'.endDate'),
$qb->expr()->gt($alias.'.endDate', ':'.$atDate)
)
) )
); )
);
$qb->setParameter($atDate, $this->rollingDateConverter->convert($data['at_date'])); $qb->setParameter($atDate, $this->rollingDateConverter->convert($data['at_date']));
$qb->addSelect("IDENTITY({$alias}.center) AS ".self::COLUMN_NAME); $qb->addSelect("IDENTITY({$alias}.center) AS ".self::COLUMN_NAME);

View File

@@ -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 // 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 // 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 // IMPORTANT: we must NOT bypass selected centers just because the current user is the referrer.
'acp.user = :list_acp_current_user', $aclConditionsOrX = $qb->expr()->orX();
);
$qb->setParameter('list_acp_current_user', $user);
$i = 0; $i = 0;
foreach ($centers as $center) { foreach ($centers as $center) {
@@ -93,6 +91,12 @@ final readonly class FilterListAccompanyingPeriodHelper implements FilterListAcc
++$i; ++$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);
}
} }
} }

View File

@@ -100,9 +100,9 @@ class AccompanyingPeriodWorkRepository implements ObjectRepository
$rsm->addRootEntityFromClassMetadata(AccompanyingPeriodWork::class, 'w'); $rsm->addRootEntityFromClassMetadata(AccompanyingPeriodWork::class, 'w');
$sql = "SELECT {$rsm} FROM chill_person_accompanying_period_work 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 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) AND (rw.enddate IS NULL OR rw.enddate > CURRENT_DATE)
WHERE accompanyingPeriod_id = :periodId"; WHERE accompanyingPeriod_id = :periodId";
// implement filters // implement filters
@@ -136,11 +136,14 @@ class AccompanyingPeriodWorkRepository implements ObjectRepository
} }
// set limit and offset // set limit and offset
$sql .= " ORDER BY $sql .= ' ORDER BY
CASE WHEN w.enddate IS NULL THEN '-infinity'::timestamp ELSE 'infinity'::timestamp END ASC, CASE
w.startdate DESC, WHEN w.enddate IS NULL OR w.enddate > CURRENT_DATE THEN 0
w.enddate DESC, ELSE 1
w.id DESC"; END ASC,
w.startdate DESC,
w.enddate DESC,
w.id DESC';
$sql .= ' LIMIT :limit OFFSET :offset'; $sql .= ' LIMIT :limit OFFSET :offset';

View File

@@ -20,19 +20,23 @@ use Doctrine\ORM\NoResultException;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository; use Doctrine\Persistence\ObjectRepository;
use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Clock\ClockInterface;
final readonly class GoalRepository implements ObjectRepository final readonly class GoalRepository implements ObjectRepository
{ {
private EntityRepository $repository; private EntityRepository $repository;
public function __construct(private EntityManagerInterface $entityManager, private RequestStack $requestStack) public function __construct(
{ private EntityManagerInterface $entityManager,
private ClockInterface $clock,
private RequestStack $requestStack,
) {
$this->repository = $entityManager->getRepository(Goal::class); $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)'); $qb->select('COUNT(g)');
return $qb return $qb
@@ -67,9 +71,9 @@ final readonly class GoalRepository implements ObjectRepository
/** /**
* @return Goal[] * @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->select('g');
$qb->andWhere( $qb->andWhere(
@@ -200,7 +204,7 @@ final readonly class GoalRepository implements ObjectRepository
} }
} }
private function buildQueryBySocialActionWithDescendants(SocialAction $action): QueryBuilder private function buildQueryBySocialActionWithDescendants(SocialAction $action, bool $onlyActive): QueryBuilder
{ {
$actions = $action->getDescendantsWithThis(); $actions = $action->getDescendantsWithThis();
@@ -215,6 +219,11 @@ final readonly class GoalRepository implements ObjectRepository
} }
$qb->where($orx); $qb->where($orx);
if ($onlyActive) {
$qb->andWhere('g.desactivationDate > :now OR g.desactivationDate IS NULL')
->setParameter('now', $this->clock->now());
}
return $qb; return $qb;
} }
} }

View File

@@ -21,19 +21,23 @@ use Doctrine\ORM\NoResultException;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository; use Doctrine\Persistence\ObjectRepository;
use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Clock\ClockInterface;
final readonly class ResultRepository implements ObjectRepository final readonly class ResultRepository implements ObjectRepository
{ {
private EntityRepository $repository; private EntityRepository $repository;
public function __construct(private EntityManagerInterface $entityManager, private RequestStack $requestStack) public function __construct(
{ private EntityManagerInterface $entityManager,
private ClockInterface $clock,
private RequestStack $requestStack,
) {
$this->repository = $entityManager->getRepository(Result::class); $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)'); $qb->select('COUNT(r)');
return $qb return $qb
@@ -41,9 +45,9 @@ final readonly class ResultRepository implements ObjectRepository
->getSingleScalarResult(); ->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)'); $qb->select('COUNT(r)');
return $qb return $qb
@@ -78,9 +82,9 @@ final readonly class ResultRepository implements ObjectRepository
/** /**
* @return array<Result> * @return array<Result>
*/ */
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) { if (null !== $orderBy) {
foreach ($orderBy as $sort => $order) { foreach ($orderBy as $sort => $order) {
@@ -99,9 +103,9 @@ final readonly class ResultRepository implements ObjectRepository
/** /**
* @return Result[] * @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'); $qb->select('r');
foreach ($orderBy as $sort => $order) { foreach ($orderBy as $sort => $order) {
@@ -222,17 +226,22 @@ final readonly class ResultRepository implements ObjectRepository
} }
} }
private function buildQueryByGoal(Goal $goal): QueryBuilder private function buildQueryByGoal(Goal $goal, bool $onlyActive): QueryBuilder
{ {
$qb = $this->repository->createQueryBuilder('r'); $qb = $this->repository->createQueryBuilder('r');
$qb->where(':goal MEMBER OF r.goals') $qb->where(':goal MEMBER OF r.goals')
->setParameter('goal', $goal); ->setParameter('goal', $goal);
if ($onlyActive) {
$qb->andWhere('r.desactivationDate > :now OR r.desactivationDate IS NULL')
->setParameter('now', $this->clock->now());
}
return $qb; return $qb;
} }
private function buildQueryBySocialActionWithDescendants(SocialAction $action): QueryBuilder private function buildQueryBySocialActionWithDescendants(SocialAction $action, bool $onlyActive = false): QueryBuilder
{ {
$actions = $action->getDescendantsWithThis(); $actions = $action->getDescendantsWithThis();
@@ -247,6 +256,11 @@ final readonly class ResultRepository implements ObjectRepository
} }
$qb->where($orx); $qb->where($orx);
if ($onlyActive) {
$qb->andWhere('r.desactivationDate > :now OR r.desactivationDate IS NULL')
->setParameter('now', $this->clock->now());
}
return $qb; return $qb;
} }
} }

View File

@@ -8,8 +8,8 @@
:editor="ClassicEditor" :editor="ClassicEditor"
:config="classicEditorConfig" :config="classicEditorConfig"
:placeholder="trans(EVALUATION_COMMENT_PLACEHOLDER)" :placeholder="trans(EVALUATION_COMMENT_PLACEHOLDER)"
:value="comment" :model-value="comment"
@input="$emit('update:comment', $event)" @update:model-value="$emit('update:comment', $event)"
tag-name="textarea" tag-name="textarea"
></ckeditor> ></ckeditor>
</div> </div>

View File

@@ -25,7 +25,7 @@
</td> </td>
<td> <td>
{% if entity.desactivationDate is not null %} {% if entity.desactivationDate is not null %}
{{ entity.desactivationDate|date('Y-m-d') }} {{ entity.desactivationDate|format_date('medium') }}
{% endif %} {% endif %}
</td> </td>
<td> <td>

View File

@@ -19,7 +19,7 @@
<td>{{ entity.title|localize_translatable_string }}</td> <td>{{ entity.title|localize_translatable_string }}</td>
<td> <td>
{% if entity.desactivationDate is not null %} {% if entity.desactivationDate is not null %}
{{ entity.desactivationDate|date('Y-m-d') }} {{ entity.desactivationDate|format_date('medium') }}
{% endif %} {% endif %}
</td> </td>
<td> <td>

View File

@@ -167,7 +167,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
/** /**
* (non-PHPdoc). * (non-PHPdoc).
* *
* @see \Chill\MainBundle\Search\SearchInterface::getOrder() * @see SearchInterface::getOrder()
*/ */
public function getOrder(): int public function getOrder(): int
{ {
@@ -177,7 +177,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
/** /**
* (non-PHPdoc). * (non-PHPdoc).
* *
* @see \Chill\MainBundle\Search\SearchInterface::isActiveByDefault() * @see SearchInterface::isActiveByDefault()
*/ */
public function isActiveByDefault() public function isActiveByDefault()
{ {

View File

@@ -14,6 +14,7 @@ namespace Chill\PersonBundle\Security\Authorization\StoredObjectVoter;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository; use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository;
@@ -26,8 +27,9 @@ class AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter extends Abstract
private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository, private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository,
Security $security, Security $security,
WorkflowRelatedEntityPermissionHelper $workflowDocumentService, WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
EntityWorkflowAttachmentRepository $attachmentRepository,
) { ) {
parent::__construct($security, $workflowDocumentService); parent::__construct($security, $attachmentRepository, $workflowDocumentService);
} }
protected function getRepository(): AssociatedEntityToStoredObjectInterface protected function getRepository(): AssociatedEntityToStoredObjectInterface

View File

@@ -103,6 +103,11 @@ Employment status: Situation professionelle
Administrative status: Situation administrative Administrative status: Situation administrative
person: person:
# trans key according to new conventions
export:
aggregator:
by_center:
no_center: Sans territoire
Identifiers: Identifiants Identifiers: Identifiants