mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2026-01-15 22:01:23 +00:00
Compare commits
3 Commits
v4.12.0
...
486-user-c
| Author | SHA1 | Date | |
|---|---|---|---|
| 51d97fa3f9 | |||
| 98af0d9bbf | |||
| 835d1a2638 |
6
.changes/unreleased/Feature-20251230-164303.yaml
Normal file
6
.changes/unreleased/Feature-20251230-164303.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
kind: Feature
|
||||
body: Add filter and aggregator based on referrer's main center for exports of accompanying period
|
||||
time: 2025-12-30T16:43:03.898677616+01:00
|
||||
custom:
|
||||
Issue: "486"
|
||||
SchemaChange: No schema change
|
||||
6
.changes/unreleased/Fixed-20251218-102405.yaml
Normal file
6
.changes/unreleased/Fixed-20251218-102405.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
kind: Fixed
|
||||
body: Fix the condition to display concerned persons in calendar list items.
|
||||
time: 2025-12-18T10:24:05.885090777+01:00
|
||||
custom:
|
||||
Issue: "480"
|
||||
SchemaChange: No schema change
|
||||
6
.changes/unreleased/Fixed-20251218-110722.yaml
Normal file
6
.changes/unreleased/Fixed-20251218-110722.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
kind: Fixed
|
||||
body: 'Fix ordering of social actions: actions with a closing date in the future should be considered as ''still open''.'
|
||||
time: 2025-12-18T11:07:22.699897317+01:00
|
||||
custom:
|
||||
Issue: "481"
|
||||
SchemaChange: No schema change
|
||||
6
.changes/unreleased/Fixed-20251230-125728.yaml
Normal file
6
.changes/unreleased/Fixed-20251230-125728.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
kind: Fixed
|
||||
body: Fix export group by center for persons without a center in CenterAggregator.php
|
||||
time: 2025-12-30T12:57:28.773521385+01:00
|
||||
custom:
|
||||
Issue: "477"
|
||||
SchemaChange: No schema change
|
||||
@@ -1,16 +0,0 @@
|
||||
## 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.
|
||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -6,21 +6,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
|
||||
|
||||
## 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)
|
||||
|
||||
## 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
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
"ext-openssl": "*",
|
||||
"ext-redis": "*",
|
||||
"ext-zlib": "*",
|
||||
"composer-runtime-api": "*",
|
||||
"champs-libres/wopi-bundle": "dev-symfony-v5@dev",
|
||||
"champs-libres/wopi-lib": "dev-master@dev",
|
||||
"doctrine/data-fixtures": "^1.8",
|
||||
@@ -83,7 +82,7 @@
|
||||
"symfony/templating": "^5.4",
|
||||
"symfony/translation": "^5.4",
|
||||
"symfony/twig-bundle": "^5.4",
|
||||
"symfony/ux-translator": "2.31.0",
|
||||
"symfony/ux-translator": "^2.22",
|
||||
"symfony/validator": "^5.4",
|
||||
"symfony/webpack-encore-bundle": "^1.11",
|
||||
"symfony/workflow": "^5.4",
|
||||
@@ -98,7 +97,7 @@
|
||||
"require-dev": {
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.3",
|
||||
"fakerphp/faker": "^1.13",
|
||||
"friendsofphp/php-cs-fixer": "3.92.5",
|
||||
"friendsofphp/php-cs-fixer": "3.65.0",
|
||||
"jangregor/phpstan-prophecy": "^1.0",
|
||||
"nelmio/alice": "^3.8",
|
||||
"nikic/php-parser": "^4.15",
|
||||
|
||||
@@ -16,8 +16,7 @@ use Chill\ActivityBundle\Repository\ActivityRepository;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelperInterface;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
|
||||
@@ -25,10 +24,9 @@ class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
|
||||
public function __construct(
|
||||
private readonly ActivityRepository $repository,
|
||||
Security $security,
|
||||
WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService,
|
||||
EntityWorkflowAttachmentRepository $attachmentRepository,
|
||||
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||
) {
|
||||
parent::__construct($security, $attachmentRepository, $workflowDocumentService);
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
}
|
||||
|
||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||
|
||||
@@ -72,20 +72,14 @@
|
||||
|
||||
{% macro table_results(actualCharges, actualResources, results) %}
|
||||
|
||||
{% set now = date() %}
|
||||
|
||||
{% set totalCharges = 0 %}
|
||||
{% for c in actualCharges %}
|
||||
{% if c.startDate <= now and (c.endDate is null or c.endDate >= now) %}
|
||||
{% set totalCharges = totalCharges + c.amount %}
|
||||
{% endif %}
|
||||
{% set totalCharges = totalCharges + c.amount %}
|
||||
{% endfor %}
|
||||
|
||||
{% set totalResources = 0 %}
|
||||
{% for r in actualResources %}
|
||||
{% if r.startDate <= now and (r.endDate is null or r.endDate >= now) %}
|
||||
{% set totalResources = totalResources + r.amount %}
|
||||
{% endif %}
|
||||
{% set totalResources = totalResources + r.amount %}
|
||||
{% endfor %}
|
||||
|
||||
{% set result = (totalResources - totalCharges) %}
|
||||
|
||||
@@ -15,10 +15,7 @@ use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelperInterface;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
@@ -37,8 +34,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
|
||||
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly EntityWorkflowAttachmentRepository $entityWorkflowAttachmentRepository,
|
||||
private readonly WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService,
|
||||
private readonly ?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
|
||||
) {}
|
||||
|
||||
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool
|
||||
@@ -50,6 +46,16 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
|
||||
|
||||
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
|
||||
{
|
||||
// we first try to get the permission from the workflow, as attachement (this is the less intensive query)
|
||||
$workflowPermissionAsAttachment = match ($attribute) {
|
||||
StoredObjectRoleEnum::SEE => $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($subject),
|
||||
StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($subject),
|
||||
};
|
||||
|
||||
if (WorkflowRelatedEntityPermissionHelper::FORCE_DENIED === $workflowPermissionAsAttachment) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Retrieve the related entity
|
||||
$entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
|
||||
|
||||
@@ -59,7 +65,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
|
||||
$regularPermission = $this->security->isGranted($voterAttribute, $entity);
|
||||
|
||||
if (!$this->canBeAssociatedWithWorkflow()) {
|
||||
return $this->voteOnStoredObjectAsAttachementOfAWorkflow($attribute, $regularPermission, $subject);
|
||||
return $regularPermission;
|
||||
}
|
||||
|
||||
$workflowPermission = match ($attribute) {
|
||||
@@ -68,41 +74,9 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
|
||||
};
|
||||
|
||||
return match ($workflowPermission) {
|
||||
WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT => true,
|
||||
WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED => false,
|
||||
WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN => $this->voteOnStoredObjectAsAttachementOfAWorkflow($attribute, $regularPermission, $subject),
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT => true,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED => false,
|
||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN => WorkflowRelatedEntityPermissionHelper::FORCE_GRANT === $workflowPermissionAsAttachment || $regularPermission,
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
@@ -26,9 +25,8 @@ final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredOb
|
||||
private readonly AccompanyingCourseDocumentRepository $repository,
|
||||
Security $security,
|
||||
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||
EntityWorkflowAttachmentRepository $attachmentRepository,
|
||||
) {
|
||||
parent::__construct($security, $attachmentRepository, $workflowDocumentService);
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
}
|
||||
|
||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||
|
||||
@@ -16,7 +16,6 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
|
||||
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
@@ -26,9 +25,8 @@ class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter
|
||||
private readonly PersonDocumentRepository $repository,
|
||||
Security $security,
|
||||
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||
EntityWorkflowAttachmentRepository $attachmentRepository,
|
||||
) {
|
||||
parent::__construct($security, $attachmentRepository, $workflowDocumentService);
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
}
|
||||
|
||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||
|
||||
@@ -16,11 +16,8 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelperInterface;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
@@ -34,31 +31,21 @@ class AbstractStoredObjectVoterTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
/**
|
||||
* @param array<int, EntityWorkflowAttachment> $attachments
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function buildStoredObjectVoter(
|
||||
bool $canBeAssociatedWithWorkflow,
|
||||
AssociatedEntityToStoredObjectInterface $repository,
|
||||
Security $security,
|
||||
?WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService = null,
|
||||
array $attachments = [],
|
||||
?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
|
||||
): AbstractStoredObjectVoter {
|
||||
$attachmentsRepository = $this->prophesize(EntityWorkflowAttachmentRepository::class);
|
||||
$attachmentsRepository->findByStoredObject(Argument::type(StoredObject::class))->willReturn($attachments);
|
||||
|
||||
// Anonymous class extending the abstract class
|
||||
return new class ($canBeAssociatedWithWorkflow, $repository, $security, $attachmentsRepository->reveal(), $workflowDocumentService) extends AbstractStoredObjectVoter {
|
||||
return new class ($canBeAssociatedWithWorkflow, $repository, $security, $workflowDocumentService) extends AbstractStoredObjectVoter {
|
||||
public function __construct(
|
||||
private readonly bool $canBeAssociatedWithWorkflow,
|
||||
private readonly AssociatedEntityToStoredObjectInterface $repository,
|
||||
Security $security,
|
||||
EntityWorkflowAttachmentRepository $attachmentRepository,
|
||||
WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService,
|
||||
?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
|
||||
) {
|
||||
parent::__construct($security, $attachmentRepository, $workflowDocumentService);
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
}
|
||||
|
||||
protected function attributeToRole($attribute): string
|
||||
@@ -85,29 +72,28 @@ class AbstractStoredObjectVoterTest extends TestCase
|
||||
|
||||
public function testSupportsOnAttribute(): void
|
||||
{
|
||||
$entityWorkflowService = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class);
|
||||
|
||||
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(new \stdClass()), $this->prophesize(Security::class)->reveal(), $entityWorkflowService->reveal());
|
||||
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(new \stdClass()), $this->prophesize(Security::class)->reveal(), null);
|
||||
|
||||
self::assertTrue($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject()));
|
||||
|
||||
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(new User()), $this->prophesize(Security::class)->reveal(), $entityWorkflowService->reveal());
|
||||
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(new User()), $this->prophesize(Security::class)->reveal(), null);
|
||||
|
||||
self::assertFalse($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject()));
|
||||
|
||||
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(null), $this->prophesize(Security::class)->reveal(), $entityWorkflowService->reveal());
|
||||
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(null), $this->prophesize(Security::class)->reveal(), null);
|
||||
|
||||
self::assertFalse($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderVoteOnAttributeWithWorkflow
|
||||
* @dataProvider dataProviderVoteOnAttributeWithStoredObjectPermission
|
||||
*/
|
||||
public function testVoteOnAttributeWithWorkflow(
|
||||
public function testVoteOnAttributeWithStoredObjectPermission(
|
||||
StoredObjectRoleEnum $attribute,
|
||||
bool $expected,
|
||||
bool $isGrantedRegularPermission,
|
||||
string $isGrantedWorkflowPermission,
|
||||
string $isGrantedStoredObjectAttachment,
|
||||
): void {
|
||||
$storedObject = new StoredObject();
|
||||
$repository = new DummyRepository($related = new \stdClass());
|
||||
@@ -116,28 +102,31 @@ class AbstractStoredObjectVoterTest extends TestCase
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
|
||||
|
||||
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class);
|
||||
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);
|
||||
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
|
||||
|
||||
$attachementRepository = $this->prophesize(EntityWorkflowAttachmentRepository::class);
|
||||
$attachementRepository->findByStoredObject($storedObject)->willReturn([]);
|
||||
|
||||
if (StoredObjectRoleEnum::SEE === $attribute) {
|
||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)
|
||||
->shouldBeCalled()
|
||||
->willReturn($isGrantedStoredObjectAttachment);
|
||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
|
||||
->willReturn($isGrantedWorkflowPermission);
|
||||
} elseif (StoredObjectRoleEnum::EDIT === $attribute) {
|
||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)
|
||||
->shouldBeCalled()
|
||||
->willReturn($isGrantedStoredObjectAttachment);
|
||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related)
|
||||
->willReturn($isGrantedWorkflowPermission);
|
||||
} else {
|
||||
throw new \LogicException('Invalid attribute for StoredObjectVoter');
|
||||
}
|
||||
|
||||
$storedObjectVoter = new class ($repository, $workflowRelatedEntityPermissionHelper->reveal(), $security->reveal(), $attachementRepository->reveal()) extends AbstractStoredObjectVoter {
|
||||
public function __construct(private $repository, $helper, $security, EntityWorkflowAttachmentRepository $attachmentRepository)
|
||||
$storedObjectVoter = new class ($repository, $workflowRelatedEntityPermissionHelper->reveal(), $security->reveal()) extends AbstractStoredObjectVoter {
|
||||
public function __construct(private $repository, $helper, $security)
|
||||
{
|
||||
parent::__construct($security, $attachmentRepository, $helper);
|
||||
parent::__construct($security, $helper);
|
||||
}
|
||||
|
||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||
@@ -166,64 +155,96 @@ class AbstractStoredObjectVoterTest extends TestCase
|
||||
self::assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
public static function dataProviderVoteOnAttributeWithWorkflow(): iterable
|
||||
public static function dataProviderVoteOnAttributeWithStoredObjectPermission(): iterable
|
||||
{
|
||||
foreach (['read' => StoredObjectRoleEnum::SEE, 'write' => StoredObjectRoleEnum::EDIT] as $action => $attribute) {
|
||||
yield 'Not related to any workflow nor attachment ('.$action.')' => [
|
||||
$attribute,
|
||||
true,
|
||||
true,
|
||||
WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
|
||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
||||
];
|
||||
|
||||
yield 'Not related to any workflow nor attachment (refuse) ('.$action.')' => [
|
||||
$attribute,
|
||||
false,
|
||||
false,
|
||||
WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
|
||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
||||
];
|
||||
|
||||
yield 'Is granted by a workflow takes precedence (workflow) ('.$action.')' => [
|
||||
$attribute,
|
||||
false,
|
||||
true,
|
||||
WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
||||
];
|
||||
|
||||
yield 'Is granted by a workflow takes precedence (stored object) ('.$action.')' => [
|
||||
$attribute,
|
||||
false,
|
||||
true,
|
||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
||||
];
|
||||
|
||||
yield 'Is granted by a workflow takes precedence (workflow) although grant ('.$action.')' => [
|
||||
$attribute,
|
||||
false,
|
||||
WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
|
||||
true,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
||||
];
|
||||
|
||||
yield 'Is granted by a workflow takes precedence (stored object) although grant ('.$action.')' => [
|
||||
$attribute,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
||||
];
|
||||
|
||||
yield 'Is granted by a workflow takes precedence (initially refused) (workflow) although grant ('.$action.')' => [
|
||||
$attribute,
|
||||
false,
|
||||
false,
|
||||
WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
||||
];
|
||||
|
||||
yield 'Is granted by a workflow takes precedence (initially refused) (stored object) although grant ('.$action.')' => [
|
||||
$attribute,
|
||||
false,
|
||||
false,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
||||
];
|
||||
|
||||
yield 'Force grant inverse the regular permission (workflow) ('.$action.')' => [
|
||||
$attribute,
|
||||
true,
|
||||
false,
|
||||
WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
||||
];
|
||||
|
||||
yield 'Force grant inverse the regular permission (so) ('.$action.')' => [
|
||||
$attribute,
|
||||
true,
|
||||
false,
|
||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
||||
];
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderVoteOnAttribute
|
||||
* @dataProvider dataProviderVoteOnAttributeWithoutStoredObjectPermission
|
||||
*/
|
||||
public function testVoteOnAttribute(
|
||||
public function testVoteOnAttributeWithoutStoredObjectPermission(
|
||||
StoredObjectRoleEnum $attribute,
|
||||
bool $expected,
|
||||
bool $canBeAssociatedWithWorkflow,
|
||||
@@ -239,7 +260,10 @@ class AbstractStoredObjectVoterTest extends TestCase
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
|
||||
|
||||
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class);
|
||||
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);
|
||||
|
||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN);
|
||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN);
|
||||
|
||||
if (null !== $isGrantedWorkflowPermissionRead) {
|
||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
|
||||
@@ -259,155 +283,27 @@ class AbstractStoredObjectVoterTest extends TestCase
|
||||
self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message);
|
||||
}
|
||||
|
||||
public static function dataProviderVoteOnAttribute(): iterable
|
||||
public static function dataProviderVoteOnAttributeWithoutStoredObjectPermission(): iterable
|
||||
{
|
||||
// not associated on a workflow
|
||||
yield [StoredObjectRoleEnum::SEE, true, false, true, null, null, 'not associated on a workflow, granted by regular access, must not rely on helper'];
|
||||
yield [StoredObjectRoleEnum::SEE, false, false, false, null, null, 'not associated on a workflow, denied by regular access, must not rely on helper'];
|
||||
|
||||
// associated on a workflow, read operation
|
||||
yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be granted'];
|
||||
yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, null, 'associated on a workflow, read by regular, abstain by workflow, ask for read, should be granted'];
|
||||
yield [StoredObjectRoleEnum::SEE, false, true, true, WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be denied'];
|
||||
yield [StoredObjectRoleEnum::SEE, true, true, false, WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be granted'];
|
||||
yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, null, 'associated on a workflow, denied read by regular, abstain by workflow, ask for read, should be granted'];
|
||||
yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be denied'];
|
||||
yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be granted'];
|
||||
yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelper::ABSTAIN, null, 'associated on a workflow, read by regular, abstain by workflow, ask for read, should be granted'];
|
||||
yield [StoredObjectRoleEnum::SEE, false, true, true, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be denied'];
|
||||
yield [StoredObjectRoleEnum::SEE, true, true, false, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be granted'];
|
||||
yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelper::ABSTAIN, null, 'associated on a workflow, denied read by regular, abstain by workflow, ask for read, should be granted'];
|
||||
yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be denied'];
|
||||
|
||||
// association on a workflow, write operation
|
||||
yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be granted'];
|
||||
yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, 'associated on a workflow, write by regular, abstain by workflow, ask for write, should be granted'];
|
||||
yield [StoredObjectRoleEnum::EDIT, false, true, true, null, WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be denied'];
|
||||
yield [StoredObjectRoleEnum::EDIT, true, true, false, null, WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be granted'];
|
||||
yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, 'associated on a workflow, denied write by regular, abstain by workflow, ask for write, should be granted'];
|
||||
yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be denied'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderPrecedenceOfDirectAssociationOverWorkflowAttachments
|
||||
*/
|
||||
public function testPrecedenceOfDirectAssociationOverWorkflowAttachments(
|
||||
StoredObjectRoleEnum $attribute,
|
||||
bool $expected,
|
||||
bool $regularPermission,
|
||||
string $directWorkflowPermission,
|
||||
string $attachmentWorkflowPermission,
|
||||
string $message,
|
||||
): void {
|
||||
$storedObject = new StoredObject();
|
||||
$repository = new DummyRepository($related = new \stdClass());
|
||||
$token = new UsernamePasswordToken(new User(), 'dummy');
|
||||
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->isGranted('SOME_ROLE', $related)->willReturn($regularPermission);
|
||||
|
||||
$workflowHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class);
|
||||
|
||||
// Direct association permission
|
||||
if (StoredObjectRoleEnum::SEE === $attribute) {
|
||||
$workflowHelper->isAllowedByWorkflowForReadOperation($related)
|
||||
->willReturn($directWorkflowPermission);
|
||||
} else {
|
||||
$workflowHelper->isAllowedByWorkflowForWriteOperation($related)
|
||||
->willReturn($directWorkflowPermission);
|
||||
}
|
||||
|
||||
// Attachment permission
|
||||
$entityWorkflow = $this->prophesize(\Chill\MainBundle\Entity\Workflow\EntityWorkflow::class)->reveal();
|
||||
$attachment = $this->prophesize(EntityWorkflowAttachment::class);
|
||||
$attachment->getEntityWorkflow()->willReturn($entityWorkflow);
|
||||
|
||||
if (StoredObjectRoleEnum::SEE === $attribute) {
|
||||
$workflowHelper->isAllowedByWorkflowForReadOperation($entityWorkflow)
|
||||
->willReturn($attachmentWorkflowPermission);
|
||||
} else {
|
||||
$workflowHelper->isAllowedByWorkflowForWriteOperation($entityWorkflow)
|
||||
->willReturn($attachmentWorkflowPermission);
|
||||
}
|
||||
|
||||
$voter = $this->buildStoredObjectVoter(
|
||||
true,
|
||||
$repository,
|
||||
$security->reveal(),
|
||||
$workflowHelper->reveal(),
|
||||
[$attachment->reveal()]
|
||||
);
|
||||
|
||||
self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message);
|
||||
}
|
||||
|
||||
public static function dataProviderPrecedenceOfDirectAssociationOverWorkflowAttachments(): iterable
|
||||
{
|
||||
$cases = [
|
||||
[
|
||||
'expected' => true,
|
||||
'regular' => false,
|
||||
'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
|
||||
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
|
||||
'message' => 'Direct FORCE_GRANT should win over attachment FORCE_DENIED',
|
||||
],
|
||||
[
|
||||
'expected' => false,
|
||||
'regular' => true,
|
||||
'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
|
||||
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
|
||||
'message' => 'Direct FORCE_DENIED should win over attachment FORCE_GRANT',
|
||||
],
|
||||
[
|
||||
'expected' => true,
|
||||
'regular' => false,
|
||||
'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
|
||||
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
|
||||
'message' => 'Direct FORCE_GRANT should win over attachment ABSTAIN',
|
||||
],
|
||||
[
|
||||
'expected' => false,
|
||||
'regular' => true,
|
||||
'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
|
||||
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
|
||||
'message' => 'Direct FORCE_DENIED should win over attachment ABSTAIN',
|
||||
],
|
||||
[
|
||||
'expected' => true,
|
||||
'regular' => false,
|
||||
'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
|
||||
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
|
||||
'message' => 'Direct ABSTAIN should let attachment FORCE_GRANT win',
|
||||
],
|
||||
[
|
||||
'expected' => false,
|
||||
'regular' => true,
|
||||
'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
|
||||
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
|
||||
'message' => 'Direct ABSTAIN should let attachment FORCE_DENIED win',
|
||||
],
|
||||
[
|
||||
'expected' => true,
|
||||
'regular' => true,
|
||||
'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
|
||||
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
|
||||
'message' => 'Both ABSTAIN should let regular permission (true) win',
|
||||
],
|
||||
[
|
||||
'expected' => false,
|
||||
'regular' => false,
|
||||
'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
|
||||
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
|
||||
'message' => 'Both ABSTAIN should let regular permission (false) win',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ([StoredObjectRoleEnum::SEE, StoredObjectRoleEnum::EDIT] as $attribute) {
|
||||
foreach ($cases as $case) {
|
||||
yield sprintf('%s - %s', $attribute->name, $case['message']) => [
|
||||
$attribute,
|
||||
$case['expected'],
|
||||
$case['regular'],
|
||||
$case['direct'],
|
||||
$case['attachment'],
|
||||
$case['message'],
|
||||
];
|
||||
}
|
||||
}
|
||||
yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be granted'];
|
||||
yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelper::ABSTAIN, 'associated on a workflow, write by regular, abstain by workflow, ask for write, should be granted'];
|
||||
yield [StoredObjectRoleEnum::EDIT, false, true, true, null, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be denied'];
|
||||
yield [StoredObjectRoleEnum::EDIT, true, true, false, null, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be granted'];
|
||||
yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelper::ABSTAIN, 'associated on a workflow, denied write by regular, abstain by workflow, ask for write, should be granted'];
|
||||
yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be denied'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ namespace Chill\EventBundle\Security\Authorization;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Chill\EventBundle\Entity\Event;
|
||||
use Chill\EventBundle\Repository\EventRepository;
|
||||
@@ -27,9 +26,8 @@ class EventStoredObjectVoter extends AbstractStoredObjectVoter
|
||||
private readonly EventRepository $repository,
|
||||
Security $security,
|
||||
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||
EntityWorkflowAttachmentRepository $attachmentRepository,
|
||||
) {
|
||||
parent::__construct($security, $attachmentRepository, $workflowDocumentService);
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
}
|
||||
|
||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
<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 }}
|
||||
<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">
|
||||
{{ 'User manual'|trans }}
|
||||
</a>
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ class CancelStaleWorkflowCronJob implements CronJobInterface
|
||||
{
|
||||
public const KEY = 'remove-stale-workflow';
|
||||
|
||||
public const KEEP_INTERVAL = 'P180D';
|
||||
public const KEEP_INTERVAL = 'P90D';
|
||||
|
||||
private const LAST_CANCELED_WORKFLOW = 'last-canceled-workflow-id';
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
@@ -42,14 +42,14 @@ class CancelStaleWorkflowHandlerTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testWorkflowWithOneStepOlderThan180DaysIsCanceled(): void
|
||||
public function testWorkflowWithOneStepOlderThan90DaysIsCanceled(): void
|
||||
{
|
||||
$clock = new MockClock('2024-01-01');
|
||||
$daysAgos = new \DateTimeImmutable('2023-06-01');
|
||||
$daysAgos = new \DateTimeImmutable('2023-09-01');
|
||||
|
||||
$workflow = new EntityWorkflow();
|
||||
$workflow->setWorkflowName('dummy_workflow');
|
||||
$workflow->setCreatedAt(new \DateTimeImmutable('2023-06-01'));
|
||||
$workflow->setCreatedAt(new \DateTimeImmutable('2023-09-01'));
|
||||
$workflow->setStep('step1', new WorkflowTransitionContextDTO($workflow), 'to_step1', $daysAgos, new User());
|
||||
|
||||
$em = $this->prophesize(EntityManagerInterface::class);
|
||||
@@ -94,7 +94,7 @@ class CancelStaleWorkflowHandlerTest extends TestCase
|
||||
|
||||
$workflow = new EntityWorkflow();
|
||||
$workflow->setWorkflowName('dummy_workflow');
|
||||
$workflow->setCreatedAt(new \DateTimeImmutable('2023-06-01'));
|
||||
$workflow->setCreatedAt(new \DateTimeImmutable('2023-09-01'));
|
||||
|
||||
$em = $this->prophesize(EntityManagerInterface::class);
|
||||
$em->flush()->shouldBeCalled();
|
||||
|
||||
@@ -15,9 +15,7 @@ use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\EventSubscriber\NotificationOnTransition;
|
||||
@@ -89,7 +87,7 @@ final class NotificationOnTransitionTest extends TestCase
|
||||
->willReturn([]);
|
||||
|
||||
$registry = $this->prophesize(Registry::class);
|
||||
$registry->get(Argument::type(EntityWorkflow::class), Argument::any())
|
||||
$registry->get(Argument::type(EntityWorkflow::class), Argument::type('string'))
|
||||
->willReturn($workflow);
|
||||
|
||||
$security = $this->prophesize(Security::class);
|
||||
@@ -113,74 +111,4 @@ final class NotificationOnTransitionTest extends TestCase
|
||||
|
||||
$notificationOnTransition->onCompletedSendNotification($event);
|
||||
}
|
||||
|
||||
public function testOnCompleteDoNotSendNotificationIfStepCreatedByPreviousSignature(): void
|
||||
{
|
||||
$dest = new User();
|
||||
$currentUser = new User();
|
||||
$workflowProphecy = $this->prophesize(WorkflowInterface::class);
|
||||
$workflow = $workflowProphecy->reveal();
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$entityWorkflow
|
||||
->setWorkflowName('workflow_name')
|
||||
->setRelatedEntityClass(\stdClass::class)
|
||||
->setRelatedEntityId(1);
|
||||
// force an id to entityWorkflow:
|
||||
$reflection = new \ReflectionClass($entityWorkflow);
|
||||
$id = $reflection->getProperty('id');
|
||||
$id->setValue($entityWorkflow, 1);
|
||||
|
||||
$previousStep = new EntityWorkflowStep();
|
||||
$previousStep->addSignature($signature = new EntityWorkflowStepSignature($previousStep, $dest));
|
||||
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED);
|
||||
|
||||
$currentStep = new EntityWorkflowStep();
|
||||
$currentStep->addDestUser($dest);
|
||||
$currentStep->setCurrentStep('to_state');
|
||||
|
||||
$entityWorkflow->addStep($previousStep);
|
||||
$entityWorkflow->addStep($currentStep);
|
||||
|
||||
$em = $this->prophesize(EntityManagerInterface::class);
|
||||
|
||||
// we check that NO notification has been persisted for $dest
|
||||
$em->persist(Argument::that(
|
||||
fn ($notificationCandidate) => $notificationCandidate instanceof Notification && $notificationCandidate->getAddressees()->contains($dest)
|
||||
))->shouldNotBeCalled();
|
||||
|
||||
$engine = $this->prophesize(\Twig\Environment::class);
|
||||
$engine->render(Argument::type('string'), Argument::type('array'))
|
||||
->willReturn('dummy text');
|
||||
|
||||
$extractor = $this->prophesize(MetadataExtractor::class);
|
||||
$extractor->buildArrayPresentationForPlace(Argument::type(EntityWorkflow::class), Argument::any())
|
||||
->willReturn([]);
|
||||
$extractor->buildArrayPresentationForWorkflow(Argument::any())
|
||||
->willReturn([]);
|
||||
|
||||
$registry = $this->prophesize(Registry::class);
|
||||
$registry->get(Argument::type(EntityWorkflow::class), Argument::any())
|
||||
->willReturn($workflow);
|
||||
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->getUser()->willReturn(null);
|
||||
|
||||
$entityWorkflowHandler = $this->prophesize(EntityWorkflowHandlerInterface::class);
|
||||
$entityWorkflowHandler->getEntityTitle($entityWorkflow)->willReturn('workflow title');
|
||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
||||
$entityWorkflowManager->getHandler($entityWorkflow)->willReturn($entityWorkflowHandler->reveal());
|
||||
|
||||
$notificationOnTransition = new NotificationOnTransition(
|
||||
$em->reveal(),
|
||||
$engine->reveal(),
|
||||
$extractor->reveal(),
|
||||
$security->reveal(),
|
||||
$registry->reveal(),
|
||||
$entityWorkflowManager->reveal(),
|
||||
);
|
||||
|
||||
$event = new Event($entityWorkflow, new Marking(), new Transition('dummy_transition', ['from_state'], ['to_state']), $workflow);
|
||||
|
||||
$notificationOnTransition->onCompletedSendNotification($event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Tests\Workflow\Helper;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
@@ -266,7 +269,217 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
|
||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
||||
$entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn($entityWorkflows);
|
||||
|
||||
return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable()));
|
||||
$repository = $this->prophesize(EntityWorkflowAttachmentRepository::class);
|
||||
$repository->findByStoredObject(Argument::type(StoredObject::class))->willReturn([]);
|
||||
|
||||
return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $repository->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideDataAllowedByWorkflowReadOperationByAttachment
|
||||
*
|
||||
* @param list<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
|
||||
|
||||
@@ -103,10 +103,7 @@ class NotificationOnTransition implements EventSubscriberInterface
|
||||
|
||||
foreach ($dests as $subscriber) {
|
||||
if (
|
||||
// prevent to send a notification to the one who created the step
|
||||
$this->security->getUser() === $subscriber
|
||||
// prevent to send a notification if the user applyied a signature on the previous step
|
||||
|| $this->isStepCreatedByPreviousSignature($entityWorkflow, $subscriber)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -134,31 +131,4 @@ class NotificationOnTransition implements EventSubscriberInterface
|
||||
$this->entityManager->persist($notification);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current step in the workflow was created by a previous signature of the specified user.
|
||||
*
|
||||
* This method retrieves the current step of the workflow and its preceding step. It iterates through
|
||||
* the signatures of the preceding step to verify if the provided user is the signer of any of those
|
||||
* signatures. Returns true if the user matches any signer; otherwise, returns false.
|
||||
*
|
||||
* @param EntityWorkflow $entityWorkflow the workflow entity containing the current step and its details
|
||||
* @param User $user the user to check against the signatures of the previous step in the workflow
|
||||
*
|
||||
* @return bool true if the specified user created the step via a previous signature, false otherwise
|
||||
*/
|
||||
private function isStepCreatedByPreviousSignature(EntityWorkflow $entityWorkflow, User $user): bool
|
||||
{
|
||||
$step = $entityWorkflow->getCurrentStepChained();
|
||||
$previous = $step->getPrevious();
|
||||
|
||||
|
||||
foreach ($previous->getSignatures() as $signature) {
|
||||
if ($signature->getSigner() === $user) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Workflow\Helper;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
@@ -49,28 +52,48 @@ use Symfony\Component\Workflow\Registry;
|
||||
* the workflow denys write operations;
|
||||
* - if there is no case above and the user is involved in the workflow (is part of the current step, of a step before), the user is granted;
|
||||
*/
|
||||
final readonly class WorkflowRelatedEntityPermissionHelper implements WorkflowRelatedEntityPermissionHelperInterface
|
||||
class WorkflowRelatedEntityPermissionHelper
|
||||
{
|
||||
public const FORCE_GRANT = 'FORCE_GRANT';
|
||||
public const FORCE_DENIED = 'FORCE_DENIED';
|
||||
public const ABSTAIN = 'ABSTAIN';
|
||||
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private EntityWorkflowManager $entityWorkflowManager,
|
||||
private Registry $registry,
|
||||
private ClockInterface $clock,
|
||||
private readonly Security $security,
|
||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||
private readonly EntityWorkflowAttachmentRepository $entityWorkflowAttachmentRepository,
|
||||
private readonly Registry $registry,
|
||||
private readonly ClockInterface $clock,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param object $entity The entity may be an object that is a related entity of a workflow, or an EntityWorkflow itself
|
||||
* @param object $entity The entity may be an
|
||||
*
|
||||
* @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN'
|
||||
*/
|
||||
public function isAllowedByWorkflowForReadOperation(object $entity): string
|
||||
{
|
||||
$entityWorkflows = $entity instanceof EntityWorkflow ? [$entity] : $this->entityWorkflowManager->findByRelatedEntity($entity);
|
||||
if ($entity instanceof StoredObject) {
|
||||
$attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($entity);
|
||||
$entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments);
|
||||
$isAttached = true;
|
||||
} else {
|
||||
$entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
|
||||
$isAttached = false;
|
||||
}
|
||||
|
||||
if ([] === $entityWorkflows) {
|
||||
return self::ABSTAIN;
|
||||
}
|
||||
|
||||
if ($this->isUserInvolvedInAWorkflow($entityWorkflows)) {
|
||||
return self::FORCE_GRANT;
|
||||
}
|
||||
|
||||
if ($isAttached) {
|
||||
return self::ABSTAIN;
|
||||
}
|
||||
|
||||
// give a view permission if there is a Person signature pending, or in the 12 hours following
|
||||
// the signature last state
|
||||
foreach ($entityWorkflows as $workflow) {
|
||||
@@ -94,20 +117,24 @@ final readonly class WorkflowRelatedEntityPermissionHelper implements WorkflowRe
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
{
|
||||
$entityWorkflows = $entity instanceof EntityWorkflow ? [$entity] : $this->entityWorkflowManager->findByRelatedEntity($entity);
|
||||
if ($entity instanceof StoredObject) {
|
||||
$attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($entity);
|
||||
$entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments);
|
||||
$isAttached = true;
|
||||
} else {
|
||||
$entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
|
||||
$isAttached = false;
|
||||
}
|
||||
|
||||
if ([] === $entityWorkflows) {
|
||||
return self::ABSTAIN;
|
||||
}
|
||||
|
||||
|
||||
// if a workflow is finalized positive or isSentExternal, no one is allowed to edit the document anymore
|
||||
// if a workflow is finalized positive, anyone is allowed to edit the document anymore
|
||||
foreach ($entityWorkflows as $entityWorkflow) {
|
||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
$marking = $workflow->getMarkingStore()->getMarking($entityWorkflow);
|
||||
@@ -120,57 +147,28 @@ final readonly class WorkflowRelatedEntityPermissionHelper implements WorkflowRe
|
||||
// the workflow is final, and final positive, or is sentExternal, so we stop here.
|
||||
return self::FORCE_DENIED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
foreach ($entityWorkflow->getSteps() as $step) {
|
||||
foreach ($step->getSignatures() as $signature) {
|
||||
if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) {
|
||||
return self::FORCE_DENIED;
|
||||
}
|
||||
if (
|
||||
// if not finalized positive
|
||||
$entityWorkflow->isFinal() && !($placeMetadata['isFinalPositive'] ?? false)
|
||||
) {
|
||||
return self::ABSTAIN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
$runningWorkflows = array_filter($entityWorkflows, fn (EntityWorkflow $ew) => !$ew->isFinal());
|
||||
|
||||
// if there is a signature on a **running workflow**, no one is allowed edit the workflow anymore
|
||||
if (!$isAttached) {
|
||||
foreach ($runningWorkflows as $entityWorkflow) {
|
||||
foreach ($entityWorkflow->getSteps() as $step) {
|
||||
foreach ($step->getSignatures() as $signature) {
|
||||
if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) {
|
||||
return self::FORCE_DENIED;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$isFinalNegative) {
|
||||
$runningWorkflows[] = $entityWorkflow;
|
||||
}
|
||||
}
|
||||
|
||||
if ([] === $runningWorkflows) {
|
||||
return self::ABSTAIN;
|
||||
}
|
||||
|
||||
// allow only the users involved
|
||||
@@ -178,6 +176,10 @@ final readonly class WorkflowRelatedEntityPermissionHelper implements WorkflowRe
|
||||
return self::FORCE_GRANT;
|
||||
}
|
||||
|
||||
if ($isAttached) {
|
||||
return self::ABSTAIN;
|
||||
}
|
||||
|
||||
return self::FORCE_DENIED;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -115,7 +115,3 @@ services:
|
||||
$vienEntityInfoProviders: !tagged_iterator chill_main.entity_info_provider
|
||||
|
||||
Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommandHandler: ~
|
||||
|
||||
Chill\MainBundle\Service\VersionProvider:
|
||||
arguments:
|
||||
$packageName: 'chill-project/chill-bundles'
|
||||
|
||||
@@ -66,7 +66,3 @@ services:
|
||||
resource: './../../Templating/Listing'
|
||||
|
||||
Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface: '@Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory'
|
||||
|
||||
Chill\MainBundle\Templating\VersionRenderExtension:
|
||||
tags:
|
||||
- { name: twig.extension }
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
common:
|
||||
after: Après
|
||||
until: Jusqu'à
|
||||
centers: Territoires
|
||||
"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>": "Ce programme est un logiciel libre: vous pouvez le redistribuer et/ou le modifier selon les termes de la licence <strong>GNU Affero GPL</strong>"
|
||||
User manual: Manuel d'utilisation
|
||||
Search: Rechercher
|
||||
@@ -48,9 +52,6 @@ See: Voir
|
||||
Name: Nom
|
||||
Label: Nom
|
||||
|
||||
footer:
|
||||
Running chill version %version%: "Version de Chill: %version%"
|
||||
|
||||
user:
|
||||
current_user: Utilisateur courant
|
||||
profile:
|
||||
|
||||
@@ -38,7 +38,7 @@ class SocialWorkEvaluationApiController extends AbstractController
|
||||
$pagination->getCurrentPageFirstItemNumber(),
|
||||
$pagination->getItemsPerPage()
|
||||
);
|
||||
$collection = new Collection(array_values($evaluations), $pagination);
|
||||
$collection = new Collection($evaluations, $pagination);
|
||||
|
||||
return $this->json($collection, Response::HTTP_OK, [], ['groups' => ['read']]);
|
||||
}
|
||||
|
||||
@@ -25,15 +25,14 @@ class SocialWorkGoalApiController extends ApiController
|
||||
|
||||
public function listBySocialAction(Request $request, SocialAction $action): Response
|
||||
{
|
||||
$totalItems = $this->goalRepository->countBySocialActionWithDescendants($action, true);
|
||||
$paginator = $this->paginator->create($totalItems);
|
||||
$totalItems = $this->goalRepository->countBySocialActionWithDescendants($action);
|
||||
$paginator = $this->getPaginatorFactory()->create($totalItems);
|
||||
|
||||
$entities = $this->goalRepository->findBySocialActionWithDescendants(
|
||||
$action,
|
||||
['id' => 'ASC'],
|
||||
$paginator->getItemsPerPage(),
|
||||
$paginator->getCurrentPageFirstItemNumber(),
|
||||
onlyActive: true
|
||||
$paginator->getCurrentPageFirstItemNumber()
|
||||
);
|
||||
|
||||
$model = new Collection($entities, $paginator);
|
||||
|
||||
@@ -25,15 +25,14 @@ class SocialWorkResultApiController extends ApiController
|
||||
|
||||
public function listByGoal(Request $request, Goal $goal): Response
|
||||
{
|
||||
$totalItems = $this->resultRepository->countByGoal($goal, true);
|
||||
$totalItems = $this->resultRepository->countByGoal($goal);
|
||||
$paginator = $this->getPaginatorFactory()->create($totalItems);
|
||||
|
||||
$entities = $this->resultRepository->findByGoal(
|
||||
$goal,
|
||||
['id' => 'ASC'],
|
||||
$paginator->getItemsPerPage(),
|
||||
$paginator->getCurrentPageFirstItemNumber(),
|
||||
onlyActive: true,
|
||||
$paginator->getCurrentPageFirstItemNumber()
|
||||
);
|
||||
|
||||
$model = new Collection($entities, $paginator);
|
||||
@@ -43,15 +42,14 @@ class SocialWorkResultApiController extends ApiController
|
||||
|
||||
public function listBySocialAction(Request $request, SocialAction $action): Response
|
||||
{
|
||||
$totalItems = $this->resultRepository->countBySocialActionWithDescendants($action, true);
|
||||
$totalItems = $this->resultRepository->countBySocialActionWithDescendants($action);
|
||||
$paginator = $this->getPaginatorFactory()->create($totalItems);
|
||||
|
||||
$entities = $this->resultRepository->findBySocialActionWithDescendants(
|
||||
$action,
|
||||
['id' => 'ASC'],
|
||||
$paginator->getItemsPerPage(),
|
||||
$paginator->getCurrentPageFirstItemNumber(),
|
||||
onlyActive: true
|
||||
$paginator->getCurrentPageFirstItemNumber()
|
||||
);
|
||||
|
||||
$model = new Collection($entities, $paginator);
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<?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\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
|
||||
|
||||
use Chill\MainBundle\Export\AggregatorInterface;
|
||||
use Chill\MainBundle\Export\DataTransformerInterface;
|
||||
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||
use Chill\MainBundle\Repository\CenterRepositoryInterface;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Chill\PersonBundle\Export\Declarations;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
final readonly class ReferrerMainCenterAggregator implements AggregatorInterface, DataTransformerInterface
|
||||
{
|
||||
private const P = 'acp_agg_referrer_main_center';
|
||||
|
||||
public function __construct(
|
||||
private CenterRepositoryInterface $centerRepository,
|
||||
private RollingDateConverterInterface $rollingDateConverter,
|
||||
) {}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void
|
||||
{
|
||||
$p = self::P;
|
||||
|
||||
$qb
|
||||
->leftJoin('acp.userHistories', "{$p}_uh", Join::WITH, $qb->expr()->andX(
|
||||
$qb->expr()->eq("{$p}_uh.accompanyingPeriod", 'acp.id'),
|
||||
"OVERLAPSI (acp.openingDate, acp.closingDate), ({$p}_uh.startDate, {$p}_uh.endDate) = TRUE",
|
||||
"OVERLAPSI (:{$p}_startDate, :{$p}_endDate), ({$p}_uh.startDate, {$p}_uh.endDate) = TRUE"
|
||||
))
|
||||
->leftJoin("{$p}_uh.user", "{$p}_user")
|
||||
->addSelect("IDENTITY({$p}_user.mainCenter) AS {$p}_select")
|
||||
->addGroupBy("{$p}_select")
|
||||
->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date']))
|
||||
->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date']));
|
||||
}
|
||||
|
||||
public function applyOn(): string
|
||||
{
|
||||
return Declarations::ACP_TYPE;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder): void
|
||||
{
|
||||
$builder
|
||||
->add('start_date', PickRollingDateType::class, [
|
||||
'label' => 'common.after',
|
||||
'required' => true,
|
||||
])
|
||||
->add('end_date', PickRollingDateType::class, [
|
||||
'label' => 'common.until',
|
||||
'required' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getNormalizationVersion(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function normalizeFormData(array $formData): array
|
||||
{
|
||||
return [
|
||||
'start_date' => $formData['start_date']->normalize(),
|
||||
'end_date' => $formData['end_date']->normalize(),
|
||||
];
|
||||
}
|
||||
|
||||
public function denormalizeFormData(array $formData, int $fromVersion): array
|
||||
{
|
||||
$default = $this->getFormDefaultData();
|
||||
|
||||
return [
|
||||
'start_date' => array_key_exists('start_date', $formData) ? RollingDate::fromNormalized($formData['start_date']) : $default['start_date'],
|
||||
'end_date' => array_key_exists('end_date', $formData) ? RollingDate::fromNormalized($formData['end_date']) : $default['end_date'],
|
||||
];
|
||||
}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [
|
||||
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
|
||||
'end_date' => new RollingDate(RollingDate::T_TODAY),
|
||||
];
|
||||
}
|
||||
|
||||
public function transformData(?array $before): array
|
||||
{
|
||||
$default = $this->getFormDefaultData();
|
||||
|
||||
if (null === $before) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return [
|
||||
'start_date' => $before['start_date'] ?? $before['date_calc'] ?? $default['start_date'],
|
||||
'end_date' => $before['end_date'] ?? $before['date_calc'] ?? $default['end_date'],
|
||||
];
|
||||
}
|
||||
|
||||
public function getLabels($key, array $values, $data): callable
|
||||
{
|
||||
return function ($value): string {
|
||||
if ('_header' === $value) {
|
||||
return 'person.export.period.aggregator.by_referrer_main_center.column_header';
|
||||
}
|
||||
|
||||
if (null === $value || '' === $value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return (string) $this->centerRepository->find((int) $value)?->getName();
|
||||
};
|
||||
}
|
||||
|
||||
public function getQueryKeys($data): array
|
||||
{
|
||||
return [self::P.'_select'];
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'person.export.period.aggregator.by_referrer_main_center.title';
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ final readonly class CenterAggregator implements AggregatorInterface
|
||||
{
|
||||
return function (int|string|null $value) {
|
||||
if (null === $value || '' === $value) {
|
||||
return $this->translator->trans('person.export.aggregator.by_center.no_center');
|
||||
return $this->translator->trans('person.export.period.aggregator.by_center.no_center');
|
||||
}
|
||||
|
||||
if ('_header' === $value) {
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
<?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\PersonBundle\Export\Filter\AccompanyingCourseFilters;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Export\ExportGenerationContext;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||
use Chill\MainBundle\Repository\CenterRepositoryInterface;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Chill\PersonBundle\Export\Declarations;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
/**
|
||||
* Filter accompanying periods by the main center of their referrer (at a given date).
|
||||
*/
|
||||
final readonly class ReferrerMainCenterFilter implements FilterInterface
|
||||
{
|
||||
private const UH = 'acp_referrer_main_center_filter_uh';
|
||||
private const DATE_PARAM = 'acp_referrer_main_center_filter_date';
|
||||
|
||||
public function __construct(
|
||||
private RollingDateConverterInterface $rollingDateConverter,
|
||||
private CenterRepositoryInterface $centerRepository,
|
||||
) {}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void
|
||||
{
|
||||
$qb
|
||||
->join('acp.userHistories', self::UH)
|
||||
->join(self::UH.'.user', self::UH.'_user')
|
||||
->andWhere(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->lte(self::UH.'.startDate', ':'.self::DATE_PARAM),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isNull(self::UH.'.endDate'),
|
||||
$qb->expr()->gt(self::UH.'.endDate', ':'.self::DATE_PARAM)
|
||||
)
|
||||
)
|
||||
)
|
||||
->andWhere('IDENTITY('.self::UH.'_user.mainCenter) IN (:acp_referrer_main_center_filter_centers)')
|
||||
->setParameter(self::DATE_PARAM, $this->rollingDateConverter->convert($data['date_calc']))
|
||||
->setParameter('acp_referrer_main_center_filter_centers', array_map(
|
||||
static fn (Center $c): int => $c->getId(),
|
||||
$data['centers']
|
||||
));
|
||||
}
|
||||
|
||||
public function applyOn(): string
|
||||
{
|
||||
return Declarations::ACP_TYPE;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder): void
|
||||
{
|
||||
$builder
|
||||
->add('centers', EntityType::class, [
|
||||
'class' => Center::class,
|
||||
'multiple' => true,
|
||||
'expanded' => false,
|
||||
'choice_label' => static fn (Center $c) => $c->getName(),
|
||||
'required' => true,
|
||||
'label' => 'common.centers',
|
||||
])
|
||||
->add('date_calc', PickRollingDateType::class, [
|
||||
'label' => 'person.export.period.filter.by_referrer_main_center.referrer_since',
|
||||
'required' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getNormalizationVersion(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function normalizeFormData(array $formData): array
|
||||
{
|
||||
return [
|
||||
'centers' => array_values(array_map(static fn (Center $c) => $c->getId(), $formData['centers'])),
|
||||
'date_calc' => $formData['date_calc']->normalize(),
|
||||
];
|
||||
}
|
||||
|
||||
public function denormalizeFormData(array $formData, int $fromVersion): array
|
||||
{
|
||||
return [
|
||||
'centers' => array_values(array_filter(array_map(
|
||||
fn (int $id) => $this->centerRepository->find($id),
|
||||
$formData['centers'] ?? []
|
||||
))),
|
||||
'date_calc' => RollingDate::fromNormalized($formData['date_calc']),
|
||||
];
|
||||
}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [
|
||||
'centers' => [],
|
||||
'date_calc' => new RollingDate(RollingDate::T_TODAY),
|
||||
];
|
||||
}
|
||||
|
||||
public function describeAction($data, ExportGenerationContext $context): array|string
|
||||
{
|
||||
$names = array_map(static fn (Center $c) => $c->getName(), $data['centers']);
|
||||
|
||||
return [
|
||||
'person.export.period.filter.by_referrer_main_center.action_%centers%',
|
||||
['%centers%' => implode(', ', $names)],
|
||||
];
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'person.export.period.filter.by_referrer_main_center.title';
|
||||
}
|
||||
}
|
||||
@@ -44,10 +44,12 @@ final readonly class FilterListAccompanyingPeriodHelper implements FilterListAcc
|
||||
};
|
||||
|
||||
// add filtering on confidential accompanying period. The confidential is applyed on the current status of
|
||||
// the accompanying period (we do not use the 'calc_date' here)
|
||||
//
|
||||
// IMPORTANT: we must NOT bypass selected centers just because the current user is the referrer.
|
||||
$aclConditionsOrX = $qb->expr()->orX();
|
||||
// the accompanying period (we do not use the 'calc_date' here
|
||||
$aclConditionsOrX = $qb->expr()->orX(
|
||||
// either the current user is the refferer for the course
|
||||
'acp.user = :list_acp_current_user',
|
||||
);
|
||||
$qb->setParameter('list_acp_current_user', $user);
|
||||
|
||||
$i = 0;
|
||||
foreach ($centers as $center) {
|
||||
@@ -91,12 +93,6 @@ final readonly class FilterListAccompanyingPeriodHelper implements FilterListAcc
|
||||
++$i;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
$qb->andWhere($aclConditionsOrX);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,23 +20,19 @@ use Doctrine\ORM\NoResultException;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
|
||||
final readonly class GoalRepository implements ObjectRepository
|
||||
{
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private ClockInterface $clock,
|
||||
private RequestStack $requestStack,
|
||||
) {
|
||||
public function __construct(private EntityManagerInterface $entityManager, private RequestStack $requestStack)
|
||||
{
|
||||
$this->repository = $entityManager->getRepository(Goal::class);
|
||||
}
|
||||
|
||||
public function countBySocialActionWithDescendants(SocialAction $action, bool $onlyActive = false): int
|
||||
public function countBySocialActionWithDescendants(SocialAction $action): int
|
||||
{
|
||||
$qb = $this->buildQueryBySocialActionWithDescendants($action, $onlyActive);
|
||||
$qb = $this->buildQueryBySocialActionWithDescendants($action);
|
||||
$qb->select('COUNT(g)');
|
||||
|
||||
return $qb
|
||||
@@ -71,9 +67,9 @@ final readonly class GoalRepository implements ObjectRepository
|
||||
/**
|
||||
* @return Goal[]
|
||||
*/
|
||||
public function findBySocialActionWithDescendants(SocialAction $action, array $orderBy = [], ?int $limit = null, ?int $offset = null, bool $onlyActive = false): array
|
||||
public function findBySocialActionWithDescendants(SocialAction $action, array $orderBy = [], ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
$qb = $this->buildQueryBySocialActionWithDescendants($action, $onlyActive);
|
||||
$qb = $this->buildQueryBySocialActionWithDescendants($action);
|
||||
$qb->select('g');
|
||||
|
||||
$qb->andWhere(
|
||||
@@ -204,7 +200,7 @@ final readonly class GoalRepository implements ObjectRepository
|
||||
}
|
||||
}
|
||||
|
||||
private function buildQueryBySocialActionWithDescendants(SocialAction $action, bool $onlyActive): QueryBuilder
|
||||
private function buildQueryBySocialActionWithDescendants(SocialAction $action): QueryBuilder
|
||||
{
|
||||
$actions = $action->getDescendantsWithThis();
|
||||
|
||||
@@ -219,11 +215,6 @@ final readonly class GoalRepository implements ObjectRepository
|
||||
}
|
||||
$qb->where($orx);
|
||||
|
||||
if ($onlyActive) {
|
||||
$qb->andWhere('g.desactivationDate > :now OR g.desactivationDate IS NULL')
|
||||
->setParameter('now', $this->clock->now());
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,23 +21,19 @@ use Doctrine\ORM\NoResultException;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
|
||||
final readonly class ResultRepository implements ObjectRepository
|
||||
{
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private ClockInterface $clock,
|
||||
private RequestStack $requestStack,
|
||||
) {
|
||||
public function __construct(private EntityManagerInterface $entityManager, private RequestStack $requestStack)
|
||||
{
|
||||
$this->repository = $entityManager->getRepository(Result::class);
|
||||
}
|
||||
|
||||
public function countByGoal(Goal $goal, bool $onlyActive = false): int
|
||||
public function countByGoal(Goal $goal): int
|
||||
{
|
||||
$qb = $this->buildQueryByGoal($goal, $onlyActive);
|
||||
$qb = $this->buildQueryByGoal($goal);
|
||||
$qb->select('COUNT(r)');
|
||||
|
||||
return $qb
|
||||
@@ -45,9 +41,9 @@ final readonly class ResultRepository implements ObjectRepository
|
||||
->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function countBySocialActionWithDescendants(SocialAction $action, bool $onlyActive = false): int
|
||||
public function countBySocialActionWithDescendants(SocialAction $action): int
|
||||
{
|
||||
$qb = $this->buildQueryBySocialActionWithDescendants($action, $onlyActive);
|
||||
$qb = $this->buildQueryBySocialActionWithDescendants($action);
|
||||
$qb->select('COUNT(r)');
|
||||
|
||||
return $qb
|
||||
@@ -82,9 +78,9 @@ final readonly class ResultRepository implements ObjectRepository
|
||||
/**
|
||||
* @return array<Result>
|
||||
*/
|
||||
public function findByGoal(Goal $goal, ?array $orderBy = null, ?int $limit = null, ?int $offset = null, bool $onlyActive = false): array
|
||||
public function findByGoal(Goal $goal, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
$qb = $this->buildQueryByGoal($goal, $onlyActive);
|
||||
$qb = $this->buildQueryByGoal($goal);
|
||||
|
||||
if (null !== $orderBy) {
|
||||
foreach ($orderBy as $sort => $order) {
|
||||
@@ -103,9 +99,9 @@ final readonly class ResultRepository implements ObjectRepository
|
||||
/**
|
||||
* @return Result[]
|
||||
*/
|
||||
public function findBySocialActionWithDescendants(SocialAction $action, array $orderBy = [], ?int $limit = null, ?int $offset = null, bool $onlyActive = false): array
|
||||
public function findBySocialActionWithDescendants(SocialAction $action, array $orderBy = [], ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
$qb = $this->buildQueryBySocialActionWithDescendants($action, $onlyActive);
|
||||
$qb = $this->buildQueryBySocialActionWithDescendants($action);
|
||||
$qb->select('r');
|
||||
|
||||
foreach ($orderBy as $sort => $order) {
|
||||
@@ -226,22 +222,17 @@ final readonly class ResultRepository implements ObjectRepository
|
||||
}
|
||||
}
|
||||
|
||||
private function buildQueryByGoal(Goal $goal, bool $onlyActive): QueryBuilder
|
||||
private function buildQueryByGoal(Goal $goal): QueryBuilder
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('r');
|
||||
|
||||
$qb->where(':goal MEMBER OF r.goals')
|
||||
->setParameter('goal', $goal);
|
||||
|
||||
if ($onlyActive) {
|
||||
$qb->andWhere('r.desactivationDate > :now OR r.desactivationDate IS NULL')
|
||||
->setParameter('now', $this->clock->now());
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
private function buildQueryBySocialActionWithDescendants(SocialAction $action, bool $onlyActive = false): QueryBuilder
|
||||
private function buildQueryBySocialActionWithDescendants(SocialAction $action): QueryBuilder
|
||||
{
|
||||
$actions = $action->getDescendantsWithThis();
|
||||
|
||||
@@ -256,11 +247,6 @@ final readonly class ResultRepository implements ObjectRepository
|
||||
}
|
||||
$qb->where($orx);
|
||||
|
||||
if ($onlyActive) {
|
||||
$qb->andWhere('r.desactivationDate > :now OR r.desactivationDate IS NULL')
|
||||
->setParameter('now', $this->clock->now());
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
:editor="ClassicEditor"
|
||||
:config="classicEditorConfig"
|
||||
:placeholder="trans(EVALUATION_COMMENT_PLACEHOLDER)"
|
||||
:model-value="comment"
|
||||
@update:model-value="$emit('update:comment', $event)"
|
||||
:value="comment"
|
||||
@input="$emit('update:comment', $event)"
|
||||
tag-name="textarea"
|
||||
></ckeditor>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</td>
|
||||
<td>
|
||||
{% if entity.desactivationDate is not null %}
|
||||
{{ entity.desactivationDate|format_date('medium') }}
|
||||
{{ entity.desactivationDate|date('Y-m-d') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<td>{{ entity.title|localize_translatable_string }}</td>
|
||||
<td>
|
||||
{% if entity.desactivationDate is not null %}
|
||||
{{ entity.desactivationDate|format_date('medium') }}
|
||||
{{ entity.desactivationDate|date('Y-m-d') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -14,7 +14,6 @@ namespace Chill\PersonBundle\Security\Authorization\StoredObjectVoter;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
||||
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository;
|
||||
@@ -27,9 +26,8 @@ class AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter extends Abstract
|
||||
private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository,
|
||||
Security $security,
|
||||
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||
EntityWorkflowAttachmentRepository $attachmentRepository,
|
||||
) {
|
||||
parent::__construct($security, $attachmentRepository, $workflowDocumentService);
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
}
|
||||
|
||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||
|
||||
@@ -104,6 +104,10 @@ services:
|
||||
tags:
|
||||
- { name: chill.export_filter, alias: accompanyingcourse_referrer_filter_between_dates }
|
||||
|
||||
Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\ReferrerMainCenterFilter:
|
||||
tags:
|
||||
- { name: chill.export_filter, alias: accompanyingcourse_referrer_main_center_filter }
|
||||
|
||||
chill.person.export.filter_openbetweendates:
|
||||
class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\OpenBetweenDatesFilter
|
||||
tags:
|
||||
@@ -270,3 +274,7 @@ services:
|
||||
Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\PersonParticipatingAggregator:
|
||||
tags:
|
||||
- { name: chill.export_aggregator, alias: accompanyingcourse_person_part_aggregator }
|
||||
|
||||
Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ReferrerMainCenterAggregator:
|
||||
tags:
|
||||
- { name: chill.export_aggregator, alias: accompanyingcourse_referrer_main_center_aggregator }
|
||||
|
||||
@@ -105,9 +105,18 @@ Administrative status: Situation administrative
|
||||
person:
|
||||
# trans key according to new conventions
|
||||
export:
|
||||
aggregator:
|
||||
by_center:
|
||||
no_center: Sans territoire
|
||||
period:
|
||||
aggregator:
|
||||
by_center:
|
||||
no_center: Sans territoire
|
||||
by_referrer_main_center:
|
||||
title: Grouper par territoire du référent
|
||||
column_header: Territoire du référent
|
||||
filter:
|
||||
by_referrer_main_center:
|
||||
title: Filtrer par territoire du référent
|
||||
action_%centers%: 'Filtrer par territoire du référent : uniquement %centers%'
|
||||
referrer_since: Référent depuis le
|
||||
Identifiers: Identifiants
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user