mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2026-01-15 22:01:23 +00:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
4028c020ee
|
|||
| 0d4eef6a0c | |||
| b6152d5356 | |||
| 8b708f8c73 | |||
| 8d5b200107 | |||
| a9e9207d5a | |||
| 3915574ed4 | |||
| f3217d22ef | |||
|
06c5affbe7
|
|||
| bf461a1211 | |||
| 3f0ad51114 | |||
| a4de8eaab3 | |||
| 2feb137ac2 | |||
| 5ea74d118b | |||
| 8eb7a55ef5 | |||
| 281887355f | |||
| 47b285b584 | |||
| 7c9b4d02f6 | |||
| 3ff9bba4de | |||
| c0f9e953fb | |||
| a49ea2b6b9 | |||
| a30232d3ce | |||
| aae55e6f8c | |||
| c9513f2f6c | |||
| 11d7425883 | |||
| 08897e0981 | |||
| 98cbfed054 | |||
| 9af4d19744 | |||
| c1cf5a8bb2 |
9
.changes/v4.11.0.md
Normal file
9
.changes/v4.11.0.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## v4.11.0 - 2025-12-17
|
||||
### Feature
|
||||
* ([#478](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/478)) Add filtering to admin lists: social actions, social issues, goals, results, and evaluations
|
||||
### Fixed
|
||||
* ([#466](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/466)) Fix migration query after previous fix
|
||||
|
||||
* Fix translation key/value
|
||||
|
||||
Cannot start with % and should be wrapped in "".
|
||||
16
.changes/v4.12.0.md
Normal file
16
.changes/v4.12.0.md
Normal file
@@ -0,0 +1,16 @@
|
||||
## v4.12.0 - 2026-01-15
|
||||
### Feature
|
||||
* ([#473](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/473)) Display version of chill bundles in application footer
|
||||
* Increase the delay before removing stale workflow from 90 days to 180 days.
|
||||
### Fixed
|
||||
* ([#480](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/480)) Fix the condition to display concerned persons in calendar list items.
|
||||
* ([#481](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/481)) Fix ordering of social actions: actions with a closing date in the future should be considered as 'still open'.
|
||||
* ([#477](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/477)) Fix export group by center for persons without a center in CenterAggregator.php
|
||||
* Fix the calculation of budget balance to only take into account resources and charges that are still actual
|
||||
* ([#489](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/489)) Fix desactivation date for Goals and results
|
||||
* ([#490](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/490)) Prevent sending a notification when the user signs the document himself
|
||||
* ([#491](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/491)) Fix: acc periods of which user is the referrer should not be included if when the list is filtered by center and none of the participations are part of the center
|
||||
* ([#492](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/492)) fix CommentInput: replace deprecated value binding with model-value
|
||||
* ([#493](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/493)) fix issue with stored object permissions associated with workflows (as attachment, or through a related entity)
|
||||
|
||||
BC: the constructor's signature of `\Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter` has changed.
|
||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -6,6 +6,31 @@ 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
|
||||
### Fixed
|
||||
* ([#466](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/466)) Fix migration query after previous fix
|
||||
|
||||
* Fix translation key/value
|
||||
|
||||
Cannot start with % and should be wrapped in "".
|
||||
|
||||
## v4.10.1 - 2025-12-11
|
||||
### Fixed
|
||||
* Fix missing translation variable in NewLocation component
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"ext-openssl": "*",
|
||||
"ext-redis": "*",
|
||||
"ext-zlib": "*",
|
||||
"composer-runtime-api": "*",
|
||||
"champs-libres/wopi-bundle": "dev-symfony-v5@dev",
|
||||
"champs-libres/wopi-lib": "dev-master@dev",
|
||||
"doctrine/data-fixtures": "^1.8",
|
||||
@@ -82,7 +83,7 @@
|
||||
"symfony/templating": "^5.4",
|
||||
"symfony/translation": "^5.4",
|
||||
"symfony/twig-bundle": "^5.4",
|
||||
"symfony/ux-translator": "^2.22",
|
||||
"symfony/ux-translator": "2.31.0",
|
||||
"symfony/validator": "^5.4",
|
||||
"symfony/webpack-encore-bundle": "^1.11",
|
||||
"symfony/workflow": "^5.4",
|
||||
@@ -97,7 +98,7 @@
|
||||
"require-dev": {
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.3",
|
||||
"fakerphp/faker": "^1.13",
|
||||
"friendsofphp/php-cs-fixer": "3.65.0",
|
||||
"friendsofphp/php-cs-fixer": "3.92.5",
|
||||
"jangregor/phpstan-prophecy": "^1.0",
|
||||
"nelmio/alice": "^3.8",
|
||||
"nikic/php-parser": "^4.15",
|
||||
|
||||
@@ -16,7 +16,8 @@ use Chill\ActivityBundle\Repository\ActivityRepository;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelperInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
|
||||
@@ -24,9 +25,10 @@ class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
|
||||
public function __construct(
|
||||
private readonly ActivityRepository $repository,
|
||||
Security $security,
|
||||
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||
WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService,
|
||||
EntityWorkflowAttachmentRepository $attachmentRepository,
|
||||
) {
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
parent::__construct($security, $attachmentRepository, $workflowDocumentService);
|
||||
}
|
||||
|
||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||
|
||||
@@ -39,7 +39,7 @@ final class Version20251118124241 extends AbstractMigration
|
||||
$this->addSql("COMMENT ON COLUMN activity_user.by_migration IS 'For backup purpose - can be safely deleted after a while. See migration \\Chill\\Migrations\\Activity\\Version20251118124241'");
|
||||
|
||||
$this->addSql('INSERT INTO activity_user (activity_id, user_id, by_migration)
|
||||
SELECT id, user_id, true FROM activity
|
||||
SELECT id, user_id, true FROM activity WHERE user_id is not null
|
||||
ON CONFLICT DO NOTHING');
|
||||
}
|
||||
|
||||
|
||||
@@ -72,14 +72,20 @@
|
||||
|
||||
{% macro table_results(actualCharges, actualResources, results) %}
|
||||
|
||||
{% set now = date() %}
|
||||
|
||||
{% set totalCharges = 0 %}
|
||||
{% for c in actualCharges %}
|
||||
{% set totalCharges = totalCharges + c.amount %}
|
||||
{% if c.startDate <= now and (c.endDate is null or c.endDate >= now) %}
|
||||
{% set totalCharges = totalCharges + c.amount %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% set totalResources = 0 %}
|
||||
{% for r in actualResources %}
|
||||
{% set totalResources = totalResources + r.amount %}
|
||||
{% if r.startDate <= now and (r.endDate is null or r.endDate >= now) %}
|
||||
{% set totalResources = totalResources + r.amount %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% set result = (totalResources - totalCharges) %}
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
</div>
|
||||
|
||||
{% if calendar.comment.comment is not empty
|
||||
or calendar.users|length > 0
|
||||
or calendar.persons|length > 0
|
||||
or calendar.thirdParties|length > 0
|
||||
or calendar.users|length > 0 %}
|
||||
<div class="item-row details separator">
|
||||
|
||||
@@ -15,7 +15,10 @@ use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelperInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
@@ -34,7 +37,8 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
|
||||
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly ?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
|
||||
private readonly EntityWorkflowAttachmentRepository $entityWorkflowAttachmentRepository,
|
||||
private readonly WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService,
|
||||
) {}
|
||||
|
||||
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool
|
||||
@@ -46,16 +50,6 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
|
||||
|
||||
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
|
||||
{
|
||||
// we first try to get the permission from the workflow, as attachement (this is the less intensive query)
|
||||
$workflowPermissionAsAttachment = match ($attribute) {
|
||||
StoredObjectRoleEnum::SEE => $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($subject),
|
||||
StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($subject),
|
||||
};
|
||||
|
||||
if (WorkflowRelatedEntityPermissionHelper::FORCE_DENIED === $workflowPermissionAsAttachment) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Retrieve the related entity
|
||||
$entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
|
||||
|
||||
@@ -65,7 +59,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
|
||||
$regularPermission = $this->security->isGranted($voterAttribute, $entity);
|
||||
|
||||
if (!$this->canBeAssociatedWithWorkflow()) {
|
||||
return $regularPermission;
|
||||
return $this->voteOnStoredObjectAsAttachementOfAWorkflow($attribute, $regularPermission, $subject);
|
||||
}
|
||||
|
||||
$workflowPermission = match ($attribute) {
|
||||
@@ -74,9 +68,41 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
|
||||
};
|
||||
|
||||
return match ($workflowPermission) {
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT => true,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED => false,
|
||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN => WorkflowRelatedEntityPermissionHelper::FORCE_GRANT === $workflowPermissionAsAttachment || $regularPermission,
|
||||
WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT => true,
|
||||
WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED => false,
|
||||
WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN => $this->voteOnStoredObjectAsAttachementOfAWorkflow($attribute, $regularPermission, $subject),
|
||||
};
|
||||
}
|
||||
|
||||
private function voteOnStoredObjectAsAttachementOfAWorkflow(StoredObjectRoleEnum $attribute, bool $regularPermission, StoredObject $storedObject): bool
|
||||
{
|
||||
$attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($storedObject);
|
||||
|
||||
// we get all the entity workflows where the stored object is attached
|
||||
$entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments);
|
||||
|
||||
// we compute all the permission for each entity workflow
|
||||
$permissions = array_map(fn (EntityWorkflow $entityWorkflow): string => match ($attribute) {
|
||||
StoredObjectRoleEnum::SEE => $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($entityWorkflow),
|
||||
StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($entityWorkflow),
|
||||
}, $entityWorkflows);
|
||||
|
||||
// now, we reduce the permissions: abstain are ignored. Between DENIED and and GRANT, DENIED takes precedence
|
||||
$computedPermission = WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN;
|
||||
foreach ($permissions as $permission) {
|
||||
if (WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED === $permission) {
|
||||
return false;
|
||||
}
|
||||
if (WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT === $permission) {
|
||||
$computedPermission = WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT;
|
||||
}
|
||||
}
|
||||
|
||||
if (WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN === $computedPermission) {
|
||||
return $regularPermission;
|
||||
}
|
||||
|
||||
// this is the case where WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT is returned
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
@@ -25,8 +26,9 @@ final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredOb
|
||||
private readonly AccompanyingCourseDocumentRepository $repository,
|
||||
Security $security,
|
||||
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||
EntityWorkflowAttachmentRepository $attachmentRepository,
|
||||
) {
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
parent::__construct($security, $attachmentRepository, $workflowDocumentService);
|
||||
}
|
||||
|
||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||
|
||||
@@ -16,6 +16,7 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
|
||||
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
@@ -25,8 +26,9 @@ class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter
|
||||
private readonly PersonDocumentRepository $repository,
|
||||
Security $security,
|
||||
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||
EntityWorkflowAttachmentRepository $attachmentRepository,
|
||||
) {
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
parent::__construct($security, $attachmentRepository, $workflowDocumentService);
|
||||
}
|
||||
|
||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||
|
||||
@@ -16,8 +16,11 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelperInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
@@ -31,21 +34,31 @@ class AbstractStoredObjectVoterTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
/**
|
||||
* @param array<int, EntityWorkflowAttachment> $attachments
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function buildStoredObjectVoter(
|
||||
bool $canBeAssociatedWithWorkflow,
|
||||
AssociatedEntityToStoredObjectInterface $repository,
|
||||
Security $security,
|
||||
?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
|
||||
?WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService = null,
|
||||
array $attachments = [],
|
||||
): AbstractStoredObjectVoter {
|
||||
$attachmentsRepository = $this->prophesize(EntityWorkflowAttachmentRepository::class);
|
||||
$attachmentsRepository->findByStoredObject(Argument::type(StoredObject::class))->willReturn($attachments);
|
||||
|
||||
// Anonymous class extending the abstract class
|
||||
return new class ($canBeAssociatedWithWorkflow, $repository, $security, $workflowDocumentService) extends AbstractStoredObjectVoter {
|
||||
return new class ($canBeAssociatedWithWorkflow, $repository, $security, $attachmentsRepository->reveal(), $workflowDocumentService) extends AbstractStoredObjectVoter {
|
||||
public function __construct(
|
||||
private readonly bool $canBeAssociatedWithWorkflow,
|
||||
private readonly AssociatedEntityToStoredObjectInterface $repository,
|
||||
Security $security,
|
||||
?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
|
||||
EntityWorkflowAttachmentRepository $attachmentRepository,
|
||||
WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService,
|
||||
) {
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
parent::__construct($security, $attachmentRepository, $workflowDocumentService);
|
||||
}
|
||||
|
||||
protected function attributeToRole($attribute): string
|
||||
@@ -72,28 +85,29 @@ class AbstractStoredObjectVoterTest extends TestCase
|
||||
|
||||
public function testSupportsOnAttribute(): void
|
||||
{
|
||||
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(new \stdClass()), $this->prophesize(Security::class)->reveal(), null);
|
||||
$entityWorkflowService = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class);
|
||||
|
||||
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(new \stdClass()), $this->prophesize(Security::class)->reveal(), $entityWorkflowService->reveal());
|
||||
|
||||
self::assertTrue($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject()));
|
||||
|
||||
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(new User()), $this->prophesize(Security::class)->reveal(), null);
|
||||
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(new User()), $this->prophesize(Security::class)->reveal(), $entityWorkflowService->reveal());
|
||||
|
||||
self::assertFalse($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject()));
|
||||
|
||||
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(null), $this->prophesize(Security::class)->reveal(), null);
|
||||
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(null), $this->prophesize(Security::class)->reveal(), $entityWorkflowService->reveal());
|
||||
|
||||
self::assertFalse($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderVoteOnAttributeWithStoredObjectPermission
|
||||
* @dataProvider dataProviderVoteOnAttributeWithWorkflow
|
||||
*/
|
||||
public function testVoteOnAttributeWithStoredObjectPermission(
|
||||
public function testVoteOnAttributeWithWorkflow(
|
||||
StoredObjectRoleEnum $attribute,
|
||||
bool $expected,
|
||||
bool $isGrantedRegularPermission,
|
||||
string $isGrantedWorkflowPermission,
|
||||
string $isGrantedStoredObjectAttachment,
|
||||
): void {
|
||||
$storedObject = new StoredObject();
|
||||
$repository = new DummyRepository($related = new \stdClass());
|
||||
@@ -102,31 +116,28 @@ class AbstractStoredObjectVoterTest extends TestCase
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
|
||||
|
||||
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);
|
||||
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class);
|
||||
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
|
||||
|
||||
$attachementRepository = $this->prophesize(EntityWorkflowAttachmentRepository::class);
|
||||
$attachementRepository->findByStoredObject($storedObject)->willReturn([]);
|
||||
|
||||
if (StoredObjectRoleEnum::SEE === $attribute) {
|
||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)
|
||||
->shouldBeCalled()
|
||||
->willReturn($isGrantedStoredObjectAttachment);
|
||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
|
||||
->willReturn($isGrantedWorkflowPermission);
|
||||
} elseif (StoredObjectRoleEnum::EDIT === $attribute) {
|
||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)
|
||||
->shouldBeCalled()
|
||||
->willReturn($isGrantedStoredObjectAttachment);
|
||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related)
|
||||
->willReturn($isGrantedWorkflowPermission);
|
||||
} else {
|
||||
throw new \LogicException('Invalid attribute for StoredObjectVoter');
|
||||
}
|
||||
|
||||
$storedObjectVoter = new class ($repository, $workflowRelatedEntityPermissionHelper->reveal(), $security->reveal()) extends AbstractStoredObjectVoter {
|
||||
public function __construct(private $repository, $helper, $security)
|
||||
$storedObjectVoter = new class ($repository, $workflowRelatedEntityPermissionHelper->reveal(), $security->reveal(), $attachementRepository->reveal()) extends AbstractStoredObjectVoter {
|
||||
public function __construct(private $repository, $helper, $security, EntityWorkflowAttachmentRepository $attachmentRepository)
|
||||
{
|
||||
parent::__construct($security, $helper);
|
||||
parent::__construct($security, $attachmentRepository, $helper);
|
||||
}
|
||||
|
||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||
@@ -155,96 +166,64 @@ class AbstractStoredObjectVoterTest extends TestCase
|
||||
self::assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
public static function dataProviderVoteOnAttributeWithStoredObjectPermission(): iterable
|
||||
public static function dataProviderVoteOnAttributeWithWorkflow(): iterable
|
||||
{
|
||||
foreach (['read' => StoredObjectRoleEnum::SEE, 'write' => StoredObjectRoleEnum::EDIT] as $action => $attribute) {
|
||||
yield 'Not related to any workflow nor attachment ('.$action.')' => [
|
||||
$attribute,
|
||||
true,
|
||||
true,
|
||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
||||
WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
|
||||
];
|
||||
|
||||
yield 'Not related to any workflow nor attachment (refuse) ('.$action.')' => [
|
||||
$attribute,
|
||||
false,
|
||||
false,
|
||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
||||
WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
|
||||
];
|
||||
|
||||
yield 'Is granted by a workflow takes precedence (workflow) ('.$action.')' => [
|
||||
$attribute,
|
||||
false,
|
||||
true,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
||||
WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
|
||||
];
|
||||
|
||||
yield 'Is granted by a workflow takes precedence (stored object) ('.$action.')' => [
|
||||
$attribute,
|
||||
false,
|
||||
true,
|
||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
||||
];
|
||||
|
||||
yield 'Is granted by a workflow takes precedence (workflow) although grant ('.$action.')' => [
|
||||
$attribute,
|
||||
false,
|
||||
true,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
||||
WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
|
||||
];
|
||||
|
||||
yield 'Is granted by a workflow takes precedence (stored object) although grant ('.$action.')' => [
|
||||
$attribute,
|
||||
false,
|
||||
true,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
||||
true,
|
||||
WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
|
||||
];
|
||||
|
||||
yield 'Is granted by a workflow takes precedence (initially refused) (workflow) although grant ('.$action.')' => [
|
||||
$attribute,
|
||||
false,
|
||||
false,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
||||
WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
|
||||
];
|
||||
|
||||
yield 'Is granted by a workflow takes precedence (initially refused) (stored object) although grant ('.$action.')' => [
|
||||
$attribute,
|
||||
false,
|
||||
false,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
||||
];
|
||||
|
||||
yield 'Force grant inverse the regular permission (workflow) ('.$action.')' => [
|
||||
$attribute,
|
||||
true,
|
||||
false,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
||||
WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
|
||||
];
|
||||
|
||||
yield 'Force grant inverse the regular permission (so) ('.$action.')' => [
|
||||
$attribute,
|
||||
true,
|
||||
false,
|
||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
||||
];
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderVoteOnAttributeWithoutStoredObjectPermission
|
||||
* @dataProvider dataProviderVoteOnAttribute
|
||||
*/
|
||||
public function testVoteOnAttributeWithoutStoredObjectPermission(
|
||||
public function testVoteOnAttribute(
|
||||
StoredObjectRoleEnum $attribute,
|
||||
bool $expected,
|
||||
bool $canBeAssociatedWithWorkflow,
|
||||
@@ -260,10 +239,7 @@ class AbstractStoredObjectVoterTest extends TestCase
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
|
||||
|
||||
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);
|
||||
|
||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN);
|
||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN);
|
||||
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class);
|
||||
|
||||
if (null !== $isGrantedWorkflowPermissionRead) {
|
||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
|
||||
@@ -283,27 +259,155 @@ class AbstractStoredObjectVoterTest extends TestCase
|
||||
self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message);
|
||||
}
|
||||
|
||||
public static function dataProviderVoteOnAttributeWithoutStoredObjectPermission(): iterable
|
||||
public static function dataProviderVoteOnAttribute(): iterable
|
||||
{
|
||||
// not associated on a workflow
|
||||
yield [StoredObjectRoleEnum::SEE, true, false, true, null, null, 'not associated on a workflow, granted by regular access, must not rely on helper'];
|
||||
yield [StoredObjectRoleEnum::SEE, false, false, false, null, null, 'not associated on a workflow, denied by regular access, must not rely on helper'];
|
||||
|
||||
// associated on a workflow, read operation
|
||||
yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be granted'];
|
||||
yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelper::ABSTAIN, null, 'associated on a workflow, read by regular, abstain by workflow, ask for read, should be granted'];
|
||||
yield [StoredObjectRoleEnum::SEE, false, true, true, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be denied'];
|
||||
yield [StoredObjectRoleEnum::SEE, true, true, false, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be granted'];
|
||||
yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelper::ABSTAIN, null, 'associated on a workflow, denied read by regular, abstain by workflow, ask for read, should be granted'];
|
||||
yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be denied'];
|
||||
yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be granted'];
|
||||
yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, null, 'associated on a workflow, read by regular, abstain by workflow, ask for read, should be granted'];
|
||||
yield [StoredObjectRoleEnum::SEE, false, true, true, WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be denied'];
|
||||
yield [StoredObjectRoleEnum::SEE, true, true, false, WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be granted'];
|
||||
yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, null, 'associated on a workflow, denied read by regular, abstain by workflow, ask for read, should be granted'];
|
||||
yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be denied'];
|
||||
|
||||
// association on a workflow, write operation
|
||||
yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be granted'];
|
||||
yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelper::ABSTAIN, 'associated on a workflow, write by regular, abstain by workflow, ask for write, should be granted'];
|
||||
yield [StoredObjectRoleEnum::EDIT, false, true, true, null, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be denied'];
|
||||
yield [StoredObjectRoleEnum::EDIT, true, true, false, null, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be granted'];
|
||||
yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelper::ABSTAIN, 'associated on a workflow, denied write by regular, abstain by workflow, ask for write, should be granted'];
|
||||
yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be denied'];
|
||||
yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be granted'];
|
||||
yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, 'associated on a workflow, write by regular, abstain by workflow, ask for write, should be granted'];
|
||||
yield [StoredObjectRoleEnum::EDIT, false, true, true, null, WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be denied'];
|
||||
yield [StoredObjectRoleEnum::EDIT, true, true, false, null, WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be granted'];
|
||||
yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, 'associated on a workflow, denied write by regular, abstain by workflow, ask for write, should be granted'];
|
||||
yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be denied'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderPrecedenceOfDirectAssociationOverWorkflowAttachments
|
||||
*/
|
||||
public function testPrecedenceOfDirectAssociationOverWorkflowAttachments(
|
||||
StoredObjectRoleEnum $attribute,
|
||||
bool $expected,
|
||||
bool $regularPermission,
|
||||
string $directWorkflowPermission,
|
||||
string $attachmentWorkflowPermission,
|
||||
string $message,
|
||||
): void {
|
||||
$storedObject = new StoredObject();
|
||||
$repository = new DummyRepository($related = new \stdClass());
|
||||
$token = new UsernamePasswordToken(new User(), 'dummy');
|
||||
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->isGranted('SOME_ROLE', $related)->willReturn($regularPermission);
|
||||
|
||||
$workflowHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class);
|
||||
|
||||
// Direct association permission
|
||||
if (StoredObjectRoleEnum::SEE === $attribute) {
|
||||
$workflowHelper->isAllowedByWorkflowForReadOperation($related)
|
||||
->willReturn($directWorkflowPermission);
|
||||
} else {
|
||||
$workflowHelper->isAllowedByWorkflowForWriteOperation($related)
|
||||
->willReturn($directWorkflowPermission);
|
||||
}
|
||||
|
||||
// Attachment permission
|
||||
$entityWorkflow = $this->prophesize(\Chill\MainBundle\Entity\Workflow\EntityWorkflow::class)->reveal();
|
||||
$attachment = $this->prophesize(EntityWorkflowAttachment::class);
|
||||
$attachment->getEntityWorkflow()->willReturn($entityWorkflow);
|
||||
|
||||
if (StoredObjectRoleEnum::SEE === $attribute) {
|
||||
$workflowHelper->isAllowedByWorkflowForReadOperation($entityWorkflow)
|
||||
->willReturn($attachmentWorkflowPermission);
|
||||
} else {
|
||||
$workflowHelper->isAllowedByWorkflowForWriteOperation($entityWorkflow)
|
||||
->willReturn($attachmentWorkflowPermission);
|
||||
}
|
||||
|
||||
$voter = $this->buildStoredObjectVoter(
|
||||
true,
|
||||
$repository,
|
||||
$security->reveal(),
|
||||
$workflowHelper->reveal(),
|
||||
[$attachment->reveal()]
|
||||
);
|
||||
|
||||
self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message);
|
||||
}
|
||||
|
||||
public static function dataProviderPrecedenceOfDirectAssociationOverWorkflowAttachments(): iterable
|
||||
{
|
||||
$cases = [
|
||||
[
|
||||
'expected' => true,
|
||||
'regular' => false,
|
||||
'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
|
||||
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
|
||||
'message' => 'Direct FORCE_GRANT should win over attachment FORCE_DENIED',
|
||||
],
|
||||
[
|
||||
'expected' => false,
|
||||
'regular' => true,
|
||||
'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
|
||||
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
|
||||
'message' => 'Direct FORCE_DENIED should win over attachment FORCE_GRANT',
|
||||
],
|
||||
[
|
||||
'expected' => true,
|
||||
'regular' => false,
|
||||
'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
|
||||
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
|
||||
'message' => 'Direct FORCE_GRANT should win over attachment ABSTAIN',
|
||||
],
|
||||
[
|
||||
'expected' => false,
|
||||
'regular' => true,
|
||||
'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
|
||||
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
|
||||
'message' => 'Direct FORCE_DENIED should win over attachment ABSTAIN',
|
||||
],
|
||||
[
|
||||
'expected' => true,
|
||||
'regular' => false,
|
||||
'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
|
||||
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
|
||||
'message' => 'Direct ABSTAIN should let attachment FORCE_GRANT win',
|
||||
],
|
||||
[
|
||||
'expected' => false,
|
||||
'regular' => true,
|
||||
'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
|
||||
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
|
||||
'message' => 'Direct ABSTAIN should let attachment FORCE_DENIED win',
|
||||
],
|
||||
[
|
||||
'expected' => true,
|
||||
'regular' => true,
|
||||
'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
|
||||
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
|
||||
'message' => 'Both ABSTAIN should let regular permission (true) win',
|
||||
],
|
||||
[
|
||||
'expected' => false,
|
||||
'regular' => false,
|
||||
'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
|
||||
'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
|
||||
'message' => 'Both ABSTAIN should let regular permission (false) win',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ([StoredObjectRoleEnum::SEE, StoredObjectRoleEnum::EDIT] as $attribute) {
|
||||
foreach ($cases as $case) {
|
||||
yield sprintf('%s - %s', $attribute->name, $case['message']) => [
|
||||
$attribute,
|
||||
$case['expected'],
|
||||
$case['regular'],
|
||||
$case['direct'],
|
||||
$case['attachment'],
|
||||
$case['message'],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Chill\EventBundle\Security\Authorization;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Chill\EventBundle\Entity\Event;
|
||||
use Chill\EventBundle\Repository\EventRepository;
|
||||
@@ -26,8 +27,9 @@ class EventStoredObjectVoter extends AbstractStoredObjectVoter
|
||||
private readonly EventRepository $repository,
|
||||
Security $security,
|
||||
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||
EntityWorkflowAttachmentRepository $attachmentRepository,
|
||||
) {
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
parent::__construct($security, $attachmentRepository, $workflowDocumentService);
|
||||
}
|
||||
|
||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||
|
||||
@@ -189,14 +189,14 @@ crud:
|
||||
title_edit: Rapport "belemmering" bewerken
|
||||
title_delete: Belemmering verwijderen
|
||||
button_delete: Verwijderen
|
||||
confirm_message_delete: %as_string% verwijderen?
|
||||
confirm_message_delete: "%as_string% verwijderen?"
|
||||
cscv:
|
||||
title_new: Nieuw CV voor %person%
|
||||
title_view: CV voor %person%
|
||||
title_edit: CV bewerken
|
||||
title_delete: CV verwijderen
|
||||
button_delete: Verwijderen
|
||||
confirm_message_delete: %as_string% verwijderen?
|
||||
confirm_message_delete: "%as_string% verwijderen?"
|
||||
no_date: Geen datum aangegeven
|
||||
no_end_date: einddatum onbekend
|
||||
no_start_date: startdatum onbekend
|
||||
@@ -206,7 +206,7 @@ crud:
|
||||
title_edit: Immersie bewerken
|
||||
title_delete: Immersie verwijderen
|
||||
button_delete: Verwijderen
|
||||
confirm_message_delete: %as_string% verwijderen?
|
||||
confirm_message_delete: "%as_string% verwijderen?"
|
||||
projet_prof:
|
||||
title_new: Nieuw professioneel project voor %person%
|
||||
title_view: Professioneel project voor %person%
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
<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>
|
||||
|
||||
45
src/Bundle/ChillMainBundle/Service/VersionProvider.php
Normal file
45
src/Bundle/ChillMainBundle/Service/VersionProvider.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Service;
|
||||
|
||||
use Composer\InstalledVersions;
|
||||
|
||||
readonly class VersionProvider
|
||||
{
|
||||
public function __construct(private string $packageName) {}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
try {
|
||||
$version = InstalledVersions::getPrettyVersion($this->packageName);
|
||||
|
||||
if (null === $version) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
return $version;
|
||||
} catch (\OutOfBoundsException) {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
public function getFormattedVersion(): string
|
||||
{
|
||||
$version = $this->getVersion();
|
||||
|
||||
if ('unknown' === $version) {
|
||||
return 'Version unavailable';
|
||||
}
|
||||
|
||||
return $version;
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ class CancelStaleWorkflowCronJob implements CronJobInterface
|
||||
{
|
||||
public const KEY = 'remove-stale-workflow';
|
||||
|
||||
public const KEEP_INTERVAL = 'P90D';
|
||||
public const KEEP_INTERVAL = 'P180D';
|
||||
|
||||
private const LAST_CANCELED_WORKFLOW = 'last-canceled-workflow-id';
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Templating;
|
||||
|
||||
use Chill\MainBundle\Service\VersionProvider;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class VersionRenderExtension extends AbstractExtension
|
||||
{
|
||||
public function __construct(
|
||||
private readonly VersionProvider $versionProvider,
|
||||
) {}
|
||||
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
new TwigFunction('get_chill_version', $this->getChillVersion(...)),
|
||||
];
|
||||
}
|
||||
|
||||
public function getChillVersion(): string
|
||||
{
|
||||
return $this->versionProvider->getFormattedVersion();
|
||||
}
|
||||
}
|
||||
@@ -42,14 +42,14 @@ class CancelStaleWorkflowHandlerTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testWorkflowWithOneStepOlderThan90DaysIsCanceled(): void
|
||||
public function testWorkflowWithOneStepOlderThan180DaysIsCanceled(): void
|
||||
{
|
||||
$clock = new MockClock('2024-01-01');
|
||||
$daysAgos = new \DateTimeImmutable('2023-09-01');
|
||||
$daysAgos = new \DateTimeImmutable('2023-06-01');
|
||||
|
||||
$workflow = new EntityWorkflow();
|
||||
$workflow->setWorkflowName('dummy_workflow');
|
||||
$workflow->setCreatedAt(new \DateTimeImmutable('2023-09-01'));
|
||||
$workflow->setCreatedAt(new \DateTimeImmutable('2023-06-01'));
|
||||
$workflow->setStep('step1', new WorkflowTransitionContextDTO($workflow), 'to_step1', $daysAgos, new User());
|
||||
|
||||
$em = $this->prophesize(EntityManagerInterface::class);
|
||||
@@ -94,7 +94,7 @@ class CancelStaleWorkflowHandlerTest extends TestCase
|
||||
|
||||
$workflow = new EntityWorkflow();
|
||||
$workflow->setWorkflowName('dummy_workflow');
|
||||
$workflow->setCreatedAt(new \DateTimeImmutable('2023-09-01'));
|
||||
$workflow->setCreatedAt(new \DateTimeImmutable('2023-06-01'));
|
||||
|
||||
$em = $this->prophesize(EntityManagerInterface::class);
|
||||
$em->flush()->shouldBeCalled();
|
||||
|
||||
@@ -15,7 +15,9 @@ use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\EventSubscriber\NotificationOnTransition;
|
||||
@@ -87,7 +89,7 @@ final class NotificationOnTransitionTest extends TestCase
|
||||
->willReturn([]);
|
||||
|
||||
$registry = $this->prophesize(Registry::class);
|
||||
$registry->get(Argument::type(EntityWorkflow::class), Argument::type('string'))
|
||||
$registry->get(Argument::type(EntityWorkflow::class), Argument::any())
|
||||
->willReturn($workflow);
|
||||
|
||||
$security = $this->prophesize(Security::class);
|
||||
@@ -111,4 +113,74 @@ final class NotificationOnTransitionTest extends TestCase
|
||||
|
||||
$notificationOnTransition->onCompletedSendNotification($event);
|
||||
}
|
||||
|
||||
public function testOnCompleteDoNotSendNotificationIfStepCreatedByPreviousSignature(): void
|
||||
{
|
||||
$dest = new User();
|
||||
$currentUser = new User();
|
||||
$workflowProphecy = $this->prophesize(WorkflowInterface::class);
|
||||
$workflow = $workflowProphecy->reveal();
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$entityWorkflow
|
||||
->setWorkflowName('workflow_name')
|
||||
->setRelatedEntityClass(\stdClass::class)
|
||||
->setRelatedEntityId(1);
|
||||
// force an id to entityWorkflow:
|
||||
$reflection = new \ReflectionClass($entityWorkflow);
|
||||
$id = $reflection->getProperty('id');
|
||||
$id->setValue($entityWorkflow, 1);
|
||||
|
||||
$previousStep = new EntityWorkflowStep();
|
||||
$previousStep->addSignature($signature = new EntityWorkflowStepSignature($previousStep, $dest));
|
||||
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED);
|
||||
|
||||
$currentStep = new EntityWorkflowStep();
|
||||
$currentStep->addDestUser($dest);
|
||||
$currentStep->setCurrentStep('to_state');
|
||||
|
||||
$entityWorkflow->addStep($previousStep);
|
||||
$entityWorkflow->addStep($currentStep);
|
||||
|
||||
$em = $this->prophesize(EntityManagerInterface::class);
|
||||
|
||||
// we check that NO notification has been persisted for $dest
|
||||
$em->persist(Argument::that(
|
||||
fn ($notificationCandidate) => $notificationCandidate instanceof Notification && $notificationCandidate->getAddressees()->contains($dest)
|
||||
))->shouldNotBeCalled();
|
||||
|
||||
$engine = $this->prophesize(\Twig\Environment::class);
|
||||
$engine->render(Argument::type('string'), Argument::type('array'))
|
||||
->willReturn('dummy text');
|
||||
|
||||
$extractor = $this->prophesize(MetadataExtractor::class);
|
||||
$extractor->buildArrayPresentationForPlace(Argument::type(EntityWorkflow::class), Argument::any())
|
||||
->willReturn([]);
|
||||
$extractor->buildArrayPresentationForWorkflow(Argument::any())
|
||||
->willReturn([]);
|
||||
|
||||
$registry = $this->prophesize(Registry::class);
|
||||
$registry->get(Argument::type(EntityWorkflow::class), Argument::any())
|
||||
->willReturn($workflow);
|
||||
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->getUser()->willReturn(null);
|
||||
|
||||
$entityWorkflowHandler = $this->prophesize(EntityWorkflowHandlerInterface::class);
|
||||
$entityWorkflowHandler->getEntityTitle($entityWorkflow)->willReturn('workflow title');
|
||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
||||
$entityWorkflowManager->getHandler($entityWorkflow)->willReturn($entityWorkflowHandler->reveal());
|
||||
|
||||
$notificationOnTransition = new NotificationOnTransition(
|
||||
$em->reveal(),
|
||||
$engine->reveal(),
|
||||
$extractor->reveal(),
|
||||
$security->reveal(),
|
||||
$registry->reveal(),
|
||||
$entityWorkflowManager->reveal(),
|
||||
);
|
||||
|
||||
$event = new Event($entityWorkflow, new Marking(), new Transition('dummy_transition', ['from_state'], ['to_state']), $workflow);
|
||||
|
||||
$notificationOnTransition->onCompletedSendNotification($event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Tests\Workflow\Helper;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
@@ -269,217 +266,7 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
|
||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
||||
$entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn($entityWorkflows);
|
||||
|
||||
$repository = $this->prophesize(EntityWorkflowAttachmentRepository::class);
|
||||
$repository->findByStoredObject(Argument::type(StoredObject::class))->willReturn([]);
|
||||
|
||||
return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $repository->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideDataAllowedByWorkflowReadOperationByAttachment
|
||||
*
|
||||
* @param list<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()));
|
||||
return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable()));
|
||||
}
|
||||
|
||||
private static function buildRegistry(): Registry
|
||||
|
||||
@@ -103,7 +103,10 @@ class NotificationOnTransition implements EventSubscriberInterface
|
||||
|
||||
foreach ($dests as $subscriber) {
|
||||
if (
|
||||
// prevent to send a notification to the one who created the step
|
||||
$this->security->getUser() === $subscriber
|
||||
// prevent to send a notification if the user applyied a signature on the previous step
|
||||
|| $this->isStepCreatedByPreviousSignature($entityWorkflow, $subscriber)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -131,4 +134,31 @@ class NotificationOnTransition implements EventSubscriberInterface
|
||||
$this->entityManager->persist($notification);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current step in the workflow was created by a previous signature of the specified user.
|
||||
*
|
||||
* This method retrieves the current step of the workflow and its preceding step. It iterates through
|
||||
* the signatures of the preceding step to verify if the provided user is the signer of any of those
|
||||
* signatures. Returns true if the user matches any signer; otherwise, returns false.
|
||||
*
|
||||
* @param EntityWorkflow $entityWorkflow the workflow entity containing the current step and its details
|
||||
* @param User $user the user to check against the signatures of the previous step in the workflow
|
||||
*
|
||||
* @return bool true if the specified user created the step via a previous signature, false otherwise
|
||||
*/
|
||||
private function isStepCreatedByPreviousSignature(EntityWorkflow $entityWorkflow, User $user): bool
|
||||
{
|
||||
$step = $entityWorkflow->getCurrentStepChained();
|
||||
$previous = $step->getPrevious();
|
||||
|
||||
|
||||
foreach ($previous->getSignatures() as $signature) {
|
||||
if ($signature->getSigner() === $user) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,12 +11,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Workflow\Helper;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
@@ -52,48 +49,28 @@ use Symfony\Component\Workflow\Registry;
|
||||
* the workflow denys write operations;
|
||||
* - if there is no case above and the user is involved in the workflow (is part of the current step, of a step before), the user is granted;
|
||||
*/
|
||||
class WorkflowRelatedEntityPermissionHelper
|
||||
final readonly class WorkflowRelatedEntityPermissionHelper implements WorkflowRelatedEntityPermissionHelperInterface
|
||||
{
|
||||
public const FORCE_GRANT = 'FORCE_GRANT';
|
||||
public const FORCE_DENIED = 'FORCE_DENIED';
|
||||
public const ABSTAIN = 'ABSTAIN';
|
||||
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||
private readonly EntityWorkflowAttachmentRepository $entityWorkflowAttachmentRepository,
|
||||
private readonly Registry $registry,
|
||||
private readonly ClockInterface $clock,
|
||||
private Security $security,
|
||||
private EntityWorkflowManager $entityWorkflowManager,
|
||||
private Registry $registry,
|
||||
private ClockInterface $clock,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param object $entity The entity may be an
|
||||
* @param object $entity The entity may be an object that is a related entity of a workflow, or an EntityWorkflow itself
|
||||
*
|
||||
* @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN'
|
||||
*/
|
||||
public function isAllowedByWorkflowForReadOperation(object $entity): string
|
||||
{
|
||||
if ($entity instanceof StoredObject) {
|
||||
$attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($entity);
|
||||
$entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments);
|
||||
$isAttached = true;
|
||||
} else {
|
||||
$entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
|
||||
$isAttached = false;
|
||||
}
|
||||
|
||||
if ([] === $entityWorkflows) {
|
||||
return self::ABSTAIN;
|
||||
}
|
||||
$entityWorkflows = $entity instanceof EntityWorkflow ? [$entity] : $this->entityWorkflowManager->findByRelatedEntity($entity);
|
||||
|
||||
if ($this->isUserInvolvedInAWorkflow($entityWorkflows)) {
|
||||
return self::FORCE_GRANT;
|
||||
}
|
||||
|
||||
if ($isAttached) {
|
||||
return self::ABSTAIN;
|
||||
}
|
||||
|
||||
// give a view permission if there is a Person signature pending, or in the 12 hours following
|
||||
// the signature last state
|
||||
foreach ($entityWorkflows as $workflow) {
|
||||
@@ -117,24 +94,20 @@ class WorkflowRelatedEntityPermissionHelper
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $entity the entity may be an object which is the related entity of a workflow
|
||||
*
|
||||
* @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN'
|
||||
*/
|
||||
public function isAllowedByWorkflowForWriteOperation(object $entity): string
|
||||
{
|
||||
if ($entity instanceof StoredObject) {
|
||||
$attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($entity);
|
||||
$entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments);
|
||||
$isAttached = true;
|
||||
} else {
|
||||
$entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
|
||||
$isAttached = false;
|
||||
}
|
||||
$entityWorkflows = $entity instanceof EntityWorkflow ? [$entity] : $this->entityWorkflowManager->findByRelatedEntity($entity);
|
||||
|
||||
if ([] === $entityWorkflows) {
|
||||
return self::ABSTAIN;
|
||||
}
|
||||
|
||||
// if a workflow is finalized positive, anyone is allowed to edit the document anymore
|
||||
|
||||
// if a workflow is finalized positive or isSentExternal, no one is allowed to edit the document anymore
|
||||
foreach ($entityWorkflows as $entityWorkflow) {
|
||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
$marking = $workflow->getMarkingStore()->getMarking($entityWorkflow);
|
||||
@@ -147,39 +120,64 @@ class WorkflowRelatedEntityPermissionHelper
|
||||
// the workflow is final, and final positive, or is sentExternal, so we stop here.
|
||||
return self::FORCE_DENIED;
|
||||
}
|
||||
if (
|
||||
// if not finalized positive
|
||||
$entityWorkflow->isFinal() && !($placeMetadata['isFinalPositive'] ?? false)
|
||||
) {
|
||||
return self::ABSTAIN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$runningWorkflows = array_filter($entityWorkflows, fn (EntityWorkflow $ew) => !$ew->isFinal());
|
||||
// if there is a signature on a **running workflow**, no one is allowed edit anymore, except if the workflow is canceled
|
||||
foreach ($entityWorkflows as $entityWorkflow) {
|
||||
// if the workflow is canceled, we ignore it
|
||||
$isFinalNegative = false;
|
||||
if ($entityWorkflow->isFinal()) {
|
||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
$marking = $workflow->getMarkingStore()->getMarking($entityWorkflow);
|
||||
foreach ($marking->getPlaces() as $place => $int) {
|
||||
$placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
|
||||
if (isset($placeMetadata['isFinalPositive']) && false === $placeMetadata['isFinalPositive']) {
|
||||
$isFinalNegative = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($isFinalNegative) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if there is a signature on a **running workflow**, no one is allowed edit the workflow anymore
|
||||
if (!$isAttached) {
|
||||
foreach ($runningWorkflows as $entityWorkflow) {
|
||||
foreach ($entityWorkflow->getSteps() as $step) {
|
||||
foreach ($step->getSignatures() as $signature) {
|
||||
if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) {
|
||||
return self::FORCE_DENIED;
|
||||
}
|
||||
foreach ($entityWorkflow->getSteps() as $step) {
|
||||
foreach ($step->getSignatures() as $signature) {
|
||||
if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) {
|
||||
return self::FORCE_DENIED;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if all workflows are finalized negative (= canceled), we should abstain
|
||||
$runningWorkflows = [];
|
||||
foreach ($entityWorkflows as $entityWorkflow) {
|
||||
$isFinalNegative = false;
|
||||
if ($entityWorkflow->isFinal()) {
|
||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
$marking = $workflow->getMarkingStore()->getMarking($entityWorkflow);
|
||||
foreach ($marking->getPlaces() as $place => $int) {
|
||||
$placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
|
||||
if (isset($placeMetadata['isFinalPositive']) && false === $placeMetadata['isFinalPositive']) {
|
||||
$isFinalNegative = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$isFinalNegative) {
|
||||
$runningWorkflows[] = $entityWorkflow;
|
||||
}
|
||||
}
|
||||
|
||||
if ([] === $runningWorkflows) {
|
||||
return self::ABSTAIN;
|
||||
}
|
||||
|
||||
// allow only the users involved
|
||||
if ($this->isUserInvolvedInAWorkflow($runningWorkflows)) {
|
||||
return self::FORCE_GRANT;
|
||||
}
|
||||
|
||||
if ($isAttached) {
|
||||
return self::ABSTAIN;
|
||||
}
|
||||
|
||||
return self::FORCE_DENIED;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Workflow\Helper;
|
||||
|
||||
/**
|
||||
* Helper to give supplementary permissions to a related entity.
|
||||
*
|
||||
* If a related entity is associated within a workflow, the logic of the workflow can give more permissions, or
|
||||
* remove some permissions.
|
||||
*
|
||||
* The methods of this helper return either:
|
||||
*
|
||||
* - FORCE_GRANT, which means that a permission can be given, even if it would be denied when the related
|
||||
* entity is not associated with a workflow;
|
||||
* - FORCE_DENIED, which means that a permission should be denied, even if it would be granted when the related entity
|
||||
* is not associated with a workflow
|
||||
* - ABSTAIN, if there is no workflow logic to add or remove permission
|
||||
*
|
||||
* For read operations:
|
||||
*
|
||||
* - if the user is involved in the workflow (is part of the current step, of a step before), the user is granted read
|
||||
* operation;
|
||||
* - if there is a pending signature for a person, the workflow grant access to the related entity;
|
||||
* - if there a signature applyied in less than 12 hours, the workflow grant access to the related entity. This allow to
|
||||
* show the related entity to the person during this time frame.
|
||||
*
|
||||
*
|
||||
* For write operation:
|
||||
*
|
||||
* - if the workflow is finalized "positive" (means "not canceled"), the workflow denys write operations;
|
||||
* - if there isn't any finalized "positive" workflow, and if there is a signature appliyed for a running workflow (not finalized nor canceled),
|
||||
* the workflow denys write operations;
|
||||
* - if there is no case above and the user is involved in the workflow (is part of the current step, of a step before), the user is granted;
|
||||
*/
|
||||
interface WorkflowRelatedEntityPermissionHelperInterface
|
||||
{
|
||||
public const FORCE_GRANT = 'FORCE_GRANT';
|
||||
public const FORCE_DENIED = 'FORCE_DENIED';
|
||||
public const ABSTAIN = 'ABSTAIN';
|
||||
|
||||
/**
|
||||
* @param object $entity The entity may be an object that is a related entity of a workflow, or an EntityWorkflow itself
|
||||
*
|
||||
* @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN'
|
||||
*/
|
||||
public function isAllowedByWorkflowForReadOperation(object $entity): string;
|
||||
|
||||
/**
|
||||
* @param object $entity the entity may be an object which is the related entity of a workflow
|
||||
*
|
||||
* @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN'
|
||||
*/
|
||||
public function isAllowedByWorkflowForWriteOperation(object $entity): string;
|
||||
}
|
||||
@@ -115,3 +115,7 @@ 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,3 +66,7 @@ services:
|
||||
resource: './../../Templating/Listing'
|
||||
|
||||
Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface: '@Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory'
|
||||
|
||||
Chill\MainBundle\Templating\VersionRenderExtension:
|
||||
tags:
|
||||
- { name: twig.extension }
|
||||
|
||||
@@ -48,6 +48,9 @@ See: Voir
|
||||
Name: Nom
|
||||
Label: Nom
|
||||
|
||||
footer:
|
||||
Running chill version %version%: "Version de Chill: %version%"
|
||||
|
||||
user:
|
||||
current_user: Utilisateur courant
|
||||
profile:
|
||||
|
||||
@@ -13,14 +13,63 @@ namespace Chill\PersonBundle\Controller\SocialWork;
|
||||
|
||||
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
||||
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||
use Chill\PersonBundle\Repository\SocialWork\EvaluationRepository;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class EvaluationController extends CRUDController
|
||||
{
|
||||
public function __construct(private readonly EvaluationRepository $repository) {}
|
||||
|
||||
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
|
||||
{
|
||||
$query->addOrderBy('e.id', 'ASC');
|
||||
|
||||
return parent::orderQuery($action, $query, $request, $paginator);
|
||||
}
|
||||
|
||||
protected function getQueryResult(
|
||||
string $action,
|
||||
Request $request,
|
||||
int $totalItems,
|
||||
PaginatorInterface $paginator,
|
||||
?FilterOrderHelper $filterOrder = null,
|
||||
) {
|
||||
if (0 === $totalItems) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!$filterOrder instanceof FilterOrderHelper) {
|
||||
return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder);
|
||||
}
|
||||
|
||||
$queryString = $filterOrder->getQueryString();
|
||||
$activeFilter = $filterOrder->getCheckboxData('activeFilter');
|
||||
$nb = $this->repository->countFilteredEvaluations($queryString, $activeFilter);
|
||||
|
||||
$paginator = $this->getPaginatorFactory()->create($nb);
|
||||
|
||||
return $this->repository->findFilteredEvaluations($queryString, $activeFilter, $paginator->getCurrentPageFirstItemNumber(), $paginator->getItemsPerPage());
|
||||
}
|
||||
|
||||
protected function countEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null): int
|
||||
{
|
||||
if (!$filterOrder instanceof FilterOrderHelper) {
|
||||
return parent::countEntities($action, $request, $filterOrder);
|
||||
}
|
||||
|
||||
return $this->repository->countFilteredEvaluations(
|
||||
$filterOrder->getQueryString(),
|
||||
$filterOrder->getCheckboxData('activeFilter')
|
||||
);
|
||||
}
|
||||
|
||||
protected function buildFilterOrderHelper(string $action, Request $request): ?FilterOrderHelper
|
||||
{
|
||||
return $this->getFilterOrderHelperFactory()
|
||||
->create(self::class)
|
||||
->addSearchBox(['label'])
|
||||
->addCheckbox('activeFilter', [true => 'Active', false => 'Inactive'], ['Active'])
|
||||
->build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,14 +13,68 @@ namespace Chill\PersonBundle\Controller\SocialWork;
|
||||
|
||||
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
||||
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||
use Chill\PersonBundle\Repository\SocialWork\GoalRepository;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class GoalController extends CRUDController
|
||||
{
|
||||
public function __construct(private readonly GoalRepository $repository) {}
|
||||
|
||||
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
|
||||
{
|
||||
$query->addOrderBy('e.id', 'ASC');
|
||||
|
||||
return parent::orderQuery($action, $query, $request, $paginator);
|
||||
}
|
||||
|
||||
protected function getQueryResult(
|
||||
string $action,
|
||||
Request $request,
|
||||
int $totalItems,
|
||||
PaginatorInterface $paginator,
|
||||
?FilterOrderHelper $filterOrder = null,
|
||||
) {
|
||||
if (0 === $totalItems) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!$filterOrder instanceof FilterOrderHelper) {
|
||||
return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder);
|
||||
}
|
||||
|
||||
$queryString = $filterOrder->getQueryString();
|
||||
$activeFilter = $filterOrder->getCheckboxData('activeFilter');
|
||||
$nb = $this->repository->countFilteredGoals($queryString, $activeFilter);
|
||||
|
||||
$paginator = $this->getPaginatorFactory()->create($nb);
|
||||
|
||||
return $this->repository->findFilteredGoals(
|
||||
$queryString,
|
||||
$activeFilter,
|
||||
$paginator->getCurrentPageFirstItemNumber(),
|
||||
$paginator->getItemsPerPage()
|
||||
);
|
||||
}
|
||||
|
||||
protected function countEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null): int
|
||||
{
|
||||
if (!$filterOrder instanceof FilterOrderHelper) {
|
||||
return parent::countEntities($action, $request, $filterOrder);
|
||||
}
|
||||
|
||||
return $this->repository->countFilteredGoals(
|
||||
$filterOrder->getQueryString(),
|
||||
$filterOrder->getCheckboxData('activeFilter')
|
||||
);
|
||||
}
|
||||
|
||||
protected function buildFilterOrderHelper(string $action, Request $request): ?FilterOrderHelper
|
||||
{
|
||||
return $this->getFilterOrderHelperFactory()
|
||||
->create(self::class)
|
||||
->addSearchBox(['label'])
|
||||
->addCheckbox('activeFilter', [true => 'Active', false => 'Inactive'], ['Active'])
|
||||
->build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,14 +13,68 @@ namespace Chill\PersonBundle\Controller\SocialWork;
|
||||
|
||||
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
||||
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||
use Chill\PersonBundle\Repository\SocialWork\ResultRepository;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class ResultController extends CRUDController
|
||||
{
|
||||
public function __construct(private readonly ResultRepository $repository) {}
|
||||
|
||||
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
|
||||
{
|
||||
$query->addOrderBy('e.id', 'ASC');
|
||||
|
||||
return parent::orderQuery($action, $query, $request, $paginator);
|
||||
}
|
||||
|
||||
protected function getQueryResult(
|
||||
string $action,
|
||||
Request $request,
|
||||
int $totalItems,
|
||||
PaginatorInterface $paginator,
|
||||
?FilterOrderHelper $filterOrder = null,
|
||||
) {
|
||||
if (0 === $totalItems) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!$filterOrder instanceof FilterOrderHelper) {
|
||||
return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder);
|
||||
}
|
||||
|
||||
$queryString = $filterOrder->getQueryString();
|
||||
$activeFilter = $filterOrder->getCheckboxData('activeFilter');
|
||||
$nb = $this->repository->countFilteredResults($queryString, $activeFilter);
|
||||
|
||||
$paginator = $this->getPaginatorFactory()->create($nb);
|
||||
|
||||
return $this->repository->findFilteredResults(
|
||||
$queryString,
|
||||
$activeFilter,
|
||||
$paginator->getCurrentPageFirstItemNumber(),
|
||||
$paginator->getItemsPerPage()
|
||||
);
|
||||
}
|
||||
|
||||
protected function countEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null): int
|
||||
{
|
||||
if (!$filterOrder instanceof FilterOrderHelper) {
|
||||
return parent::countEntities($action, $request, $filterOrder);
|
||||
}
|
||||
|
||||
return $this->repository->countFilteredResults(
|
||||
$filterOrder->getQueryString(),
|
||||
$filterOrder->getCheckboxData('activeFilter')
|
||||
);
|
||||
}
|
||||
|
||||
protected function buildFilterOrderHelper(string $action, Request $request): ?FilterOrderHelper
|
||||
{
|
||||
return $this->getFilterOrderHelperFactory()
|
||||
->create(self::class)
|
||||
->addSearchBox(['label'])
|
||||
->addCheckbox('activeFilter', [true => 'Active', false => 'Inactive'], ['Active'])
|
||||
->build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,14 +13,68 @@ namespace Chill\PersonBundle\Controller\SocialWork;
|
||||
|
||||
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
||||
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||
use Chill\PersonBundle\Repository\SocialWork\SocialActionRepository;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class SocialActionController extends CRUDController
|
||||
{
|
||||
public function __construct(private readonly SocialActionRepository $repository) {}
|
||||
|
||||
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
|
||||
{
|
||||
$query->addOrderBy('e.ordering', 'ASC');
|
||||
|
||||
return parent::orderQuery($action, $query, $request, $paginator);
|
||||
}
|
||||
|
||||
protected function getQueryResult(
|
||||
string $action,
|
||||
Request $request,
|
||||
int $totalItems,
|
||||
PaginatorInterface $paginator,
|
||||
?FilterOrderHelper $filterOrder = null,
|
||||
) {
|
||||
if (0 === $totalItems) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!$filterOrder instanceof FilterOrderHelper) {
|
||||
return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder);
|
||||
}
|
||||
|
||||
$queryString = $filterOrder->getQueryString();
|
||||
$activeFilter = $filterOrder->getCheckboxData('activeFilter');
|
||||
$nb = $this->repository->countFilteredSocialActions($queryString, $activeFilter);
|
||||
|
||||
$paginator = $this->getPaginatorFactory()->create($nb);
|
||||
|
||||
return $this->repository->findFilteredSocialActions(
|
||||
$queryString,
|
||||
$activeFilter,
|
||||
$paginator->getCurrentPageFirstItemNumber(),
|
||||
$paginator->getItemsPerPage()
|
||||
);
|
||||
}
|
||||
|
||||
protected function countEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null): int
|
||||
{
|
||||
if (!$filterOrder instanceof FilterOrderHelper) {
|
||||
return parent::countEntities($action, $request, $filterOrder);
|
||||
}
|
||||
|
||||
return $this->repository->countFilteredSocialActions(
|
||||
$filterOrder->getQueryString(),
|
||||
$filterOrder->getCheckboxData('activeFilter')
|
||||
);
|
||||
}
|
||||
|
||||
protected function buildFilterOrderHelper(string $action, Request $request): ?FilterOrderHelper
|
||||
{
|
||||
return $this->getFilterOrderHelperFactory()
|
||||
->create(self::class)
|
||||
->addSearchBox(['label'])
|
||||
->addCheckbox('activeFilter', [true => 'Active', false => 'Inactive'], ['Active'])
|
||||
->build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,15 @@ namespace Chill\PersonBundle\Controller\SocialWork;
|
||||
|
||||
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
||||
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class SocialIssueController extends CRUDController
|
||||
{
|
||||
public function __construct(private readonly SocialIssueRepository $repository) {}
|
||||
|
||||
protected function createFormFor(string $action, $entity, ?string $formClass = null, array $formOptions = []): FormInterface
|
||||
{
|
||||
if ('new' === $action) {
|
||||
@@ -37,4 +41,54 @@ class SocialIssueController extends CRUDController
|
||||
|
||||
return parent::orderQuery($action, $query, $request, $paginator);
|
||||
}
|
||||
|
||||
protected function getQueryResult(
|
||||
string $action,
|
||||
Request $request,
|
||||
int $totalItems,
|
||||
PaginatorInterface $paginator,
|
||||
?FilterOrderHelper $filterOrder = null,
|
||||
) {
|
||||
if (0 === $totalItems) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!$filterOrder instanceof FilterOrderHelper) {
|
||||
return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder);
|
||||
}
|
||||
|
||||
$queryString = $filterOrder->getQueryString();
|
||||
$activeFilter = $filterOrder->getCheckboxData('activeFilter');
|
||||
$nb = $this->repository->countFilteredSocialIssues($queryString, $activeFilter);
|
||||
|
||||
$paginator = $this->getPaginatorFactory()->create($nb);
|
||||
|
||||
return $this->repository->findFilteredSocialIssues(
|
||||
$queryString,
|
||||
$activeFilter,
|
||||
$paginator->getCurrentPageFirstItemNumber(),
|
||||
$paginator->getItemsPerPage()
|
||||
);
|
||||
}
|
||||
|
||||
protected function countEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null): int
|
||||
{
|
||||
if (!$filterOrder instanceof FilterOrderHelper) {
|
||||
return parent::countEntities($action, $request, $filterOrder);
|
||||
}
|
||||
|
||||
return $this->repository->countFilteredSocialIssues(
|
||||
$filterOrder->getQueryString(),
|
||||
$filterOrder->getCheckboxData('activeFilter')
|
||||
);
|
||||
}
|
||||
|
||||
protected function buildFilterOrderHelper(string $action, Request $request): ?FilterOrderHelper
|
||||
{
|
||||
return $this->getFilterOrderHelperFactory()
|
||||
->create(self::class)
|
||||
->addSearchBox(['label'])
|
||||
->addCheckbox('activeFilter', [true => 'Active', false => 'Inactive'], ['Active'])
|
||||
->build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class SocialWorkEvaluationApiController extends AbstractController
|
||||
$pagination->getCurrentPageFirstItemNumber(),
|
||||
$pagination->getItemsPerPage()
|
||||
);
|
||||
$collection = new Collection($evaluations, $pagination);
|
||||
$collection = new Collection(array_values($evaluations), $pagination);
|
||||
|
||||
return $this->json($collection, Response::HTTP_OK, [], ['groups' => ['read']]);
|
||||
}
|
||||
|
||||
@@ -25,14 +25,15 @@ class SocialWorkGoalApiController extends ApiController
|
||||
|
||||
public function listBySocialAction(Request $request, SocialAction $action): Response
|
||||
{
|
||||
$totalItems = $this->goalRepository->countBySocialActionWithDescendants($action);
|
||||
$paginator = $this->getPaginatorFactory()->create($totalItems);
|
||||
$totalItems = $this->goalRepository->countBySocialActionWithDescendants($action, true);
|
||||
$paginator = $this->paginator->create($totalItems);
|
||||
|
||||
$entities = $this->goalRepository->findBySocialActionWithDescendants(
|
||||
$action,
|
||||
['id' => 'ASC'],
|
||||
$paginator->getItemsPerPage(),
|
||||
$paginator->getCurrentPageFirstItemNumber()
|
||||
$paginator->getCurrentPageFirstItemNumber(),
|
||||
onlyActive: true
|
||||
);
|
||||
|
||||
$model = new Collection($entities, $paginator);
|
||||
|
||||
@@ -25,14 +25,15 @@ class SocialWorkResultApiController extends ApiController
|
||||
|
||||
public function listByGoal(Request $request, Goal $goal): Response
|
||||
{
|
||||
$totalItems = $this->resultRepository->countByGoal($goal);
|
||||
$totalItems = $this->resultRepository->countByGoal($goal, true);
|
||||
$paginator = $this->getPaginatorFactory()->create($totalItems);
|
||||
|
||||
$entities = $this->resultRepository->findByGoal(
|
||||
$goal,
|
||||
['id' => 'ASC'],
|
||||
$paginator->getItemsPerPage(),
|
||||
$paginator->getCurrentPageFirstItemNumber()
|
||||
$paginator->getCurrentPageFirstItemNumber(),
|
||||
onlyActive: true,
|
||||
);
|
||||
|
||||
$model = new Collection($entities, $paginator);
|
||||
@@ -42,14 +43,15 @@ class SocialWorkResultApiController extends ApiController
|
||||
|
||||
public function listBySocialAction(Request $request, SocialAction $action): Response
|
||||
{
|
||||
$totalItems = $this->resultRepository->countBySocialActionWithDescendants($action);
|
||||
$totalItems = $this->resultRepository->countBySocialActionWithDescendants($action, true);
|
||||
$paginator = $this->getPaginatorFactory()->create($totalItems);
|
||||
|
||||
$entities = $this->resultRepository->findBySocialActionWithDescendants(
|
||||
$action,
|
||||
['id' => 'ASC'],
|
||||
$paginator->getItemsPerPage(),
|
||||
$paginator->getCurrentPageFirstItemNumber()
|
||||
$paginator->getCurrentPageFirstItemNumber(),
|
||||
onlyActive: true
|
||||
);
|
||||
|
||||
$model = new Collection($entities, $paginator);
|
||||
|
||||
@@ -19,6 +19,7 @@ use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Chill\PersonBundle\Export\Declarations;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final readonly class CenterAggregator implements AggregatorInterface
|
||||
{
|
||||
@@ -27,6 +28,7 @@ final readonly class CenterAggregator implements AggregatorInterface
|
||||
public function __construct(
|
||||
private CenterRepositoryInterface $centerRepository,
|
||||
private RollingDateConverterInterface $rollingDateConverter,
|
||||
private TranslatorInterface $translator,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder): void
|
||||
@@ -62,7 +64,7 @@ final readonly class CenterAggregator implements AggregatorInterface
|
||||
{
|
||||
return function (int|string|null $value) {
|
||||
if (null === $value || '' === $value) {
|
||||
return '';
|
||||
return $this->translator->trans('person.export.aggregator.by_center.no_center');
|
||||
}
|
||||
|
||||
if ('_header' === $value) {
|
||||
@@ -94,15 +96,18 @@ final readonly class CenterAggregator implements AggregatorInterface
|
||||
$atDate = 'pers_center_agg_at_date';
|
||||
|
||||
$qb->leftJoin('person.centerHistory', $alias);
|
||||
$qb
|
||||
->andWhere(
|
||||
$qb->expr()->lte($alias.'.startDate', ':'.$atDate),
|
||||
)->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isNull($alias.'.endDate'),
|
||||
$qb->expr()->gt($alias.'.endDate', ':'.$atDate)
|
||||
$qb->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isNull($alias.'.id'),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->lte($alias.'.startDate', ':'.$atDate),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isNull($alias.'.endDate'),
|
||||
$qb->expr()->gt($alias.'.endDate', ':'.$atDate)
|
||||
)
|
||||
)
|
||||
);
|
||||
)
|
||||
);
|
||||
$qb->setParameter($atDate, $this->rollingDateConverter->convert($data['at_date']));
|
||||
|
||||
$qb->addSelect("IDENTITY({$alias}.center) AS ".self::COLUMN_NAME);
|
||||
|
||||
@@ -44,12 +44,10 @@ final readonly class FilterListAccompanyingPeriodHelper implements FilterListAcc
|
||||
};
|
||||
|
||||
// add filtering on confidential accompanying period. The confidential is applyed on the current status of
|
||||
// the accompanying period (we do not use the 'calc_date' here
|
||||
$aclConditionsOrX = $qb->expr()->orX(
|
||||
// either the current user is the refferer for the course
|
||||
'acp.user = :list_acp_current_user',
|
||||
);
|
||||
$qb->setParameter('list_acp_current_user', $user);
|
||||
// the accompanying period (we do not use the 'calc_date' here)
|
||||
//
|
||||
// IMPORTANT: we must NOT bypass selected centers just because the current user is the referrer.
|
||||
$aclConditionsOrX = $qb->expr()->orX();
|
||||
|
||||
$i = 0;
|
||||
foreach ($centers as $center) {
|
||||
@@ -93,6 +91,12 @@ final readonly class FilterListAccompanyingPeriodHelper implements FilterListAcc
|
||||
++$i;
|
||||
}
|
||||
|
||||
$qb->andWhere($aclConditionsOrX);
|
||||
// Prevent invalid/empty WHERE when no conditions were added (e.g., no centers available)
|
||||
if (0 === count($aclConditionsOrX->getParts())) {
|
||||
// No allowed conditions => return no rows
|
||||
$qb->andWhere('1 = 0');
|
||||
} else {
|
||||
$qb->andWhere($aclConditionsOrX);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,9 +100,9 @@ class AccompanyingPeriodWorkRepository implements ObjectRepository
|
||||
$rsm->addRootEntityFromClassMetadata(AccompanyingPeriodWork::class, 'w');
|
||||
|
||||
$sql = "SELECT {$rsm} FROM chill_person_accompanying_period_work w
|
||||
LEFT JOIN chill_person_accompanying_period_work_referrer AS rw ON accompanyingperiodwork_id = w.id
|
||||
AND (rw.enddate IS NULL OR rw.enddate > CURRENT_DATE)
|
||||
WHERE accompanyingPeriod_id = :periodId";
|
||||
LEFT JOIN chill_person_accompanying_period_work_referrer AS rw ON accompanyingperiodwork_id = w.id
|
||||
AND (rw.enddate IS NULL OR rw.enddate > CURRENT_DATE)
|
||||
WHERE accompanyingPeriod_id = :periodId";
|
||||
|
||||
// implement filters
|
||||
|
||||
@@ -136,11 +136,14 @@ class AccompanyingPeriodWorkRepository implements ObjectRepository
|
||||
}
|
||||
|
||||
// set limit and offset
|
||||
$sql .= " ORDER BY
|
||||
CASE WHEN w.enddate IS NULL THEN '-infinity'::timestamp ELSE 'infinity'::timestamp END ASC,
|
||||
w.startdate DESC,
|
||||
w.enddate DESC,
|
||||
w.id DESC";
|
||||
$sql .= ' ORDER BY
|
||||
CASE
|
||||
WHEN w.enddate IS NULL OR w.enddate > CURRENT_DATE THEN 0
|
||||
ELSE 1
|
||||
END ASC,
|
||||
w.startdate DESC,
|
||||
w.enddate DESC,
|
||||
w.id DESC';
|
||||
|
||||
$sql .= ' LIMIT :limit OFFSET :offset';
|
||||
|
||||
|
||||
@@ -14,12 +14,16 @@ namespace Chill\PersonBundle\Repository\SocialWork;
|
||||
use Chill\PersonBundle\Entity\SocialWork\Evaluation;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
use Doctrine\ORM\NoResultException;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
final readonly class EvaluationRepository implements EvaluationRepositoryInterface
|
||||
{
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
public function __construct(private EntityManagerInterface $entityManager, private RequestStack $requestStack)
|
||||
{
|
||||
$this->repository = $entityManager->getRepository(Evaluation::class);
|
||||
}
|
||||
@@ -65,4 +69,86 @@ final readonly class EvaluationRepository implements EvaluationRepositoryInterfa
|
||||
{
|
||||
return Evaluation::class;
|
||||
}
|
||||
|
||||
private function getLang(): string
|
||||
{
|
||||
return $this->requestStack->getCurrentRequest()?->getLocale() ?? 'fr';
|
||||
}
|
||||
|
||||
public function getResult(
|
||||
QueryBuilder $qb,
|
||||
?int $start = 0,
|
||||
?int $limit = 50,
|
||||
?array $orderBy = [],
|
||||
): array {
|
||||
$qb->select('e');
|
||||
|
||||
$qb
|
||||
->setFirstResult($start)
|
||||
->setMaxResults($limit);
|
||||
|
||||
foreach ($orderBy as $field => $direction) {
|
||||
$qb->addOrderBy('e.'.$field, $direction);
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
private function queryByTitle(string $pattern): QueryBuilder
|
||||
{
|
||||
$qb = $this->entityManager->createQueryBuilder()->from(Evaluation::class, 'e');
|
||||
|
||||
// Extract the current locale's value from the JSON `title` and search on it
|
||||
$qb
|
||||
->where($qb->expr()->like('LOWER(UNACCENT(JSON_EXTRACT(e.title, :lang)))', "CONCAT('%', LOWER(UNACCENT(:pattern)), '%')"))
|
||||
->setParameter('pattern', $pattern)
|
||||
->setParameter('lang', $this->getLang());
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function buildFilterBaseQuery(?string $queryString, array $isActive): QueryBuilder
|
||||
{
|
||||
if (null !== $queryString) {
|
||||
$qb = $this->queryByTitle($queryString);
|
||||
} else {
|
||||
$qb = $this->entityManager->createQueryBuilder()->from(Evaluation::class, 'e');
|
||||
}
|
||||
|
||||
// Add condition based on active/inactive status
|
||||
if (in_array('Active', $isActive, true) && !in_array('Inactive', $isActive, true)) {
|
||||
$qb->andWhere('e.active = true');
|
||||
} elseif (in_array('Inactive', $isActive, true) && !in_array('Active', $isActive, true)) {
|
||||
$qb->andWhere('e.active = false');
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function findFilteredEvaluations(
|
||||
?string $queryString = null,
|
||||
array $isActive = ['active'],
|
||||
?int $start = 0,
|
||||
?int $limit = 50,
|
||||
?array $orderBy = ['title' => 'ASC'],
|
||||
): array {
|
||||
$qb = $this->buildFilterBaseQuery($queryString, $isActive);
|
||||
|
||||
return $this->getResult($qb, $start, $limit, $orderBy);
|
||||
}
|
||||
|
||||
public function countFilteredEvaluations(
|
||||
?string $queryString = null,
|
||||
array $isActive = ['active'],
|
||||
): int {
|
||||
$qb = $this->buildFilterBaseQuery($queryString, $isActive);
|
||||
|
||||
try {
|
||||
return $qb
|
||||
->select('COUNT(e)')
|
||||
->getQuery()->getSingleScalarResult();
|
||||
} catch (NoResultException|NonUniqueResultException $e) {
|
||||
throw new \LogicException('a count query should return one result', previous: $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,21 +15,28 @@ use Chill\PersonBundle\Entity\SocialWork\Goal;
|
||||
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
use Doctrine\ORM\NoResultException;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
|
||||
final readonly class GoalRepository implements ObjectRepository
|
||||
{
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private ClockInterface $clock,
|
||||
private RequestStack $requestStack,
|
||||
) {
|
||||
$this->repository = $entityManager->getRepository(Goal::class);
|
||||
}
|
||||
|
||||
public function countBySocialActionWithDescendants(SocialAction $action): int
|
||||
public function countBySocialActionWithDescendants(SocialAction $action, bool $onlyActive = false): int
|
||||
{
|
||||
$qb = $this->buildQueryBySocialActionWithDescendants($action);
|
||||
$qb = $this->buildQueryBySocialActionWithDescendants($action, $onlyActive);
|
||||
$qb->select('COUNT(g)');
|
||||
|
||||
return $qb
|
||||
@@ -64,9 +71,9 @@ final readonly class GoalRepository implements ObjectRepository
|
||||
/**
|
||||
* @return Goal[]
|
||||
*/
|
||||
public function findBySocialActionWithDescendants(SocialAction $action, array $orderBy = [], ?int $limit = null, ?int $offset = null): array
|
||||
public function findBySocialActionWithDescendants(SocialAction $action, array $orderBy = [], ?int $limit = null, ?int $offset = null, bool $onlyActive = false): array
|
||||
{
|
||||
$qb = $this->buildQueryBySocialActionWithDescendants($action);
|
||||
$qb = $this->buildQueryBySocialActionWithDescendants($action, $onlyActive);
|
||||
$qb->select('g');
|
||||
|
||||
$qb->andWhere(
|
||||
@@ -101,7 +108,103 @@ final readonly class GoalRepository implements ObjectRepository
|
||||
return Goal::class;
|
||||
}
|
||||
|
||||
private function buildQueryBySocialActionWithDescendants(SocialAction $action): QueryBuilder
|
||||
private function getLang(): string
|
||||
{
|
||||
return $this->requestStack->getCurrentRequest()?->getLocale() ?? 'fr';
|
||||
}
|
||||
|
||||
public function getResult(
|
||||
QueryBuilder $qb,
|
||||
?int $start = 0,
|
||||
?int $limit = 50,
|
||||
?array $orderBy = [],
|
||||
): array {
|
||||
$qb->select('g');
|
||||
|
||||
$qb
|
||||
->setFirstResult($start)
|
||||
->setMaxResults($limit);
|
||||
|
||||
foreach ($orderBy as $field => $direction) {
|
||||
$qb->addOrderBy('g.'.$field, $direction);
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
private function queryByTitle(string $pattern): QueryBuilder
|
||||
{
|
||||
$qb = $this->entityManager->createQueryBuilder()->from(Goal::class, 'g');
|
||||
|
||||
// search across locales by extracting the localized value
|
||||
$qb
|
||||
->where($qb->expr()->like('LOWER(UNACCENT(JSON_EXTRACT(g.title, :lang)))', "CONCAT('%', LOWER(UNACCENT(:pattern)), '%')"))
|
||||
->setParameter('pattern', $pattern)
|
||||
->setParameter('lang', $this->getLang());
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function buildFilterBaseQuery(?string $queryString, array $isActive): QueryBuilder
|
||||
{
|
||||
if (null !== $queryString) {
|
||||
$qb = $this->queryByTitle($queryString);
|
||||
} else {
|
||||
$qb = $this->entityManager->createQueryBuilder()->from(Goal::class, 'g');
|
||||
}
|
||||
|
||||
// Active when desactivationDate is null or in the future
|
||||
$now = new \DateTime('now');
|
||||
if (in_array('Active', $isActive, true) && !in_array('Inactive', $isActive, true)) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isNull('g.desactivationDate'),
|
||||
$qb->expr()->gt('g.desactivationDate', ':now')
|
||||
)
|
||||
)->setParameter('now', $now);
|
||||
} elseif (in_array('Inactive', $isActive, true) && !in_array('Active', $isActive, true)) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('g.desactivationDate'),
|
||||
$qb->expr()->lte('g.desactivationDate', ':now')
|
||||
)
|
||||
)->setParameter('now', $now);
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, Goal>
|
||||
*/
|
||||
public function findFilteredGoals(
|
||||
?string $queryString = null,
|
||||
array $isActive = ['active'],
|
||||
?int $start = 0,
|
||||
?int $limit = 50,
|
||||
?array $orderBy = ['id' => 'ASC'],
|
||||
): array {
|
||||
$qb = $this->buildFilterBaseQuery($queryString, $isActive);
|
||||
|
||||
return $this->getResult($qb, $start, $limit, $orderBy);
|
||||
}
|
||||
|
||||
public function countFilteredGoals(
|
||||
?string $queryString = null,
|
||||
array $isActive = ['active'],
|
||||
): int {
|
||||
$qb = $this->buildFilterBaseQuery($queryString, $isActive);
|
||||
|
||||
try {
|
||||
return $qb
|
||||
->select('COUNT(g)')
|
||||
->getQuery()->getSingleScalarResult();
|
||||
} catch (NoResultException|NonUniqueResultException $e) {
|
||||
throw new \LogicException('a count query should return one result', previous: $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function buildQueryBySocialActionWithDescendants(SocialAction $action, bool $onlyActive): QueryBuilder
|
||||
{
|
||||
$actions = $action->getDescendantsWithThis();
|
||||
|
||||
@@ -116,6 +219,11 @@ final readonly class GoalRepository implements ObjectRepository
|
||||
}
|
||||
$qb->where($orx);
|
||||
|
||||
if ($onlyActive) {
|
||||
$qb->andWhere('g.desactivationDate > :now OR g.desactivationDate IS NULL')
|
||||
->setParameter('now', $this->clock->now());
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,21 +16,28 @@ use Chill\PersonBundle\Entity\SocialWork\Result;
|
||||
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
use Doctrine\ORM\NoResultException;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
|
||||
final readonly class ResultRepository implements ObjectRepository
|
||||
{
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private ClockInterface $clock,
|
||||
private RequestStack $requestStack,
|
||||
) {
|
||||
$this->repository = $entityManager->getRepository(Result::class);
|
||||
}
|
||||
|
||||
public function countByGoal(Goal $goal): int
|
||||
public function countByGoal(Goal $goal, bool $onlyActive = false): int
|
||||
{
|
||||
$qb = $this->buildQueryByGoal($goal);
|
||||
$qb = $this->buildQueryByGoal($goal, $onlyActive);
|
||||
$qb->select('COUNT(r)');
|
||||
|
||||
return $qb
|
||||
@@ -38,9 +45,9 @@ final readonly class ResultRepository implements ObjectRepository
|
||||
->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function countBySocialActionWithDescendants(SocialAction $action): int
|
||||
public function countBySocialActionWithDescendants(SocialAction $action, bool $onlyActive = false): int
|
||||
{
|
||||
$qb = $this->buildQueryBySocialActionWithDescendants($action);
|
||||
$qb = $this->buildQueryBySocialActionWithDescendants($action, $onlyActive);
|
||||
$qb->select('COUNT(r)');
|
||||
|
||||
return $qb
|
||||
@@ -75,9 +82,9 @@ final readonly class ResultRepository implements ObjectRepository
|
||||
/**
|
||||
* @return array<Result>
|
||||
*/
|
||||
public function findByGoal(Goal $goal, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||
public function findByGoal(Goal $goal, ?array $orderBy = null, ?int $limit = null, ?int $offset = null, bool $onlyActive = false): array
|
||||
{
|
||||
$qb = $this->buildQueryByGoal($goal);
|
||||
$qb = $this->buildQueryByGoal($goal, $onlyActive);
|
||||
|
||||
if (null !== $orderBy) {
|
||||
foreach ($orderBy as $sort => $order) {
|
||||
@@ -96,9 +103,9 @@ final readonly class ResultRepository implements ObjectRepository
|
||||
/**
|
||||
* @return Result[]
|
||||
*/
|
||||
public function findBySocialActionWithDescendants(SocialAction $action, array $orderBy = [], ?int $limit = null, ?int $offset = null): array
|
||||
public function findBySocialActionWithDescendants(SocialAction $action, array $orderBy = [], ?int $limit = null, ?int $offset = null, bool $onlyActive = false): array
|
||||
{
|
||||
$qb = $this->buildQueryBySocialActionWithDescendants($action);
|
||||
$qb = $this->buildQueryBySocialActionWithDescendants($action, $onlyActive);
|
||||
$qb->select('r');
|
||||
|
||||
foreach ($orderBy as $sort => $order) {
|
||||
@@ -125,17 +132,116 @@ final readonly class ResultRepository implements ObjectRepository
|
||||
return Result::class;
|
||||
}
|
||||
|
||||
private function buildQueryByGoal(Goal $goal): QueryBuilder
|
||||
private function getLang(): string
|
||||
{
|
||||
return $this->requestStack->getCurrentRequest()?->getLocale() ?? 'fr';
|
||||
}
|
||||
|
||||
public function getResult(
|
||||
QueryBuilder $qb,
|
||||
?int $start = 0,
|
||||
?int $limit = 50,
|
||||
?array $orderBy = [],
|
||||
): array {
|
||||
$qb->select('r');
|
||||
|
||||
$qb
|
||||
->setFirstResult($start)
|
||||
->setMaxResults($limit);
|
||||
|
||||
foreach ($orderBy as $field => $direction) {
|
||||
$qb->addOrderBy('r.'.$field, $direction);
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
private function queryByTitle(string $pattern): QueryBuilder
|
||||
{
|
||||
$qb = $this->entityManager->createQueryBuilder()->from(Result::class, 'r');
|
||||
|
||||
$qb
|
||||
->where($qb->expr()->like('LOWER(UNACCENT(JSON_EXTRACT(r.title, :lang)))', "CONCAT('%', LOWER(UNACCENT(:pattern)), '%')"))
|
||||
->setParameter('pattern', $pattern)
|
||||
->setParameter('lang', $this->getLang());
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function buildFilterBaseQuery(?string $queryString, array $isActive): QueryBuilder
|
||||
{
|
||||
if (null !== $queryString) {
|
||||
$qb = $this->queryByTitle($queryString);
|
||||
} else {
|
||||
$qb = $this->entityManager->createQueryBuilder()->from(Result::class, 'r');
|
||||
}
|
||||
|
||||
$now = new \DateTime('now');
|
||||
if (in_array('Active', $isActive, true) && !in_array('Inactive', $isActive, true)) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isNull('r.desactivationDate'),
|
||||
$qb->expr()->gt('r.desactivationDate', ':now')
|
||||
)
|
||||
)->setParameter('now', $now);
|
||||
} elseif (in_array('Inactive', $isActive, true) && !in_array('Active', $isActive, true)) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('r.desactivationDate'),
|
||||
$qb->expr()->lte('r.desactivationDate', ':now')
|
||||
)
|
||||
)->setParameter('now', $now);
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, Result>
|
||||
*/
|
||||
public function findFilteredResults(
|
||||
?string $queryString = null,
|
||||
array $isActive = ['active'],
|
||||
?int $start = 0,
|
||||
?int $limit = 50,
|
||||
?array $orderBy = ['id' => 'ASC'],
|
||||
): array {
|
||||
$qb = $this->buildFilterBaseQuery($queryString, $isActive);
|
||||
|
||||
return $this->getResult($qb, $start, $limit, $orderBy);
|
||||
}
|
||||
|
||||
public function countFilteredResults(
|
||||
?string $queryString = null,
|
||||
array $isActive = ['active'],
|
||||
): int {
|
||||
$qb = $this->buildFilterBaseQuery($queryString, $isActive);
|
||||
|
||||
try {
|
||||
return $qb
|
||||
->select('COUNT(r)')
|
||||
->getQuery()->getSingleScalarResult();
|
||||
} catch (NoResultException|NonUniqueResultException $e) {
|
||||
throw new \LogicException('a count query should return one result', previous: $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function buildQueryByGoal(Goal $goal, bool $onlyActive): QueryBuilder
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('r');
|
||||
|
||||
$qb->where(':goal MEMBER OF r.goals')
|
||||
->setParameter('goal', $goal);
|
||||
|
||||
if ($onlyActive) {
|
||||
$qb->andWhere('r.desactivationDate > :now OR r.desactivationDate IS NULL')
|
||||
->setParameter('now', $this->clock->now());
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
private function buildQueryBySocialActionWithDescendants(SocialAction $action): QueryBuilder
|
||||
private function buildQueryBySocialActionWithDescendants(SocialAction $action, bool $onlyActive = false): QueryBuilder
|
||||
{
|
||||
$actions = $action->getDescendantsWithThis();
|
||||
|
||||
@@ -150,6 +256,11 @@ final readonly class ResultRepository implements ObjectRepository
|
||||
}
|
||||
$qb->where($orx);
|
||||
|
||||
if ($onlyActive) {
|
||||
$qb->andWhere('r.desactivationDate > :now OR r.desactivationDate IS NULL')
|
||||
->setParameter('now', $this->clock->now());
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,14 +14,17 @@ namespace Chill\PersonBundle\Repository\SocialWork;
|
||||
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
use Doctrine\ORM\NoResultException;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
final readonly class SocialActionRepository implements ObjectRepository
|
||||
{
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
public function __construct(private EntityManagerInterface $entityManager, private RequestStack $requestStack)
|
||||
{
|
||||
$this->repository = $entityManager->getRepository(SocialAction::class);
|
||||
}
|
||||
@@ -84,6 +87,100 @@ final readonly class SocialActionRepository implements ObjectRepository
|
||||
return SocialAction::class;
|
||||
}
|
||||
|
||||
private function getLang(): string
|
||||
{
|
||||
return $this->requestStack->getCurrentRequest()?->getLocale() ?? 'fr';
|
||||
}
|
||||
|
||||
public function getResult(
|
||||
QueryBuilder $qb,
|
||||
?int $start = 0,
|
||||
?int $limit = 50,
|
||||
?array $orderBy = [],
|
||||
): array {
|
||||
$qb->select('sa');
|
||||
|
||||
$qb
|
||||
->setFirstResult($start)
|
||||
->setMaxResults($limit);
|
||||
|
||||
foreach ($orderBy as $field => $direction) {
|
||||
$qb->addOrderBy('sa.'.$field, $direction);
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
private function queryByTitle(string $pattern): QueryBuilder
|
||||
{
|
||||
$qb = $this->entityManager->createQueryBuilder()->from(SocialAction::class, 'sa');
|
||||
|
||||
$qb
|
||||
->where($qb->expr()->like('LOWER(UNACCENT(JSON_EXTRACT(sa.title, :lang)))', "CONCAT('%', LOWER(UNACCENT(:pattern)), '%')"))
|
||||
->setParameter('pattern', $pattern)
|
||||
->setParameter('lang', $this->getLang());
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function buildFilterBaseQuery(?string $queryString, array $isActive): QueryBuilder
|
||||
{
|
||||
if (null !== $queryString) {
|
||||
$qb = $this->queryByTitle($queryString);
|
||||
} else {
|
||||
$qb = $this->entityManager->createQueryBuilder()->from(SocialAction::class, 'sa');
|
||||
}
|
||||
|
||||
$now = new \DateTime('now');
|
||||
if (in_array('Active', $isActive, true) && !in_array('Inactive', $isActive, true)) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isNull('sa.desactivationDate'),
|
||||
$qb->expr()->gt('sa.desactivationDate', ':now')
|
||||
)
|
||||
)->setParameter('now', $now);
|
||||
} elseif (in_array('Inactive', $isActive, true) && !in_array('Active', $isActive, true)) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('sa.desactivationDate'),
|
||||
$qb->expr()->lte('sa.desactivationDate', ':now')
|
||||
)
|
||||
)->setParameter('now', $now);
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, SocialAction>
|
||||
*/
|
||||
public function findFilteredSocialActions(
|
||||
?string $queryString = null,
|
||||
array $isActive = ['active'],
|
||||
?int $start = 0,
|
||||
?int $limit = 50,
|
||||
?array $orderBy = ['ordering' => 'ASC'],
|
||||
): array {
|
||||
$qb = $this->buildFilterBaseQuery($queryString, $isActive);
|
||||
|
||||
return $this->getResult($qb, $start, $limit, $orderBy);
|
||||
}
|
||||
|
||||
public function countFilteredSocialActions(
|
||||
?string $queryString = null,
|
||||
array $isActive = ['active'],
|
||||
): int {
|
||||
$qb = $this->buildFilterBaseQuery($queryString, $isActive);
|
||||
|
||||
try {
|
||||
return $qb
|
||||
->select('COUNT(sa)')
|
||||
->getQuery()->getSingleScalarResult();
|
||||
} catch (NoResultException|NonUniqueResultException $e) {
|
||||
throw new \LogicException('a count query should return one result', previous: $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function buildQueryWithDesactivatedDateCriteria(): QueryBuilder
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('sa');
|
||||
|
||||
@@ -14,14 +14,17 @@ namespace Chill\PersonBundle\Repository\SocialWork;
|
||||
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
use Doctrine\ORM\NoResultException;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
final readonly class SocialIssueRepository implements ObjectRepository
|
||||
{
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
public function __construct(private EntityManagerInterface $entityManager, private RequestStack $requestStack)
|
||||
{
|
||||
$this->repository = $entityManager->getRepository(SocialIssue::class);
|
||||
}
|
||||
@@ -79,6 +82,100 @@ final readonly class SocialIssueRepository implements ObjectRepository
|
||||
return SocialIssue::class;
|
||||
}
|
||||
|
||||
public function getResult(
|
||||
QueryBuilder $qb,
|
||||
?int $start = 0,
|
||||
?int $limit = 50,
|
||||
?array $orderBy = [],
|
||||
): array {
|
||||
$qb->select('si');
|
||||
|
||||
$qb
|
||||
->setFirstResult($start)
|
||||
->setMaxResults($limit);
|
||||
|
||||
foreach ($orderBy as $field => $direction) {
|
||||
$qb->addOrderBy('si.'.$field, $direction);
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
private function getLang(): string
|
||||
{
|
||||
return $this->requestStack->getCurrentRequest()?->getLocale() ?? 'fr';
|
||||
}
|
||||
|
||||
private function queryByTitle(string $pattern): QueryBuilder
|
||||
{
|
||||
$qb = $this->entityManager->createQueryBuilder()->from(SocialIssue::class, 'si');
|
||||
|
||||
$qb
|
||||
->where($qb->expr()->like('LOWER(UNACCENT(JSON_EXTRACT(si.title, :lang)))', "CONCAT('%', LOWER(UNACCENT(:pattern)), '%')"))
|
||||
->setParameter('pattern', $pattern)
|
||||
->setParameter('lang', $this->getLang());
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function buildFilterBaseQuery(?string $queryString, array $isActive): QueryBuilder
|
||||
{
|
||||
if (null !== $queryString) {
|
||||
$qb = $this->queryByTitle($queryString);
|
||||
} else {
|
||||
$qb = $this->entityManager->createQueryBuilder()->from(SocialIssue::class, 'si');
|
||||
}
|
||||
|
||||
$now = new \DateTime('now');
|
||||
if (in_array('Active', $isActive, true) && !in_array('Inactive', $isActive, true)) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isNull('si.desactivationDate'),
|
||||
$qb->expr()->gt('si.desactivationDate', ':now')
|
||||
)
|
||||
)->setParameter('now', $now);
|
||||
} elseif (in_array('Inactive', $isActive, true) && !in_array('Active', $isActive, true)) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('si.desactivationDate'),
|
||||
$qb->expr()->lte('si.desactivationDate', ':now')
|
||||
)
|
||||
)->setParameter('now', $now);
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, SocialIssue>
|
||||
*/
|
||||
public function findFilteredSocialIssues(
|
||||
?string $queryString = null,
|
||||
array $isActive = ['active'],
|
||||
?int $start = 0,
|
||||
?int $limit = 50,
|
||||
?array $orderBy = ['ordering' => 'ASC'],
|
||||
): array {
|
||||
$qb = $this->buildFilterBaseQuery($queryString, $isActive);
|
||||
|
||||
return $this->getResult($qb, $start, $limit, $orderBy);
|
||||
}
|
||||
|
||||
public function countFilteredSocialIssues(
|
||||
?string $queryString = null,
|
||||
array $isActive = ['active'],
|
||||
): int {
|
||||
$qb = $this->buildFilterBaseQuery($queryString, $isActive);
|
||||
|
||||
try {
|
||||
return $qb
|
||||
->select('COUNT(si)')
|
||||
->getQuery()->getSingleScalarResult();
|
||||
} catch (NoResultException|NonUniqueResultException $e) {
|
||||
throw new \LogicException('a count query should return one result', previous: $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function buildQueryWithDesactivatedDateCriteria(): QueryBuilder
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('si');
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
:editor="ClassicEditor"
|
||||
:config="classicEditorConfig"
|
||||
:placeholder="trans(EVALUATION_COMMENT_PLACEHOLDER)"
|
||||
:value="comment"
|
||||
@input="$emit('update:comment', $event)"
|
||||
:model-value="comment"
|
||||
@update:model-value="$emit('update:comment', $event)"
|
||||
tag-name="textarea"
|
||||
></ckeditor>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
{% block admin_content %}
|
||||
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
||||
|
||||
{% block filter_order %}{{ filter_order|chill_render_filter_order_helper }}{% endblock %}
|
||||
|
||||
{% block table_entities_thead_tr %}
|
||||
<th>{{ 'Id'|trans }}</th>
|
||||
<th>{{ 'Title'|trans }}</th>
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
{% block admin_content %}
|
||||
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
||||
|
||||
{% block filter_order %}{{ filter_order|chill_render_filter_order_helper }}{% endblock %}
|
||||
|
||||
{% block table_entities_thead_tr %}
|
||||
<th>{{ 'Id'|trans }}</th>
|
||||
<th>{{ 'Title'|trans }}</th>
|
||||
@@ -22,7 +25,7 @@
|
||||
</td>
|
||||
<td>
|
||||
{% if entity.desactivationDate is not null %}
|
||||
{{ entity.desactivationDate|date('Y-m-d') }}
|
||||
{{ entity.desactivationDate|format_date('medium') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
{% block admin_content %}
|
||||
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
||||
|
||||
{% block filter_order %}{{ filter_order|chill_render_filter_order_helper }}{% endblock %}
|
||||
|
||||
{% block table_entities_thead_tr %}
|
||||
<th>{{ 'Id'|trans }}</th>
|
||||
<th>{{ 'Title'|trans }}</th>
|
||||
@@ -16,7 +19,7 @@
|
||||
<td>{{ entity.title|localize_translatable_string }}</td>
|
||||
<td>
|
||||
{% if entity.desactivationDate is not null %}
|
||||
{{ entity.desactivationDate|date('Y-m-d') }}
|
||||
{{ entity.desactivationDate|format_date('medium') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
{% block admin_content %}
|
||||
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
||||
|
||||
{% block filter_order %}{{ filter_order|chill_render_filter_order_helper }}{% endblock %}
|
||||
|
||||
{% block table_entities_thead_tr %}
|
||||
<th>{{ 'Id' }}</th>
|
||||
<th>{{ 'Title'|trans }}</th>
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
{% block admin_content %}
|
||||
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
||||
|
||||
{% block filter_order %}{{ filter_order|chill_render_filter_order_helper }}{% endblock %}
|
||||
|
||||
{% block table_entities_thead_tr %}
|
||||
<th>{{ 'Id'|trans }}</th>
|
||||
<th>{{ 'Title'|trans }}</th>
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Chill\PersonBundle\Security\Authorization\StoredObjectVoter;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
||||
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository;
|
||||
@@ -26,8 +27,9 @@ class AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter extends Abstract
|
||||
private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository,
|
||||
Security $security,
|
||||
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||
EntityWorkflowAttachmentRepository $attachmentRepository,
|
||||
) {
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
parent::__construct($security, $attachmentRepository, $workflowDocumentService);
|
||||
}
|
||||
|
||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||
|
||||
@@ -103,6 +103,11 @@ Employment status: Situation professionelle
|
||||
Administrative status: Situation administrative
|
||||
|
||||
person:
|
||||
# trans key according to new conventions
|
||||
export:
|
||||
aggregator:
|
||||
by_center:
|
||||
no_center: Sans territoire
|
||||
Identifiers: Identifiants
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user