Merge branch '111_exports_suite' into 641_issues_with_children

This commit is contained in:
Mathieu Jaumotte 2022-10-17 18:16:30 +02:00
commit 8928664f87
99 changed files with 3043 additions and 512 deletions

View File

@ -16,6 +16,8 @@ and this project adheres to
* [activity][export] DX/Feature: use of an `ActivityTypeRepositoryInterface` instead of the old-style EntityRepository * [activity][export] DX/Feature: use of an `ActivityTypeRepositoryInterface` instead of the old-style EntityRepository
* [person][export] Fixed: some inconsistency with date filter on accompanying courses * [person][export] Fixed: some inconsistency with date filter on accompanying courses
* [person][export] Fixed: use left join for related entities in accompanying course aggregators * [person][export] Fixed: use left join for related entities in accompanying course aggregators
* [workflow] Feature: allow user to copy and send manually the access link for the workflow
* [workflow] Feature: show the email addresses that received an access link for the workflow
## Test releases ## Test releases
@ -32,8 +34,12 @@ and this project adheres to
* [person-thirdparty]: fix quick-add of names that consist of multiple parts (eg. De Vlieger) within onthefly modal person/thirdparty * [person-thirdparty]: fix quick-add of names that consist of multiple parts (eg. De Vlieger) within onthefly modal person/thirdparty
* [search]: Order of birthdate fields changed in advanced search to avoid confusion. * [search]: Order of birthdate fields changed in advanced search to avoid confusion.
* [workflow]: Constraint added to workflow (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/675) * [workflow]: Constraint added to workflow (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/675)
* [social_action]: only show active objectives (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/625)
* [household]: Reposition and cut button for enfant hors menage have been deleted (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/620) * [household]: Reposition and cut button for enfant hors menage have been deleted (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/620)
* [admin]: Add crud for composition type in admin (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/611) * [admin]: Add crud for composition type in admin (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/611)
* [social_action]: only show active objectives (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/625)
## Test releases
### 2022-05-30 ### 2022-05-30

View File

@ -17,12 +17,13 @@ These are alias conventions :
| | Scope::class | acp.scopes | acpscope | | | Scope::class | acp.scopes | acpscope |
| | SocialIssue::class | acp.socialIssues | acpsocialissue | | | SocialIssue::class | acp.socialIssues | acpsocialissue |
| | User::class | acp.user | acpuser | | | User::class | acp.user | acpuser |
| | AccompanyingPeriopStepHistory::class | acp.stepHistories | acpstephistories |
| AccompanyingPeriodWork::class | | | acpw | | AccompanyingPeriodWork::class | | | acpw |
| | AccompanyingPeriodWorkEvaluation::class | acpw.accompanyingPeriodWorkEvaluations | workeval | | | AccompanyingPeriodWorkEvaluation::class | acpw.accompanyingPeriodWorkEvaluations | workeval |
| | User::class | acpw.referrers | acpwuser | | | User::class | acpw.referrers | acpwuser |
| | SocialAction::class | acpw.socialAction | acpwsocialaction | | | SocialAction::class | acpw.socialAction | acpwsocialaction |
| | Goal::class | acpw.goals | goal | | | Goal::class | acpw.goals | goal |
| | Result::class | acpw.results | result | | | Result::class | acpw.results | result |
| AccompanyingPeriodParticipation::class | | | acppart | | AccompanyingPeriodParticipation::class | | | acppart |
| | Person::class | acppart.person | partperson | | | Person::class | acppart.person | partperson |
| AccompanyingPeriodWorkEvaluation::class | | | workeval | | AccompanyingPeriodWorkEvaluation::class | | | workeval |
@ -47,7 +48,7 @@ These are alias conventions :
| | HouseholdComposition::class | household.compositions | composition | | | HouseholdComposition::class | household.compositions | composition |
| Activity::class | | | activity | | Activity::class | | | activity |
| | Person::class | activity.person | actperson | | | Person::class | activity.person | actperson |
| | AccompanyingPeriod::class | activity.accompanyingPeriod | acp | | | AccompanyingPeriod::class | activity.accompanyingPeriod | acp |
| | Person::class | activity\_person\_having\_activity.person | person\_person\_having\_activity | | | Person::class | activity\_person\_having\_activity.person | person\_person\_having\_activity |
| | ActivityReason::class | activity\_person\_having\_activity.reasons | reasons\_person\_having\_activity | | | ActivityReason::class | activity\_person\_having\_activity.reasons | reasons\_person\_having\_activity |
| | ActivityType::class | activity.activityType | acttype | | | ActivityType::class | activity.activityType | acttype |
@ -59,6 +60,7 @@ These are alias conventions :
| | User::class | activity.users | actusers | | | User::class | activity.users | actusers |
| | ActivityReason::class | activity.reasons | actreasons | | | ActivityReason::class | activity.reasons | actreasons |
| | Center::class | actperson.center | actcenter | | | Center::class | actperson.center | actcenter |
| | Person::class | activity.createdBy | actcreator |
| ActivityReason::class | | | actreasons | | ActivityReason::class | | | actreasons |
| | ActivityReasonCategory::class | actreason.category | actreasoncat | | | ActivityReasonCategory::class | actreason.category | actreasoncat |
| Calendar::class | | | cal | | Calendar::class | | | cal |

View File

@ -13,6 +13,10 @@ namespace Chill\ActivityBundle\Entity;
use Chill\ActivityBundle\Validator\Constraints as ActivityValidator; use Chill\ActivityBundle\Validator\Constraints as ActivityValidator;
use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable; use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Chill\MainBundle\Entity\Embeddable\PrivateCommentEmbeddable; use Chill\MainBundle\Entity\Embeddable\PrivateCommentEmbeddable;
@ -55,8 +59,12 @@ use Symfony\Component\Validator\Constraints as Assert;
* getUserFunction="getUser", * getUserFunction="getUser",
* path="scope") * path="scope")
*/ */
class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterface, HasCentersInterface, HasScopesInterface class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterface, HasCentersInterface, HasScopesInterface, TrackCreationInterface, TrackUpdateInterface
{ {
use TrackCreationTrait;
use TrackUpdateTrait;
public const SENTRECEIVED_RECEIVED = 'received'; public const SENTRECEIVED_RECEIVED = 'received';
public const SENTRECEIVED_SENT = 'sent'; public const SENTRECEIVED_SENT = 'sent';

View File

@ -13,20 +13,19 @@ namespace Chill\ActivityBundle\Export\Aggregator\ACPAggregators;
use Chill\ActivityBundle\Export\Declarations; use Chill\ActivityBundle\Export\Declarations;
use Chill\MainBundle\Export\AggregatorInterface; use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Repository\UserRepository; use Chill\MainBundle\Repository\UserRepositoryInterface;
use Chill\MainBundle\Templating\Entity\UserRender; use Chill\MainBundle\Templating\Entity\UserRender;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use function in_array;
class ByUserAggregator implements AggregatorInterface class ByCreatorAggregator implements AggregatorInterface
{ {
private UserRender $userRender; private UserRender $userRender;
private UserRepository $userRepository; private UserRepositoryInterface $userRepository;
public function __construct( public function __construct(
UserRepository $userRepository, UserRepositoryInterface $userRepository,
UserRender $userRender UserRender $userRender
) { ) {
$this->userRepository = $userRepository; $this->userRepository = $userRepository;
@ -40,12 +39,8 @@ class ByUserAggregator implements AggregatorInterface
public function alterQuery(QueryBuilder $qb, $data) public function alterQuery(QueryBuilder $qb, $data)
{ {
if (!in_array('actusers', $qb->getAllAliases(), true)) { $qb->addSelect('IDENTITY(activity.createdBy) AS creator_aggregator');
$qb->leftJoin('activity.users', 'actusers'); $qb->addGroupBy('creator_aggregator');
}
$qb->addSelect('actusers.id AS users_aggregator');
$qb->addGroupBy('users_aggregator');
} }
public function applyOn(): string public function applyOn(): string
@ -62,7 +57,7 @@ class ByUserAggregator implements AggregatorInterface
{ {
return function ($value): string { return function ($value): string {
if ('_header' === $value) { if ('_header' === $value) {
return 'Accepted users'; return 'Created by';
} }
if (null === $value) { if (null === $value) {
@ -77,11 +72,11 @@ class ByUserAggregator implements AggregatorInterface
public function getQueryKeys($data): array public function getQueryKeys($data): array
{ {
return ['users_aggregator']; return ['creator_aggregator'];
} }
public function getTitle(): string public function getTitle(): string
{ {
return 'Group activity by linked users'; return 'Group activity by creator';
} }
} }

View File

@ -19,7 +19,7 @@ use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use function in_array; use function in_array;
class UserScopeAggregator implements AggregatorInterface class CreatorScopeAggregator implements AggregatorInterface
{ {
private ScopeRepository $scopeRepository; private ScopeRepository $scopeRepository;
@ -40,12 +40,12 @@ class UserScopeAggregator implements AggregatorInterface
public function alterQuery(QueryBuilder $qb, $data) public function alterQuery(QueryBuilder $qb, $data)
{ {
if (!in_array('actuser', $qb->getAllAliases(), true)) { if (!in_array('actcreator', $qb->getAllAliases(), true)) {
$qb->leftJoin('activity.user', 'actuser'); $qb->leftJoin('activity.createdBy', 'actcreator');
} }
$qb->addSelect('IDENTITY(actuser.mainScope) AS userscope_aggregator'); $qb->addSelect('IDENTITY(actcreator.mainScope) AS creatorscope_aggregator');
$qb->addGroupBy('userscope_aggregator'); $qb->addGroupBy('creatorscope_aggregator');
} }
public function applyOn(): string public function applyOn(): string
@ -79,11 +79,11 @@ class UserScopeAggregator implements AggregatorInterface
public function getQueryKeys($data): array public function getQueryKeys($data): array
{ {
return ['userscope_aggregator']; return ['creatorscope_aggregator'];
} }
public function getTitle(): string public function getTitle(): string
{ {
return 'Group activity by userscope'; return 'Group activity by creator scope';
} }
} }

View File

@ -0,0 +1,86 @@
<?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\ActivityBundle\Export\Aggregator;
use Chill\ActivityBundle\Export\Declarations;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Chill\MainBundle\Templating\Entity\UserRender;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use function in_array;
class ActivityUsersAggregator implements AggregatorInterface
{
private UserRender $userRender;
private UserRepositoryInterface $userRepository;
public function __construct(UserRepositoryInterface $userRepository, UserRender $userRender)
{
$this->userRepository = $userRepository;
$this->userRender = $userRender;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
if (!in_array('actusers', $qb->getAllAliases(), true)) {
$qb->leftJoin('activity.users', 'actusers');
}
$qb
->addSelect('actusers.id AS activity_users_aggregator')
->addGroupBy('activity_users_aggregator');
}
public function applyOn(): string
{
return Declarations::ACTIVITY;
}
public function buildForm(FormBuilderInterface $builder)
{
// nothing to add on the form
}
public function getLabels($key, array $values, $data)
{
return function ($value) {
if ('_header' === $value) {
return 'Activity users';
}
if (null === $value) {
return '';
}
$u = $this->userRepository->find($value);
return $this->userRender->renderString($u, []);
};
}
public function getQueryKeys($data)
{
return ['activity_users_aggregator'];
}
public function getTitle()
{
return 'Aggregate by activity users';
}
}

View File

@ -0,0 +1,87 @@
<?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\ActivityBundle\Export\Aggregator;
use Chill\ActivityBundle\Export\Declarations;
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use function in_array;
class ActivityUsersJobAggregator implements \Chill\MainBundle\Export\AggregatorInterface
{
private TranslatableStringHelperInterface $translatableStringHelper;
private UserJobRepositoryInterface $userJobRepository;
public function __construct(UserJobRepositoryInterface $userJobRepository, TranslatableStringHelperInterface $translatableStringHelper)
{
$this->userJobRepository = $userJobRepository;
$this->translatableStringHelper = $translatableStringHelper;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
if (!in_array('actusers', $qb->getAllAliases(), true)) {
$qb->leftJoin('activity.users', 'actusers');
}
$qb
->addSelect('IDENTITY(actusers.userJob) AS activity_users_job_aggregator')
->addGroupBy('activity_users_job_aggregator');
}
public function applyOn()
{
return Declarations::ACTIVITY;
}
public function buildForm(FormBuilderInterface $builder)
{
// nothing to add in the form
}
public function getLabels($key, array $values, $data)
{
return function ($value): string {
if ('_header' === $value) {
return 'Users \'s job';
}
if (null === $value) {
return '';
}
$j = $this->userJobRepository->find($value);
return $this->translatableStringHelper->localize(
$j->getLabel()
);
};
}
public function getQueryKeys($data): array
{
return ['activity_users_job_aggregator'];
}
public function getTitle()
{
return 'Aggregate by users job';
}
}

View File

@ -0,0 +1,87 @@
<?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\ActivityBundle\Export\Aggregator;
use Chill\ActivityBundle\Export\Declarations;
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use function in_array;
class ActivityUsersScopeAggregator implements \Chill\MainBundle\Export\AggregatorInterface
{
private ScopeRepositoryInterface $scopeRepository;
private TranslatableStringHelperInterface $translatableStringHelper;
public function __construct(ScopeRepositoryInterface $scopeRepository, TranslatableStringHelperInterface $translatableStringHelper)
{
$this->scopeRepository = $scopeRepository;
$this->translatableStringHelper = $translatableStringHelper;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
if (!in_array('actusers', $qb->getAllAliases(), true)) {
$qb->leftJoin('activity.users', 'actusers');
}
$qb
->addSelect('IDENTITY(actusers.mainScope) AS activity_users_main_scope_aggregator')
->addGroupBy('activity_users_main_scope_aggregator');
}
public function applyOn()
{
return Declarations::ACTIVITY;
}
public function buildForm(FormBuilderInterface $builder)
{
// nothing to add in the form
}
public function getLabels($key, array $values, $data)
{
return function ($value): string {
if ('_header' === $value) {
return 'Users \'s scope';
}
if (null === $value) {
return '';
}
$s = $this->scopeRepository->find($value);
return $this->translatableStringHelper->localize(
$s->getName()
);
};
}
public function getQueryKeys($data): array
{
return ['activity_users_main_scope_aggregator'];
}
public function getTitle()
{
return 'Aggregate by users scope';
}
}

View File

@ -15,12 +15,10 @@ use Chill\ActivityBundle\Export\Declarations;
use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickUserDynamicType; use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Templating\Entity\UserRender; use Chill\MainBundle\Templating\Entity\UserRender;
use Doctrine\ORM\Query\Expr\Andx;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use function in_array;
class ByUserFilter implements FilterInterface class ByCreatorFilter implements FilterInterface
{ {
private UserRender $userRender; private UserRender $userRender;
@ -36,22 +34,11 @@ class ByUserFilter implements FilterInterface
public function alterQuery(QueryBuilder $qb, $data) public function alterQuery(QueryBuilder $qb, $data)
{ {
$where = $qb->getDQLPart('where'); $qb
->andWhere(
if (!in_array('actusers', $qb->getAllAliases(), true)) { $qb->expr()->in('activity.createdBy', ':users')
$qb->join('activity.users', 'actusers'); )
} ->setParameter('users', $data['accepted_users']);
$clause = $qb->expr()->in('actusers.id', ':users');
if ($where instanceof Andx) {
$where->add($clause);
} else {
$where = $qb->expr()->andX($clause);
}
$qb->add('where', $where);
$qb->setParameter('users', $data['accepted_users']);
} }
public function applyOn(): string public function applyOn(): string
@ -74,13 +61,13 @@ class ByUserFilter implements FilterInterface
$users[] = $this->userRender->renderString($u, []); $users[] = $this->userRender->renderString($u, []);
} }
return ['Filtered activity by linked users: only %users%', [ return ['Filtered activity by creator: only %users%', [
'%users%' => implode(', ou ', $users), '%users%' => implode(', ou ', $users),
]]; ]];
} }
public function getTitle(): string public function getTitle(): string
{ {
return 'Filter activity by linked users'; return 'Filter activity by creator';
} }
} }

View File

@ -0,0 +1,77 @@
<?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\ActivityBundle\Export\Filter;
use Chill\ActivityBundle\Export\Declarations;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Templating\Entity\UserRender;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class ActivityUsersFilter implements FilterInterface
{
private UserRender $userRender;
public function __construct(UserRender $userRender)
{
$this->userRender = $userRender;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$orX = $qb->expr()->orX();
foreach ($data['accepted_users'] as $key => $user) {
$orX->add($qb->expr()->isMemberOf(':activity_users_filter_u' . $key, 'activity.users'));
$qb->setParameter('activity_users_filter_u' . $key, $user);
}
$qb->andWhere($orX);
}
public function applyOn()
{
return Declarations::ACTIVITY;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('accepted_users', PickUserDynamicType::class, [
'multiple' => true,
'label' => 'Users',
]);
}
public function describeAction($data, $format = 'string')
{
$users = [];
foreach ($data['accepted_users'] as $u) {
$users[] = $this->userRender->renderString($u, []);
}
return ['Filtered activity by users: only %users%', [
'%users%' => implode(', ', $users),
]];
}
public function getTitle(): string
{
return 'Filter activity by users';
}
}

View File

@ -12,7 +12,7 @@ declare(strict_types=1);
namespace Chill\ActivityBundle\Tests\Export\Aggregator\ACPAggregators; namespace Chill\ActivityBundle\Tests\Export\Aggregator\ACPAggregators;
use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Export\Aggregator\ACPAggregators\ByUserAggregator; use Chill\ActivityBundle\Export\Aggregator\ACPAggregators\ByCreatorAggregator;
use Chill\MainBundle\Test\Export\AbstractAggregatorTest; use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@ -22,7 +22,7 @@ use Doctrine\ORM\EntityManagerInterface;
*/ */
final class ByUserAggregatorTest extends AbstractAggregatorTest final class ByUserAggregatorTest extends AbstractAggregatorTest
{ {
private ByUserAggregator $aggregator; private ByCreatorAggregator $aggregator;
protected function setUp(): void protected function setUp(): void
{ {

View File

@ -12,7 +12,7 @@ declare(strict_types=1);
namespace Chill\ActivityBundle\Tests\Export\Aggregator\ACPAggregators; namespace Chill\ActivityBundle\Tests\Export\Aggregator\ACPAggregators;
use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Export\Aggregator\ACPAggregators\UserScopeAggregator; use Chill\ActivityBundle\Export\Aggregator\ACPAggregators\CreatorScopeAggregator;
use Chill\MainBundle\Test\Export\AbstractAggregatorTest; use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@ -22,7 +22,7 @@ use Doctrine\ORM\EntityManagerInterface;
*/ */
final class UserScopeAggregatorTest extends AbstractAggregatorTest final class UserScopeAggregatorTest extends AbstractAggregatorTest
{ {
private UserScopeAggregator $aggregator; private CreatorScopeAggregator $aggregator;
protected function setUp(): void protected function setUp(): void
{ {

View File

@ -12,7 +12,7 @@ declare(strict_types=1);
namespace Chill\ActivityBundle\Tests\Export\Filter\ACPFilters; namespace Chill\ActivityBundle\Tests\Export\Filter\ACPFilters;
use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Export\Filter\ACPFilters\ByUserFilter; use Chill\ActivityBundle\Export\Filter\ACPFilters\ByCreatorFilter;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Test\Export\AbstractFilterTest; use Chill\MainBundle\Test\Export\AbstractFilterTest;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@ -23,7 +23,7 @@ use Doctrine\ORM\EntityManagerInterface;
*/ */
final class ByUserFilterTest extends AbstractFilterTest final class ByUserFilterTest extends AbstractFilterTest
{ {
private ByUserFilter $filter; private ByCreatorFilter $filter;
protected function setUp(): void protected function setUp(): void
{ {

View File

@ -55,6 +55,10 @@ services:
tags: tags:
- { name: chill.export_filter, alias: 'activity_date_filter' } - { name: chill.export_filter, alias: 'activity_date_filter' }
Chill\ActivityBundle\Export\Filter\ActivityUsersFilter:
tags:
- { name: chill.export_filter, alias: 'activity_users_filter' }
chill.activity.export.reason_filter: chill.activity.export.reason_filter:
class: Chill\ActivityBundle\Export\Filter\PersonFilters\ActivityReasonFilter class: Chill\ActivityBundle\Export\Filter\PersonFilters\ActivityReasonFilter
tags: tags:
@ -77,10 +81,9 @@ services:
tags: tags:
- { name: chill.export_filter, alias: 'activity_locationtype_filter' } - { name: chill.export_filter, alias: 'activity_locationtype_filter' }
chill.activity.export.byuser_filter: # TMS (M2M) Chill\ActivityBundle\Export\Filter\ACPFilters\ByCreatorFilter:
class: Chill\ActivityBundle\Export\Filter\ACPFilters\ByUserFilter
tags: tags:
- { name: chill.export_filter, alias: 'activity_byuser_filter' } - { name: chill.export_filter, alias: 'activity_bycreator_filter' }
chill.activity.export.emergency_filter: chill.activity.export.emergency_filter:
class: Chill\ActivityBundle\Export\Filter\ACPFilters\EmergencyFilter class: Chill\ActivityBundle\Export\Filter\ACPFilters\EmergencyFilter
@ -138,10 +141,9 @@ services:
tags: tags:
- { name: chill.export_aggregator, alias: activity_date_aggregator } - { name: chill.export_aggregator, alias: activity_date_aggregator }
chill.activity.export.byuser_aggregator: Chill\ActivityBundle\Export\Aggregator\ACPAggregators\ByCreatorAggregator:
class: Chill\ActivityBundle\Export\Aggregator\ACPAggregators\ByUserAggregator
tags: tags:
- { name: chill.export_aggregator, alias: activity_byuser_aggregator } - { name: chill.export_aggregator, alias: activity_by_creator_aggregator }
chill.activity.export.bythirdparty_aggregator: chill.activity.export.bythirdparty_aggregator:
class: Chill\ActivityBundle\Export\Aggregator\ACPAggregators\ByThirdpartyAggregator class: Chill\ActivityBundle\Export\Aggregator\ACPAggregators\ByThirdpartyAggregator
@ -158,7 +160,18 @@ services:
tags: tags:
- { name: chill.export_aggregator, alias: activity_bysocialissue_aggregator } - { name: chill.export_aggregator, alias: activity_bysocialissue_aggregator }
chill.activity.export.userscope_aggregator: Chill\ActivityBundle\Export\Aggregator\ACPAggregators\CreatorScopeAggregator:
class: Chill\ActivityBundle\Export\Aggregator\ACPAggregators\UserScopeAggregator
tags: tags:
- { name: chill.export_aggregator, alias: activity_userscope_aggregator } - { name: chill.export_aggregator, alias: activity_creator_scope_aggregator }
Chill\ActivityBundle\Export\Aggregator\ActivityUsersAggregator:
tags:
- { name: chill.export_aggregator, alias: activity_users_aggregator }
Chill\ActivityBundle\Export\Aggregator\ActivityUsersScopeAggregator:
tags:
- { name: chill.export_aggregator, alias: activity_users_scope_aggregator }
Chill\ActivityBundle\Export\Aggregator\ActivityUsersJobAggregator:
tags:
- { name: chill.export_aggregator, alias: activity_users_job_aggregator }

View File

@ -0,0 +1,59 @@
<?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\Migrations\Activity;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20221014130554 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE activity DROP updatedAt');
$this->addSql('ALTER TABLE activity DROP createdAt');
$this->addSql('ALTER TABLE activity DROP updatedBy_id');
$this->addSql('ALTER TABLE activity DROP createdBy_id');
// rename some indexes on activity
$this->addSql('ALTER INDEX idx_ac74095a217bbb47 RENAME TO idx_55026b0c217bbb47');
$this->addSql('ALTER INDEX idx_ac74095a682b5931 RENAME TO idx_55026b0c682b5931');
$this->addSql('ALTER INDEX idx_ac74095aa76ed395 RENAME TO idx_55026b0ca76ed395');
$this->addSql('ALTER INDEX idx_ac74095ac54c8c93 RENAME TO idx_55026b0cc54c8c93');
}
public function getDescription(): string
{
return 'Track update and create on activity';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE activity ADD updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
$this->addSql('ALTER TABLE activity ADD createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
$this->addSql('ALTER TABLE activity ADD updatedBy_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE activity ADD createdBy_id INT DEFAULT NULL');
$this->addSql('COMMENT ON COLUMN activity.updatedAt IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN activity.createdAt IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE activity ADD CONSTRAINT FK_AC74095A65FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE activity ADD CONSTRAINT FK_AC74095A3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX IDX_AC74095A65FF1AEC ON activity (updatedBy_id)');
$this->addSql('CREATE INDEX IDX_AC74095A3174800F ON activity (createdBy_id)');
// rename some indexes on activity
$this->addSql('ALTER INDEX idx_55026b0cc54c8c93 RENAME TO IDX_AC74095AC54C8C93');
$this->addSql('ALTER INDEX idx_55026b0c217bbb47 RENAME TO IDX_AC74095A217BBB47');
$this->addSql('ALTER INDEX idx_55026b0c682b5931 RENAME TO IDX_AC74095A682B5931');
$this->addSql('ALTER INDEX idx_55026b0ca76ed395 RENAME TO IDX_AC74095AA76ED395');
$this->addSql('UPDATE activity SET updatedBy_id=user_id, createdBy_id=user_id, createdAt="date", updatedAt="date"');
}
}

View File

@ -252,8 +252,6 @@ Filter by activity type: Filtrer les activités par type
Filter activity by locationtype: Filtrer les activités par type de localisation Filter activity by locationtype: Filtrer les activités par type de localisation
'Filtered activity by locationtype: only %types%': "Filtré par type de localisation: uniquement %types%" 'Filtered activity by locationtype: only %types%': "Filtré par type de localisation: uniquement %types%"
Accepted locationtype: Types de localisation Accepted locationtype: Types de localisation
Filter activity by linked users: Filtrer les activités par TMS
'Filtered activity by linked users: only %users%': "Filtré par TMS: uniquement %users%"
Accepted users: TMS(s) Accepted users: TMS(s)
Filter activity by emergency: Filtrer les activités par urgence Filter activity by emergency: Filtrer les activités par urgence
'Filtered activity by emergency: only %emergency%': "Filtré par urgence: uniquement si %emergency%" 'Filtered activity by emergency: only %emergency%': "Filtré par urgence: uniquement si %emergency%"
@ -269,7 +267,11 @@ Filter activity by linked socialaction: Filtrer les activités par action liée
Filter activity by linked socialissue: Filtrer les activités par problématique liée Filter activity by linked socialissue: Filtrer les activités par problématique liée
'Filtered activity by linked socialissue: only %issues%': "Filtré par problématique liée: uniquement %issues%" 'Filtered activity by linked socialissue: only %issues%': "Filtré par problématique liée: uniquement %issues%"
Filter activity by user: Filtrer les activités par créateur Filter activity by user: Filtrer les activités par créateur
'Filtered activity by user: only %users%': "Filtré par créateur: uniquement %users%" Filter activity by users: Filtrer les activités par utilisateur participant
Filter activity by creator: Filtrer les activités par créateur de l'échange
'Filtered activity by user: only %users%': "Filtré par référent: uniquement %users%"
'Filtered activity by users: only %users%': "Filtré par utilisateurs participants: uniquement %users%"
'Filtered activity by creator: only %users%': "Filtré par créateur: uniquement %users%"
Creators: Créateurs Creators: Créateurs
Filter activity by userscope: Filtrer les activités par service du créateur Filter activity by userscope: Filtrer les activités par service du créateur
'Filtered activity by userscope: only %scopes%': "Filtré par service du créateur: uniquement %scopes%" 'Filtered activity by userscope: only %scopes%': "Filtré par service du créateur: uniquement %scopes%"
@ -282,9 +284,14 @@ By reason: Par sujet
By category of reason: Par catégorie de sujet By category of reason: Par catégorie de sujet
Reason's level: Niveau du sujet Reason's level: Niveau du sujet
Group by reasons: Sujet d'activité Group by reasons: Sujet d'activité
Aggregate by activity user: Grouper les activités par utilisateur Aggregate by activity user: Grouper les activités par référent
Aggregate by activity users: Grouper les activités par utilisateurs participants
Aggregate by activity type: Grouper les activités par type Aggregate by activity type: Grouper les activités par type
Aggregate by activity reason: Grouper les activités par sujet Aggregate by activity reason: Grouper les activités par sujet
Aggregate by users scope: Grouper les activités par service principal de l'utilisateur
Users 's scope: Service principal des utilisateurs participants à l'activité
Aggregate by users job: Grouper les activités par métier des utilisateurs participants
Users 's job: Métier des utilisateurs participants à l'activité
Group activity by locationtype: Grouper les activités par type de localisation Group activity by locationtype: Grouper les activités par type de localisation
Group activity by date: Grouper les activités par date Group activity by date: Grouper les activités par date
@ -294,7 +301,8 @@ by week: Par semaine
for week: Semaine for week: Semaine
by year: Par année by year: Par année
in year: En in year: En
Group activity by linked users: Grouper les activités par TMS impliqué Group activity by creator: Grouper les activités par créateur de l'échange
Group activity by creator scope: Grouper les activités par service du créateur de l'échange
Group activity by linked thirdparties: Grouper les activités par tiers impliqué Group activity by linked thirdparties: Grouper les activités par tiers impliqué
Accepted thirdparty: Tiers impliqué Accepted thirdparty: Tiers impliqué
Group activity by linked socialaction: Grouper les activités par action liée Group activity by linked socialaction: Grouper les activités par action liée

View File

@ -14,7 +14,7 @@ namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController; use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\CountryRepository; use Chill\MainBundle\Repository\CountryRepository;
use Chill\MainBundle\Repository\PostalCodeRepository; use Chill\MainBundle\Repository\PostalCodeRepositoryInterface;
use Chill\MainBundle\Serializer\Model\Collection; use Chill\MainBundle\Serializer\Model\Collection;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@ -30,11 +30,11 @@ final class PostalCodeAPIController extends ApiController
private PaginatorFactory $paginatorFactory; private PaginatorFactory $paginatorFactory;
private PostalCodeRepository $postalCodeRepository; private PostalCodeRepositoryInterface $postalCodeRepository;
public function __construct( public function __construct(
CountryRepository $countryRepository, CountryRepository $countryRepository,
PostalCodeRepository $postalCodeRepository, PostalCodeRepositoryInterface $postalCodeRepository,
PaginatorFactory $paginatorFactory PaginatorFactory $paginatorFactory
) { ) {
$this->countryRepository = $countryRepository; $this->countryRepository = $countryRepository;

View File

@ -22,6 +22,7 @@ use Chill\MainBundle\Controller\UserController;
use Chill\MainBundle\Controller\UserJobApiController; use Chill\MainBundle\Controller\UserJobApiController;
use Chill\MainBundle\Controller\UserJobController; use Chill\MainBundle\Controller\UserJobController;
use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface; use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface;
use Chill\MainBundle\Doctrine\DQL\Age;
use Chill\MainBundle\Doctrine\DQL\Extract; use Chill\MainBundle\Doctrine\DQL\Extract;
use Chill\MainBundle\Doctrine\DQL\GetJsonFieldByKey; use Chill\MainBundle\Doctrine\DQL\GetJsonFieldByKey;
use Chill\MainBundle\Doctrine\DQL\JsonAggregate; use Chill\MainBundle\Doctrine\DQL\JsonAggregate;
@ -243,6 +244,7 @@ class ChillMainExtension extends Extension implements
'datetime_functions' => [ 'datetime_functions' => [
'EXTRACT' => Extract::class, 'EXTRACT' => Extract::class,
'TO_CHAR' => ToChar::class, 'TO_CHAR' => ToChar::class,
'AGE' => Age::class,
], ],
], ],
'hydrators' => [ 'hydrators' => [

View File

@ -0,0 +1,54 @@
<?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\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
class Age extends FunctionNode
{
private $value1;
private $value2;
public function getSql(SqlWalker $sqlWalker)
{
if (null !== $this->value2) {
return sprintf(
'AGE(%s, %s)',
$this->value1->dispatch($sqlWalker),
$this->value2->dispatch($sqlWalker)
);
}
return sprintf(
'AGE(%s)',
$this->value1->dispatch($sqlWalker),
);
}
public function parse(Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->value1 = $parser->SimpleArithmeticExpression();
$parser->match(Lexer::T_COMMA);
$this->value2 = $parser->SimpleArithmeticExpression();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
}

View File

@ -28,6 +28,11 @@ use Symfony\Component\Serializer\Annotation\Groups;
*/ */
class Scope class Scope
{ {
/**
* @ORM\Column(type="boolean", nullable=false, options={"default": true})
*/
private bool $active = true;
/** /**
* @ORM\Id * @ORM\Id
* @ORM\Column(name="id", type="integer") * @ORM\Column(name="id", type="integer")
@ -88,6 +93,18 @@ class Scope
return $this->roleScopes; return $this->roleScopes;
} }
public function isActive(): bool
{
return $this->active;
}
public function setActive(bool $active): Scope
{
$this->active = $active;
return $this;
}
/** /**
* @param $name * @param $name
* *

View File

@ -0,0 +1,55 @@
<?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\Form\Type\DataTransformer;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Repository\PostalCodeRepositoryInterface;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use function gettype;
use function is_int;
class PostalCodeToIdTransformer implements DataTransformerInterface
{
private PostalCodeRepositoryInterface $postalCodeRepository;
public function __construct(PostalCodeRepositoryInterface $postalCodeRepository)
{
$this->postalCodeRepository = $postalCodeRepository;
}
public function reverseTransform($value)
{
if (null === $value || trim('') === $value) {
return null;
}
if (!is_int((int) $value)) {
throw new TransformationFailedException('Cannot transform ' . gettype($value));
}
return $this->postalCodeRepository->find((int) $value);
}
public function transform($value)
{
if (null === $value) {
return null;
}
if ($value instanceof PostalCode) {
return $value->getId();
}
throw new TransformationFailedException('Could not reverseTransform ' . gettype($value));
}
}

View File

@ -0,0 +1,49 @@
<?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\Form\Type;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Form\Type\DataTransformer\PostalCodeToIdTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PickPostalCodeType extends AbstractType
{
private PostalCodeToIdTransformer $postalCodeToIdTransformer;
public function __construct(PostalCodeToIdTransformer $postalCodeToIdTransformer)
{
$this->postalCodeToIdTransformer = $postalCodeToIdTransformer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer($this->postalCodeToIdTransformer);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['uniqid'] = $view->vars['attr']['data-input-postal-code'] = uniqid('input_pick_postal_code_');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefault('class', PostalCode::class)
->setDefault('multiple', false)
->setAllowedTypes('multiple', ['bool'])
->setDefault('compound', false);
}
}

View File

@ -15,9 +15,9 @@ use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\DataMapper\ScopePickerDataMapper; use Chill\MainBundle\Form\DataMapper\ScopePickerDataMapper;
use Chill\MainBundle\Repository\ScopeRepository;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface; use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use RuntimeException;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\HiddenType;
@ -26,11 +26,9 @@ use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView; use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Role\Role; use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Security\Core\Security;
use function array_map; use Symfony\Component\Security\Core\Security;
use function count; use function count;
/** /**
@ -44,47 +42,37 @@ use function count;
*/ */
class ScopePickerType extends AbstractType class ScopePickerType extends AbstractType
{ {
protected AuthorizationHelperInterface $authorizationHelper; private AuthorizationHelperInterface $authorizationHelper;
/** private Security $security;
* @var ScopeRepository
*/
protected $scopeRepository;
protected Security $security; private TranslatableStringHelperInterface $translatableStringHelper;
/**
* @var TokenStorageInterface
*/
protected $tokenStorage;
/**
* @var TranslatableStringHelper
*/
protected $translatableStringHelper;
public function __construct( public function __construct(
AuthorizationHelperInterface $authorizationHelper, AuthorizationHelperInterface $authorizationHelper,
TokenStorageInterface $tokenStorage,
ScopeRepository $scopeRepository,
Security $security, Security $security,
TranslatableStringHelper $translatableStringHelper TranslatableStringHelperInterface $translatableStringHelper
) { ) {
$this->authorizationHelper = $authorizationHelper; $this->authorizationHelper = $authorizationHelper;
$this->tokenStorage = $tokenStorage;
$this->scopeRepository = $scopeRepository;
$this->security = $security; $this->security = $security;
$this->translatableStringHelper = $translatableStringHelper; $this->translatableStringHelper = $translatableStringHelper;
} }
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options)
{ {
$items = $this->authorizationHelper->getReachableScopes( $items = array_filter(
$this->security->getUser(), $this->authorizationHelper->getReachableScopes(
$options['role'] instanceof Role ? $options['role']->getRole() : $options['role'], $this->security->getUser(),
$options['center'] $options['role'] instanceof Role ? $options['role']->getRole() : $options['role'],
$options['center']
),
static function (Scope $s) { return $s->isActive(); }
); );
if (0 === count($items)) {
throw new RuntimeException('no scopes are reachable. This form should not be shown to user');
}
if (1 !== count($items)) { if (1 !== count($items)) {
$builder->add('scope', EntityType::class, [ $builder->add('scope', EntityType::class, [
'class' => Scope::class, 'class' => Scope::class,
@ -123,35 +111,4 @@ class ScopePickerType extends AbstractType
->setRequired('role') ->setRequired('role')
->setAllowedTypes('role', ['string', Role::class]); ->setAllowedTypes('role', ['string', Role::class]);
} }
/**
* @param array|Center|Center[] $center
* @param string $role
*
* @return \Doctrine\ORM\QueryBuilder
*/
protected function buildAccessibleScopeQuery($center, $role)
{
$roles = $this->authorizationHelper->getParentRoles($role);
$roles[] = $role;
$centers = $center instanceof Center ? [$center] : $center;
$qb = $this->scopeRepository->createQueryBuilder('s');
$qb
// jointure to center
->join('s.roleScopes', 'rs')
->join('rs.permissionsGroups', 'pg')
->join('pg.groupCenters', 'gc')
// add center constraint
->where($qb->expr()->in('IDENTITY(gc.center)', ':centers'))
->setParameter('centers', array_map(static fn (Center $c) => $c->getId(), $centers))
// role constraints
->andWhere($qb->expr()->in('rs.role', ':roles'))
->setParameter('roles', $roles)
// user contraint
->andWhere(':user MEMBER OF gc.users')
->setParameter('user', $this->tokenStorage->getToken()->getUser());
return $qb;
}
} }

View File

@ -18,10 +18,9 @@ use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\ResultSetMapping; use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\Persistence\ObjectRepository;
use RuntimeException; use RuntimeException;
final class PostalCodeRepository implements ObjectRepository final class PostalCodeRepository implements PostalCodeRepositoryInterface
{ {
private EntityManagerInterface $entityManager; private EntityManagerInterface $entityManager;
@ -29,7 +28,7 @@ final class PostalCodeRepository implements ObjectRepository
public function __construct(EntityManagerInterface $entityManager) public function __construct(EntityManagerInterface $entityManager)
{ {
$this->repository = $entityManager->getRepository(PostalCode::class); $this->repository = $entityManager->getRepository($this->getClassName());
$this->entityManager = $entityManager; $this->entityManager = $entityManager;
} }
@ -51,20 +50,11 @@ final class PostalCodeRepository implements ObjectRepository
return $this->repository->find($id, $lockMode, $lockVersion); return $this->repository->find($id, $lockMode, $lockVersion);
} }
/**
* @return PostalCode[]
*/
public function findAll(): array public function findAll(): array
{ {
return $this->repository->findAll(); return $this->repository->findAll();
} }
/**
* @param mixed|null $limit
* @param mixed|null $offset
*
* @return PostalCode[]
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
{ {
return $this->repository->findBy($criteria, $orderBy, $limit, $offset); return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
@ -95,7 +85,7 @@ final class PostalCodeRepository implements ObjectRepository
return $this->repository->findOneBy($criteria, $orderBy); return $this->repository->findOneBy($criteria, $orderBy);
} }
public function getClassName() public function getClassName(): string
{ {
return PostalCode::class; return PostalCode::class;
} }

View File

@ -0,0 +1,42 @@
<?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\Repository;
use Chill\MainBundle\Entity\Country;
use Chill\MainBundle\Entity\PostalCode;
use Doctrine\Persistence\ObjectRepository;
interface PostalCodeRepositoryInterface extends ObjectRepository
{
public function countByPattern(string $pattern, ?Country $country): int;
public function find($id, $lockMode = null, $lockVersion = null): ?PostalCode;
/**
* @return PostalCode[]
*/
public function findAll(): array;
/**
* @param mixed|null $limit
* @param mixed|null $offset
*
* @return PostalCode[]
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array;
public function findByPattern(string $pattern, ?Country $country, ?int $start = 0, ?int $limit = 50): array;
public function findOneBy(array $criteria, ?array $orderBy = null): ?PostalCode;
public function getClassName(): string;
}

View File

@ -43,6 +43,15 @@ final class ScopeRepository implements ScopeRepositoryInterface
return $this->repository->findAll(); return $this->repository->findAll();
} }
public function findAllActive(): array
{
$qb = $this->repository->createQueryBuilder('s');
$qb->where('s.active = \'TRUE\'');
return $qb->getQuery()->getResult();
}
/** /**
* @param mixed|null $limit * @param mixed|null $limit
* @param mixed|null $offset * @param mixed|null $offset

View File

@ -22,10 +22,15 @@ interface ScopeRepositoryInterface extends ObjectRepository
public function find($id, $lockMode = null, $lockVersion = null): ?Scope; public function find($id, $lockMode = null, $lockVersion = null): ?Scope;
/** /**
* @return Scope[] * @return array|Scope[]
*/ */
public function findAll(): array; public function findAll(): array;
/**
* @return array|Scope[]
*/
public function findAllActive(): array;
/** /**
* @param null|mixed $limit * @param null|mixed $limit
* @param null|mixed $offset * @param null|mixed $offset

View File

@ -14,9 +14,8 @@ namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Entity\UserJob;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
class UserJobRepository implements ObjectRepository class UserJobRepository implements UserJobRepositoryInterface
{ {
private EntityRepository $repository; private EntityRepository $repository;
@ -38,6 +37,11 @@ class UserJobRepository implements ObjectRepository
return $this->repository->findAll(); return $this->repository->findAll();
} }
public function findAllActive(): array
{
return $this->repository->findBy(['active' => true]);
}
/** /**
* @param mixed|null $limit * @param mixed|null $limit
* @param mixed|null $offset * @param mixed|null $offset
@ -49,12 +53,12 @@ class UserJobRepository implements ObjectRepository
return $this->repository->findBy($criteria, $orderBy, $limit, $offset); return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
} }
public function findOneBy(array $criteria) public function findOneBy(array $criteria): ?UserJob
{ {
return $this->repository->findOneBy($criteria); return $this->repository->findOneBy($criteria);
} }
public function getClassName() public function getClassName(): string
{ {
return UserJob::class; return UserJob::class;
} }

View File

@ -0,0 +1,42 @@
<?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\Repository;
use Chill\MainBundle\Entity\UserJob;
use Doctrine\Persistence\ObjectRepository;
interface UserJobRepositoryInterface extends ObjectRepository
{
public function find($id): ?UserJob;
/**
* @return array|UserJob[]
*/
public function findAll(): array;
/**
* @return array|UserJob[]
*/
public function findAllActive(): array;
/**
* @param mixed|null $limit
* @param mixed|null $offset
*
* @return array|object[]|UserJob[]
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null);
public function findOneBy(array $criteria): ?UserJob;
public function getClassName(): string;
}

View File

@ -16,11 +16,10 @@ use Chill\MainBundle\Entity\User;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
use function count; use function count;
final class UserRepository implements ObjectRepository final class UserRepository implements UserRepositoryInterface
{ {
private EntityManagerInterface $entityManager; private EntityManagerInterface $entityManager;
@ -171,7 +170,7 @@ final class UserRepository implements ObjectRepository
return $qb->getQuery()->getResult(); return $qb->getQuery()->getResult();
} }
public function getClassName() public function getClassName(): string
{ {
return User::class; return User::class;
} }

View File

@ -0,0 +1,64 @@
<?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\Repository;
use Chill\MainBundle\Entity\User;
use Doctrine\Persistence\ObjectRepository;
interface UserRepositoryInterface extends ObjectRepository
{
public function countBy(array $criteria): int;
public function countByActive(): int;
public function countByUsernameOrEmail(string $pattern): int;
public function find($id, $lockMode = null, $lockVersion = null): ?User;
/**
* @return User[]
*/
public function findAll(): array;
/**
* @param mixed|null $limit
* @param mixed|null $offset
*
* @return User[]
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array;
/**
* @return array|User[]
*/
public function findByActive(?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
public function findByUsernameOrEmail(string $pattern, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array;
public function findOneBy(array $criteria, ?array $orderBy = null): ?User;
public function findOneByUsernameOrEmail(string $pattern);
/**
* Get the users having a specific flags.
*
* If provided, only the users amongst "filtered users" are searched. This
* allows to make a first search amongst users based on role and center
* and, then filter those users having some flags.
*
* @param \Chill\MainBundle\Entity\User[] $amongstUsers
* @param mixed $flag
*/
public function findUsersHavingFlags($flag, array $amongstUsers = []): array;
public function getClassName(): string;
}

View File

@ -0,0 +1,60 @@
import { createApp } from 'vue';
import PickPostalCode from 'ChillMainAssets/vuejs/PickPostalCode/PickPostalCode';
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n';
import { appMessages } from 'ChillMainAssets/vuejs/PickEntity/i18n';
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
const i18n = _createI18n(appMessages);
function loadOnePicker(el, input, uniqId, city) {
const app = createApp({
template: '<pick-postal-code @select-city="onCitySelected" @removeCity="onCityRemoved" :picked="city"></pick-postal-code>',
components: {
PickPostalCode,
},
data() {
return {
city: city,
}
},
methods: {
onCitySelected(city) {
this.city = city;
input.value = city.id;
},
onCityRemoved(city) {
this.city = null;
input.value = '';
}
}
})
.use(i18n)
.mount(el);
}
function loadDynamicPickers(element) {
let apps = element.querySelectorAll('[data-module="pick-postal-code"]');
apps.forEach(function(el) {
const
uniqId = el.dataset.uniqid,
input = document.querySelector(`input[data-input-uniqid="${uniqId}"]`),
cityIdValue = input.value === '' ? null : input.value
;
if (cityIdValue !== null) {
makeFetch('GET', `/api/1.0/main/postal-code/${cityIdValue}.json`).then(city => {
loadOnePicker(el, input, uniqId, city);
})
} else {
loadOnePicker(el, input, uniqId, null);
}
});
}
document.addEventListener('DOMContentLoaded', function(e) {
loadDynamicPickers(document)
})

View File

@ -0,0 +1,29 @@
# Pickpostalcode
Allow to pick a postal code.
In use with module `mod_pick_postal_code`, associated with `PickPostalCodeType` in php.
## Usage
`<pick-postal-code @select-city="onCitySelected" @removeCity="onCityRemoved" :picked="city"></pick-postal-code>`
## Props
* `picked`: the city picked. A javascript object (a city). Null if empty.
* `country`: country to restraint search on picked. May be null.
## Emits
### `selectCity`
When a city is onCitySelected.
Argument: a js object, representing a city
### `removeCity`
When a city is removed.
Argument: a js object, representing a city

View File

@ -0,0 +1,107 @@
<template>
<div class="PickPostalCode">
<vue-multiselect
id="citySelector"
@search-change="listenInputSearch"
ref="citySelector"
v-model="internalPicked"
@select="selectCity"
@remove="remove"
name=""
track-by="id"
label="value"
:custom-label="transName"
:placeholder="$t('select_city')"
:select-label="$t('multiselect.select_label')"
:deselect-label="$t('multiselect.deselect_label')"
:selected-label="$t('multiselect.selected_label')"
:taggable="true"
:multiple="false"
:internal-search="false"
:loading="isLoading"
:options="cities"></vue-multiselect>
</div>
</template>
<script lang="js">
import VueMultiselect from "vue-multiselect";
import {reactive, defineProps, onMounted} from "vue";
import {fetchCities, searchCities} from "./api";
export default {
components: {
VueMultiselect,
},
data() {
return {
cities: [],
internalPicked: null,
isLoading: false,
abortControllers: [],
}
},
emits: ['pickCity', 'removeCity'],
props: {
picked: {
type: Object,
required: false,
default: null
},
country: {
type: Object,
required: false,
default: null
}
},
mounted() {
if (this.picked !== null) {
this.internalPicked = this.picked;
this.cities.push(this.picked);
}
},
methods: {
transName(value) {
return (value.code && value.name) ? `${value.name} (${value.code})` : '';
},
selectCity(city) {
this.$emit('selectCity', city);
},
listenInputSearch(query) {
if (query.length <= 2) {
return;
}
let c = this.abortControllers.pop();
while (typeof c !== 'undefined') {
c.abort();
c = this.abortControllers.pop();
}
this.isLoading = true;
let controller = new AbortController();
this.abortControllers.push(controller);
searchCities(query, this.country, controller).then(
newCities => {
this.cities = this.cities.filter(city => city.id === this.picked);
newCities.forEach(item => {
this.cities.push(item);
})
this.isLoading = false;
return Promise.resolve();
})
.catch((error) => {
console.log(error); //TODO better error handling
this.isLoading = false;
});
},
remove(item) {
this.$emit('removeCity', item);
}
},
}
</script>

View File

@ -0,0 +1,3 @@
.PickPostalCode {
}

View File

@ -0,0 +1,43 @@
import {makeFetch, fetchResults} from 'ChillMainAssets/lib/api/apiMethods';
/**
* Endpoint chill_api_single_postal_code__index
* method GET, get Cities Object
* @params {object} a country object
* @returns {Promise} a promise containing all Postal Code objects filtered with country
*/
const fetchCities = (country) => {
// warning: do not use fetchResults (in apiMethods): we need only a **part** of the results in the db
const params = new URLSearchParams({item_per_page: 100});
if (country !== null) {
params.append('country', country.id);
}
return makeFetch('GET', `/api/1.0/main/postal-code.json?${params.toString()}`).then(r => Promise.resolve(r.results));
};
/**
* Endpoint chill_main_postalcodeapi_search
* method GET, get Cities Object
* @params {string} search a search string
* @params {object} country a country object
* @params {AbortController} an abort controller
* @returns {Promise} a promise containing all Postal Code objects filtered with country and a search string
*/
const searchCities = (search, country, controller) => {
const url = '/api/1.0/main/postal-code/search.json?';
const params = new URLSearchParams({q: search});
if (country !== null) {
Object.assign('country', country.id);
}
return makeFetch('GET', url + params, null, {signal: controller.signal})
.then(result => Promise.resolve(result.results));
};
export {
fetchCities,
searchCities,
};

View File

@ -238,3 +238,9 @@
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}"/> <input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}"/>
<div data-module="pick-dynamic" data-types="{{ form.vars['types']|json_encode }}" data-multiple="{{ form.vars['multiple'] }}" data-uniqid="{{ form.vars['uniqid'] }}"></div> <div data-module="pick-dynamic" data-types="{{ form.vars['types']|json_encode }}" data-multiple="{{ form.vars['multiple'] }}" data-uniqid="{{ form.vars['uniqid'] }}"></div>
{% endblock %} {% endblock %}
{% block pick_postal_code_widget %}
{{ form_help(form)}}
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}"/>
<div data-module="pick-postal-code" data-uniqid="{{ form.vars['uniqid'] }}"></div>
{% endblock %}

View File

@ -95,6 +95,15 @@
</ul> </ul>
{% endif %} {% endif %}
{% if entity_workflow.currentStep.destEmail|length > 0 %}
<p><b>{{ 'workflow.An access key was also sent to those addresses'|trans }}&nbsp;:</b></p>
<ul>
{% for e in entity_workflow.currentStep.destEmail -%}
<li><a href="mailto:{{ e|escape('html_attr') }}">{{ e }}</a></li>
{%- endfor %}
</ul>
{% endif %}
{% if entity_workflow.currentStep.destUserByAccessKey|length > 0 %} {% if entity_workflow.currentStep.destUserByAccessKey|length > 0 %}
<p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }}&nbsp;:</b></p> <p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }}&nbsp;:</b></p>
<ul> <ul>
@ -103,6 +112,21 @@
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
{% if is_granted('CHILL_MAIN_WORKFLOW_LINK_SHOW', entity_workflow) %}
<p><b>{{ 'workflow.This link grant any user to apply a transition'|trans }}&nbsp;:</b></p>
{% set link = absolute_url(path('chill_main_workflow_grant_access_by_key', {'id': entity_workflow.currentStep.id, 'accessKey': entity_workflow.currentStep.accessKey})) %}
<div class="input-group mb-3">
<input type="text" readonly value="{{ link|e('html_attr') }}" class="form-control">
<button class="btn btn-secondary" type="button" id="button-copy" onclick="navigator.clipboard.writeText('{{ link|e('html_attr') }}').then(() => { window.alert({{ ('"' ~ 'workflow.Access link copied'|trans ~ ' !"') |e('html_attr') }})});"><i class="fa fa-files-o"></i></button>
<a class="btn btn-secondary" type="button" id="button-email"
href="mailto:?body={{ ((('workflow.The workflow may be accssed through this link'|trans)~':')|e('url')) ~ '%0D%0A%0D%0A' ~ link|e('url') }}"><i class="fa fa-envelope"></i></a>
</div>
{% endif %}
{% endif %} {% endif %}
</div> </div>

View File

@ -81,6 +81,15 @@
</ul> </ul>
{% endif %} {% endif %}
{% if entity_workflow.currentStep.destEmail|length > 0 %}
<p><b>{{ 'workflow.An access key was also sent to those addresses'|trans }}&nbsp;:</b></p>
<ul>
{% for e in entity_workflow.currentStep.destEmail -%}
<li><a href="mailto:{{ e|escape('html_attr') }}">{{ e }}</a></li>
{%- endfor %}
</ul>
{% endif %}
{% if step.destUserByAccessKey|length > 0 %} {% if step.destUserByAccessKey|length > 0 %}
<p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }}&nbsp;:</b></p> <p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }}&nbsp;:</b></p>
<ul> <ul>

View File

@ -27,6 +27,8 @@ class EntityWorkflowVoter extends Voter
public const SEE = 'CHILL_MAIN_WORKFLOW_SEE'; public const SEE = 'CHILL_MAIN_WORKFLOW_SEE';
public const SHOW_ENTITY_LINK = 'CHILL_MAIN_WORKFLOW_LINK_SHOW';
private EntityWorkflowManager $manager; private EntityWorkflowManager $manager;
private Security $security; private Security $security;
@ -80,6 +82,19 @@ class EntityWorkflowVoter extends Voter
case self::DELETE: case self::DELETE:
return $subject->getStep() === 'initial'; return $subject->getStep() === 'initial';
case self::SHOW_ENTITY_LINK:
if ($subject->getStep() === 'initial') {
return false;
}
$currentStep = $subject->getCurrentStepChained();
if ($currentStep->isFinal()) {
return false;
}
return $currentStep->getPrevious()->getTransitionBy() === $this->security->getUser();
default: default:
throw new UnexpectedValueException("attribute {$attribute} not supported"); throw new UnexpectedValueException("attribute {$attribute} not supported");
} }
@ -91,6 +106,7 @@ class EntityWorkflowVoter extends Voter
self::SEE, self::SEE,
self::CREATE, self::CREATE,
self::DELETE, self::DELETE,
self::SHOW_ENTITY_LINK,
]; ];
} }
} }

View File

@ -108,12 +108,9 @@ abstract class AbstractAggregatorTest extends KernelTestCase
abstract public function getQueryBuilders(); abstract public function getQueryBuilders();
/** /**
* Compare aliases array before and after that aggregator alter query * Compare aliases array before and after that aggregator alter query.
* *
* @dataProvider dataProviderAliasDidNotDisappears * @dataProvider dataProviderAliasDidNotDisappears
*
* @param QueryBuilder $qb
* @param array $data
* @return void * @return void
*/ */
public function testAliasDidNotDisappears(QueryBuilder $qb, array $data) public function testAliasDidNotDisappears(QueryBuilder $qb, array $data)

View File

@ -100,12 +100,9 @@ abstract class AbstractFilterTest extends KernelTestCase
abstract public function getQueryBuilders(); abstract public function getQueryBuilders();
/** /**
* Compare aliases array before and after that filter alter query * Compare aliases array before and after that filter alter query.
* *
* @dataProvider dataProviderAliasDidNotDisappears * @dataProvider dataProviderAliasDidNotDisappears
*
* @param QueryBuilder $qb
* @param array $data
* @return void * @return void
*/ */
public function testAliasDidNotDisappears(QueryBuilder $qb, array $data) public function testAliasDidNotDisappears(QueryBuilder $qb, array $data)

View File

@ -17,6 +17,7 @@ namespace Chill\MainBundle\Test;
* **Usage : ** You must set up trait with `setUpTrait` before use * **Usage : ** You must set up trait with `setUpTrait` before use
* and use tearDownTrait after usage. * and use tearDownTrait after usage.
* *
* @deprecated use @see{\Prophecy\PhpUnit\ProphecyTrait} instead
* @codeCoverageIgnore * @codeCoverageIgnore
* *
* @deprecated use @class{Prophecy\PhpUnit\ProphecyTrait} instead * @deprecated use @class{Prophecy\PhpUnit\ProphecyTrait} instead

View File

@ -0,0 +1,79 @@
<?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\Tests\Doctrine\DQL;
use Chill\MainBundle\Entity\Address;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @internal
* @coversNothing
*/
final class AgeTest extends KernelTestCase
{
private EntityManagerInterface $entityManager;
protected function setUp(): void
{
self::bootKernel();
$this->entityManager = self::$container->get(EntityManagerInterface::class);
}
public function generateQueries(): iterable
{
yield [
'SELECT AGE(a.validFrom, a.validTo) FROM ' . Address::class . ' a',
[],
];
yield [
'SELECT AGE(:date0, :date1) FROM ' . Address::class . ' a',
[
'date0' => new DateTimeImmutable('now'),
'date1' => new DateTimeImmutable('2020-01-01'),
],
];
yield [
'SELECT AGE(a.validFrom, :date1) FROM ' . Address::class . ' a',
[
'date1' => new DateTimeImmutable('now'),
],
];
yield [
'SELECT AGE(:date0, a.validFrom) FROM ' . Address::class . ' a',
[
'date0' => new DateTimeImmutable('now'),
],
];
}
/**
* @dataProvider generateQueries
*/
public function testWorking(string $dql, array $args)
{
$dql = $this->entityManager->createQuery($dql)->setMaxResults(3);
foreach ($args as $key => $value) {
$dql->setParameter($key, $value);
}
$results = $dql->getResult();
$this->assertIsArray($results);
}
}

View File

@ -0,0 +1,70 @@
<?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 Form\Type;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Form\Type\DataTransformer\PostalCodeToIdTransformer;
use Chill\MainBundle\Form\Type\PickPostalCodeType;
use Chill\MainBundle\Repository\PostalCodeRepositoryInterface;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use ReflectionClass;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
/**
* @internal
* @coversNothing
*/
final class PickPostalCodeTypeTest extends TypeTestCase
{
use ProphecyTrait;
public function testSubmitValidData(): void
{
$builder = $this->factory->createBuilder(FormType::class, ['postal_code' => null]);
$builder->add('postal_code', PickPostalCodeType::class);
$form = $builder->getForm();
$form->submit(['postal_code' => '1']);
$this->assertTrue($form->isSynchronized());
$this->assertEquals(1, $form['postal_code']->getData()->getId());
}
protected function getExtensions()
{
$postalCodeRepository = $this->prophesize(PostalCodeRepositoryInterface::class);
$postalCodeRepository->find(Argument::any())
->will(static function ($args) {
$postalCode = new PostalCode();
$reflectionClass = new ReflectionClass($postalCode);
$id = $reflectionClass->getProperty('id');
$id->setAccessible(true);
$id->setValue($postalCode, (int) $args[0]);
return $postalCode;
});
$type = new PickPostalCodeType(
new PostalCodeToIdTransformer(
$postalCodeRepository->reveal()
)
);
return [
new PreloadedExtension([$type], []),
];
}
}

View File

@ -0,0 +1,127 @@
<?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 Form\Type;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\Type\ScopePickerType;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ManagerRegistry;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Security\Core\Security;
/**
* @internal
* @coversNothing
*/
final class ScopePickerTypeTest extends TypeTestCase
{
use ProphecyTrait;
public function testBuildOneScopeIsSuccessful()
{
$form = $this->factory->create(ScopePickerType::class, null, [
'center' => new Center(),
'role' => 'ONE_SCOPE',
]);
$view = $form->createView();
$this->assertContains('hidden', $view['scope']->vars['block_prefixes']);
}
public function testBuildThreeScopesIsSuccessful()
{
$form = $this->factory->create(ScopePickerType::class, null, [
'center' => new Center(),
'role' => 'THREE_SCOPE',
]);
$view = $form->createView();
$this->assertContains('entity', $view['scope']->vars['block_prefixes']);
}
public function testBuildTwoScopesIsSuccessful()
{
$form = $this->factory->create(ScopePickerType::class, null, [
'center' => new Center(),
'role' => 'TWO_SCOPE',
]);
$view = $form->createView();
$this->assertContains('entity', $view['scope']->vars['block_prefixes']);
}
protected function getExtensions()
{
$user = new User();
$role1Scope = 'ONE_SCOPE';
$role2Scope = 'TWO_SCOPE';
$role3Scope = 'THREE_SCOPE';
$scopeA = (new Scope())->setName(['fr' => 'scope a']);
$scopeB = (new Scope())->setName(['fr' => 'scope b']);
$scopeC = (new Scope())->setName(['fr' => 'scope b'])->setActive(false);
$authorizationHelper = $this->prophesize(AuthorizationHelperInterface::class);
$authorizationHelper->getReachableScopes($user, $role1Scope, Argument::any())
->willReturn([$scopeA]);
$authorizationHelper->getReachableScopes($user, $role2Scope, Argument::any())
->willReturn([$scopeA, $scopeB]);
$authorizationHelper->getReachableScopes($user, $role3Scope, Argument::any())
->willReturn([$scopeA, $scopeB, $scopeC]);
$security = $this->prophesize(Security::class);
$security->getUser()->willReturn($user);
$translatableStringHelper = $this->prophesize(TranslatableStringHelperInterface::class);
$translatableStringHelper->localize(Argument::type('array'))->will(
static function ($args) { return $args[0]['fr']; }
);
$type = new ScopePickerType(
$authorizationHelper->reveal(),
$security->reveal(),
$translatableStringHelper->reveal()
);
// add the mocks for creating EntityType
$entityManager = DoctrineTestHelper::createTestEntityManager();
$em = $this->prophesize(EntityManagerInterface::class);
$em->getClassMetadata(Scope::class)->willReturn($entityManager->getClassMetadata(Scope::class));
$em->contains(Argument::type(Scope::class))->willReturn(true);
$em->initializeObject(Argument::type(Scope::class))->will(static fn ($o) => $o);
$emRevealed = $em->reveal();
$managerRegistry = $this->prophesize(ManagerRegistry::class);
$managerRegistry->getManager(Argument::any())->willReturn($emRevealed);
$managerRegistry->getManagerForClass(Scope::class)->willReturn($emRevealed);
$entityType = $this->prophesize(EntityType::class);
$entityType->getParent()->willReturn(ChoiceType::class);
return [
new PreloadedExtension([$type], []),
new DoctrineOrmExtension($managerRegistry->reveal()),
];
}
}

View File

@ -70,6 +70,7 @@ module.exports = function(encore, entries)
encore.addEntry('mod_entity_workflow_subscribe', __dirname + '/Resources/public/module/entity-workflow-subscribe/index.js'); encore.addEntry('mod_entity_workflow_subscribe', __dirname + '/Resources/public/module/entity-workflow-subscribe/index.js');
encore.addEntry('mod_entity_workflow_pick', __dirname + '/Resources/public/module/entity-workflow-pick/index.js'); encore.addEntry('mod_entity_workflow_pick', __dirname + '/Resources/public/module/entity-workflow-pick/index.js');
encore.addEntry('mod_wopi_link', __dirname + '/Resources/public/module/wopi-link/index.js'); encore.addEntry('mod_wopi_link', __dirname + '/Resources/public/module/wopi-link/index.js');
encore.addEntry('mod_pick_postal_code', __dirname + '/Resources/public/module/pick-postal-code/index.js');
// Vue entrypoints // Vue entrypoints
encore.addEntry('vue_address', __dirname + '/Resources/public/vuejs/Address/index.js'); encore.addEntry('vue_address', __dirname + '/Resources/public/vuejs/Address/index.js');

View File

@ -0,0 +1,33 @@
<?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\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20221010142417 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE scopes DROP active');
}
public function getDescription(): string
{
return 'Allow a scope to be desactivated';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE scopes ADD active BOOLEAN DEFAULT true NOT NULL');
}
}

View File

@ -455,7 +455,6 @@ workflow:
Delete workflow: Supprimer le workflow Delete workflow: Supprimer le workflow
Steps is not waiting for transition. Maybe someone apply the transition before you ?: L'étape que vous cherchez a déjà été modifiée par un autre utilisateur. Peut-être quelqu'un a-t-il modifié cette étape avant vous ? Steps is not waiting for transition. Maybe someone apply the transition before you ?: L'étape que vous cherchez a déjà été modifiée par un autre utilisateur. Peut-être quelqu'un a-t-il modifié cette étape avant vous ?
You get access to this step: Vous avez acquis les droits pour appliquer une transition sur ce workflow. You get access to this step: Vous avez acquis les droits pour appliquer une transition sur ce workflow.
Those users are also granted to apply a transition by using an access key: Ces utilisateurs peuvent également valider cette étape, grâce à un lien d'accès
dest by email: Liens d'autorisation par email dest by email: Liens d'autorisation par email
dest by email help: Les adresses email mentionnées ici recevront un lien d'accès. Ce lien d'accès permettra à l'utilisateur de valider cette étape. dest by email help: Les adresses email mentionnées ici recevront un lien d'accès. Ce lien d'accès permettra à l'utilisateur de valider cette étape.
Add an email: Ajouter une adresse email Add an email: Ajouter une adresse email
@ -467,6 +466,11 @@ workflow:
Previous workflow transitionned help: Workflows où vous avez exécuté une action. Previous workflow transitionned help: Workflows où vous avez exécuté une action.
For: Pour For: Pour
You must select a next step, pick another decision if no next steps are available: Il faut une prochaine étape. Choissisez une autre décision si nécessaire. You must select a next step, pick another decision if no next steps are available: Il faut une prochaine étape. Choissisez une autre décision si nécessaire.
An access key was also sent to those addresses: Un lien d'accès a été envoyé à ces addresses
Those users are also granted to apply a transition by using an access key: Ces utilisateurs ont obtennu l'accès grâce au lien reçu par email
Access link copied: Lien d'accès copié
This link grant any user to apply a transition: Le lien d'accès suivant permet d'appliquer une transition
The workflow may be accssed through this link: Une transition peut être appliquée sur ce workflow grâce au lien d'accès suivant
Subscribe final: Recevoir une notification à l'étape finale Subscribe final: Recevoir une notification à l'étape finale

View File

@ -62,6 +62,7 @@ class UserRefEventSubscriber implements EventSubscriberInterface
&& $period->getUser() !== $this->security->getUser() && $period->getUser() !== $this->security->getUser()
&& null !== $period->getUser() && null !== $period->getUser()
&& $period->getStep() !== AccompanyingPeriod::STEP_DRAFT && $period->getStep() !== AccompanyingPeriod::STEP_DRAFT
&& !$period->isPreventUserIsChangedNotification()
) { ) {
$this->generateNotificationToUser($period); $this->generateNotificationToUser($period);
} }

View File

@ -11,7 +11,9 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Controller; namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\Type\PickPostalCodeType;
use Chill\MainBundle\Form\Type\PickUserDynamicType; use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\UserRepository; use Chill\MainBundle\Repository\UserRepository;
@ -92,12 +94,14 @@ class ReassignAccompanyingPeriodController extends AbstractController
$form->handleRequest($request); $form->handleRequest($request);
$userFrom = $form['user']->getData(); $userFrom = $form['user']->getData();
$postalCodes = $form['postal_code']->getData() instanceof PostalCode ? [$form['postal_code']->getData()] : [];
$total = $this->accompanyingPeriodACLAwareRepository->countByUserOpenedAccompanyingPeriod($userFrom); $total = $this->accompanyingPeriodACLAwareRepository->countByUserOpenedAccompanyingPeriod($userFrom);
$paginator = $this->paginatorFactory->create($total); $paginator = $this->paginatorFactory->create($total);
$periods = $this->accompanyingPeriodACLAwareRepository $periods = $this->accompanyingPeriodACLAwareRepository
->findByUserOpenedAccompanyingPeriod( ->findByUserAndPostalCodesOpenedAccompanyingPeriod(
$userFrom, $userFrom,
$postalCodes,
['openingDate' => 'ASC'], ['openingDate' => 'ASC'],
$paginator->getItemsPerPage(), $paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber() $paginator->getCurrentPageFirstItemNumber()
@ -123,7 +127,7 @@ class ReassignAccompanyingPeriodController extends AbstractController
$period = $this->courseRepository->find($periodId); $period = $this->courseRepository->find($periodId);
if ($period->getUser() === $userFrom) { if ($period->getUser() === $userFrom) {
$period->setUser($userTo); $period->setUser($userTo, true);
} }
} }
@ -148,7 +152,9 @@ class ReassignAccompanyingPeriodController extends AbstractController
{ {
$data = [ $data = [
'user' => null, 'user' => null,
'postal_code' => null,
]; ];
$builder = $this->formFactory->createBuilder(FormType::class, $data, [ $builder = $this->formFactory->createBuilder(FormType::class, $data, [
'method' => 'get', 'csrf_protection' => false, ]); 'method' => 'get', 'csrf_protection' => false, ]);
@ -158,12 +164,17 @@ class ReassignAccompanyingPeriodController extends AbstractController
'label' => 'reassign.Current user', 'label' => 'reassign.Current user',
'required' => false, 'required' => false,
'help' => 'reassign.Choose a user and click on "Filter" to apply', 'help' => 'reassign.Choose a user and click on "Filter" to apply',
])
->add('postal_code', PickPostalCodeType::class, [
'label' => 'reassign.Filter by postal code',
'required' => false,
'help' => 'reassign.Filter course which are located inside a postal code',
]); ]);
return $builder->getForm(); return $builder->getForm();
} }
private function buildReassignForm(array $periodIds, ?User $userFrom): FormInterface private function buildReassignForm(array $periodIds, ?User $userFrom = null): FormInterface
{ {
$defaultData = [ $defaultData = [
'userFrom' => $userFrom, 'userFrom' => $userFrom,

View File

@ -13,6 +13,7 @@ namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Serializer\Model\Collection; use Chill\MainBundle\Serializer\Model\Collection;
use Chill\PersonBundle\Entity\SocialWork\Evaluation;
use Chill\PersonBundle\Entity\SocialWork\SocialAction; use Chill\PersonBundle\Entity\SocialWork\SocialAction;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@ -39,9 +40,11 @@ class SocialWorkEvaluationApiController extends AbstractController
*/ */
public function listEvaluationBySocialAction(SocialAction $action): Response public function listEvaluationBySocialAction(SocialAction $action): Response
{ {
$pagination = $this->paginatorFactory->create($action->getEvaluations()->count()); $evaluations = $action->getEvaluations()->filter(static fn (Evaluation $eval) => $eval->isActive());
$evaluations = $action->getEvaluations()->slice( $pagination = $this->paginatorFactory->create($evaluations->count());
$evaluations = $evaluations->slice(
$pagination->getCurrentPageFirstItemNumber(), $pagination->getCurrentPageFirstItemNumber(),
$pagination->getItemsPerPage() $pagination->getItemsPerPage()
); );

View File

@ -22,6 +22,7 @@ use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Entity\UserJob;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodLocationHistory; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodLocationHistory;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodStepHistory;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive; use Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment; use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment;
@ -265,6 +266,8 @@ class AccompanyingPeriod implements
*/ */
private ?Comment $pinnedComment = null; private ?Comment $pinnedComment = null;
private bool $preventUserIsChangedNotification = false;
/** /**
* @ORM\Column(type="text") * @ORM\Column(type="text")
* @Groups({"read", "write"}) * @Groups({"read", "write"})
@ -334,6 +337,12 @@ class AccompanyingPeriod implements
*/ */
private string $step = self::STEP_DRAFT; private string $step = self::STEP_DRAFT;
/**
* @ORM\OneToMany(targetEntity=AccompanyingPeriodStepHistory::class,
* mappedBy="period", cascade={"persist", "remove"}, orphanRemoval=true)
*/
private Collection $stepHistories;
/** /**
* @ORM\Column(type="datetime", nullable=true, options={"default": NULL}) * @ORM\Column(type="datetime", nullable=true, options={"default": NULL})
*/ */
@ -388,7 +397,6 @@ class AccompanyingPeriod implements
*/ */
public function __construct(?DateTime $dateOpening = null) public function __construct(?DateTime $dateOpening = null)
{ {
$this->setOpeningDate($dateOpening ?? new DateTime('now'));
$this->participations = new ArrayCollection(); $this->participations = new ArrayCollection();
$this->scopes = new ArrayCollection(); $this->scopes = new ArrayCollection();
$this->socialIssues = new ArrayCollection(); $this->socialIssues = new ArrayCollection();
@ -397,6 +405,8 @@ class AccompanyingPeriod implements
$this->resources = new ArrayCollection(); $this->resources = new ArrayCollection();
$this->userHistories = new ArrayCollection(); $this->userHistories = new ArrayCollection();
$this->locationHistories = new ArrayCollection(); $this->locationHistories = new ArrayCollection();
$this->stepHistories = new ArrayCollection();
$this->setOpeningDate($dateOpening ?? new DateTime('now'));
} }
/** /**
@ -964,6 +974,11 @@ class AccompanyingPeriod implements
return $this->step; return $this->step;
} }
public function getStepHistories(): Collection
{
return $this->stepHistories;
}
public function getUser(): ?User public function getUser(): ?User
{ {
return $this->user; return $this->user;
@ -1048,6 +1063,11 @@ class AccompanyingPeriod implements
return false; return false;
} }
public function isPreventUserIsChangedNotification(): bool
{
return $this->preventUserIsChangedNotification;
}
public function isRequestorAnonymous(): bool public function isRequestorAnonymous(): bool
{ {
return $this->requestorAnonymous; return $this->requestorAnonymous;
@ -1227,7 +1247,11 @@ class AccompanyingPeriod implements
*/ */
public function setOpeningDate($openingDate) public function setOpeningDate($openingDate)
{ {
$this->openingDate = $openingDate; if ($this->openingDate !== $openingDate) {
$this->openingDate = $openingDate;
$this->ensureStepContinuity();
}
return $this; return $this;
} }
@ -1326,6 +1350,14 @@ class AccompanyingPeriod implements
$this->bootPeriod(); $this->bootPeriod();
} }
if (self::STEP_DRAFT !== $this->step && $previous !== $step) {
// we create a new history
$history = new AccompanyingPeriodStepHistory();
$history->setStep($this->step)->setStartDate(new DateTimeImmutable('now'));
$this->addStepHistory($history);
}
return $this; return $this;
} }
@ -1343,11 +1375,12 @@ class AccompanyingPeriod implements
return $this; return $this;
} }
public function setUser(?User $user): self public function setUser(?User $user, bool $preventNotification = false): self
{ {
if ($this->user !== $user) { if ($this->user !== $user) {
$this->userPrevious = $this->user; $this->userPrevious = $this->user;
$this->userIsChanged = true; $this->userIsChanged = true;
$this->preventUserIsChangedNotification = $preventNotification;
foreach ($this->userHistories as $history) { foreach ($this->userHistories as $history) {
if (null === $history->getEndDate()) { if (null === $history->getEndDate()) {
@ -1365,6 +1398,17 @@ class AccompanyingPeriod implements
return $this; return $this;
} }
private function addStepHistory(AccompanyingPeriodStepHistory $stepHistory): self
{
if (!$this->stepHistories->contains($stepHistory)) {
$this->stepHistories[] = $stepHistory;
$stepHistory->setPeriod($this);
$this->ensureStepContinuity();
}
return $this;
}
private function bootPeriod(): void private function bootPeriod(): void
{ {
// first location history // first location history
@ -1376,6 +1420,43 @@ class AccompanyingPeriod implements
$this->addLocationHistory($locationHistory); $this->addLocationHistory($locationHistory);
} }
private function ensureStepContinuity(): void
{
// ensure continuity of histories
$criteria = new Criteria();
$criteria->orderBy(['startDate' => Criteria::ASC, 'id' => Criteria::ASC]);
/** @var Iterator $steps */
$steps = $this->getStepHistories()->matching($criteria)->getIterator();
$steps->rewind();
// we set the start date of the first step as the opening date, only if it is
// not greater than the end date
/** @var AccompanyingPeriodStepHistory $current */
$current = $steps->current();
if (null === $current) {
return;
}
if ($this->getOpeningDate()->format('Y-m-d') !== $current->getStartDate()->format('Y-m-d')
&& ($this->getOpeningDate() <= $current->getEndDate() || null === $current->getEndDate())) {
$current->setStartDate(DateTimeImmutable::createFromMutable($this->getOpeningDate()));
}
// then we set all the end date to the start date of the next one
do {
/** @var AccompanyingPeriodStepHistory $current */
$current = $steps->current();
$steps->next();
if ($steps->valid()) {
$next = $steps->current();
$current->setEndDate($next->getStartDate());
}
} while ($steps->valid());
}
private function setRequestorPerson(?Person $requestorPerson = null): self private function setRequestorPerson(?Person $requestorPerson = null): self
{ {
$this->requestorPerson = $requestorPerson; $this->requestorPerson = $requestorPerson;

View File

@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table("chill_person_accompanying_period_step_history")
*/
class AccompanyingPeriodStepHistory implements TrackCreationInterface, TrackUpdateInterface
{
use TrackCreationTrait;
use TrackUpdateTrait;
/**
* @ORM\Column(type="date_immutable", nullable=true, options={"default": null})
*/
private ?DateTimeImmutable $endDate = null;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private ?int $id = null;
/**
* @ORM\ManyToOne(targetEntity=AccompanyingPeriod::class)
*/
private AccompanyingPeriod $period;
/**
* @ORM\Column(type="date_immutable")
*/
private ?DateTimeImmutable $startDate = null;
/**
* @ORM\Column(type="text", nullable=false)
*/
private string $step;
public function getEndDate(): ?DateTimeImmutable
{
return $this->endDate;
}
public function getId(): ?int
{
return $this->id;
}
public function getPeriod(): AccompanyingPeriod
{
return $this->period;
}
public function getStartDate(): ?DateTimeImmutable
{
return $this->startDate;
}
public function getStep(): string
{
return $this->step;
}
public function setEndDate(?DateTimeImmutable $endDate): self
{
$this->endDate = $endDate;
return $this;
}
/**
* @internal use AccompanyingPeriod::addLocationHistory
*/
public function setPeriod(AccompanyingPeriod $period): self
{
$this->period = $period;
return $this;
}
public function setStartDate(?DateTimeImmutable $startDate): self
{
$this->startDate = $startDate;
return $this;
}
public function setStep(string $step): AccompanyingPeriodStepHistory
{
$this->step = $step;
return $this;
}
}

View File

@ -36,7 +36,7 @@ class Comment implements TrackCreationInterface, TrackUpdateInterface
* inversedBy="comments") * inversedBy="comments")
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE") * @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
*/ */
private ?AccompanyingPeriod $accompanyingPeriod; private ?AccompanyingPeriod $accompanyingPeriod = null;
/** /**
* @ORM\Column(type="text") * @ORM\Column(type="text")

View File

@ -26,6 +26,11 @@ use Symfony\Component\Serializer\Annotation as Serializer;
*/ */
class Evaluation class Evaluation
{ {
/**
* @ORM\Column(type="boolean", nullable=false, options={"default": true})
*/
private bool $active = true;
/** /**
* @ORM\Column(type="dateinterval", nullable=true, options={"default": null}) * @ORM\Column(type="dateinterval", nullable=true, options={"default": null})
* @Serializer\Groups({"read"}) * @Serializer\Groups({"read"})
@ -72,6 +77,9 @@ class Evaluation
$this->socialActions = new ArrayCollection(); $this->socialActions = new ArrayCollection();
} }
/**
* @internal do use @see{SocialAction::addEvaluation}
*/
public function addSocialAction(SocialAction $socialAction): self public function addSocialAction(SocialAction $socialAction): self
{ {
if (!$this->socialActions->contains($socialAction)) { if (!$this->socialActions->contains($socialAction)) {
@ -111,6 +119,16 @@ class Evaluation
return $this->url; return $this->url;
} }
public function isActive(): bool
{
return $this->active;
}
/**
* @return $this
*
* @internal do use @see{SocialAction::removeEvaluation}
*/
public function removeSocialAction(SocialAction $socialAction): self public function removeSocialAction(SocialAction $socialAction): self
{ {
if ($this->socialActions->contains($socialAction)) { if ($this->socialActions->contains($socialAction)) {
@ -120,6 +138,13 @@ class Evaluation
return $this; return $this;
} }
public function setActive(bool $active): Evaluation
{
$this->active = $active;
return $this;
}
public function setDelay(?DateInterval $delay): self public function setDelay(?DateInterval $delay): self
{ {
$this->delay = $delay; $this->delay = $delay;

View File

@ -112,6 +112,7 @@ class SocialAction
{ {
if (!$this->evaluations->contains($evaluation)) { if (!$this->evaluations->contains($evaluation)) {
$this->evaluations[] = $evaluation; $this->evaluations[] = $evaluation;
$evaluation->addSocialAction($this);
} }
return $this; return $this;
@ -332,6 +333,7 @@ class SocialAction
public function removeEvaluation(Evaluation $evaluation): self public function removeEvaluation(Evaluation $evaluation): self
{ {
$this->evaluations->removeElement($evaluation); $this->evaluations->removeElement($evaluation);
$evaluation->removeSocialAction($this);
return $this; return $this;
} }

View File

@ -13,12 +13,21 @@ namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
use Chill\MainBundle\Export\AggregatorInterface; use Chill\MainBundle\Export\AggregatorInterface;
use Chill\PersonBundle\Export\Declarations; use Chill\PersonBundle\Export\Declarations;
use DateTimeImmutable;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use LogicException;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
final class DurationAggregator implements AggregatorInterface final class DurationAggregator implements AggregatorInterface
{ {
private const CHOICES = [
'month',
'week',
'day',
];
private TranslatorInterface $translator; private TranslatorInterface $translator;
public function __construct(TranslatorInterface $translator) public function __construct(TranslatorInterface $translator)
@ -33,19 +42,31 @@ final class DurationAggregator implements AggregatorInterface
public function alterQuery(QueryBuilder $qb, $data) public function alterQuery(QueryBuilder $qb, $data)
{ {
$qb->addSelect( switch ($data['precision']) {
' case 'day':
(acp.closingDate - acp.openingDate +15) *12/365 $qb->addSelect('(COALESCE(acp.closingDate, :now) - acp.openingDate) AS duration_aggregator');
AS duration_aggregator'
);
// TODO Pour avoir un interval plus précis (nécessaire ?): break;
// adapter la fonction extract pour pouvoir l'utiliser avec des intervals: extract(month from interval)
// et ajouter une fonction custom qui calcule plus précisément les intervals, comme doctrineum/date-interval
// https://packagist.org/packages/doctrineum/date-interval#3.1.0 (mais composer fait un conflit de dépendance)
$qb->addGroupBy('duration_aggregator'); case 'week':
$qb->addOrderBy('duration_aggregator'); $qb->addSelect('(COALESCE(acp.closingDate, :now) - acp.openingDate) / 7 AS duration_aggregator');
break;
case 'month':
$qb->addSelect('(EXTRACT (MONTH FROM AGE(COALESCE(acp.closingDate, :now), acp.openingDate)) * 12 +
EXTRACT (MONTH FROM AGE(COALESCE(acp.closingDate, :now), acp.openingDate))) AS duration_aggregator');
break;
default:
throw new LogicException('precision not supported: ' . $data['precision']);
}
$qb
->setParameter('now', new DateTimeImmutable('now'))
->addGroupBy('duration_aggregator')
->addOrderBy('duration_aggregator');
} }
public function applyOn(): string public function applyOn(): string
@ -55,25 +76,27 @@ final class DurationAggregator implements AggregatorInterface
public function buildForm(FormBuilderInterface $builder) public function buildForm(FormBuilderInterface $builder)
{ {
// no form $builder->add('precision', ChoiceType::class, [
'choices' => array_combine(self::CHOICES, self::CHOICES),
'label' => 'export.aggregator.course.duration.Precision',
'choice_label' => static fn (string $c) => 'export.aggregator.course.duration.' . $c,
'multiple' => false,
'expanded' => true,
]);
} }
public function getLabels($key, array $values, $data) public function getLabels($key, array $values, $data)
{ {
return function ($value): ?string { return static function ($value) use ($data) {
if ('_header' === $value) { if ('_header' === $value) {
return 'Rounded month duration'; return 'export.aggregator.course.duration.' . $data['precision'];
} }
if (null === $value) { if (null === $value) {
return $this->translator->trans('current duration'); // when closingDate is null return 0;
} }
if (0 === $value) { return $value;
return $this->translator->trans('duration 0 month');
}
return $value . $this->translator->trans(' months');
}; };
} }

View File

@ -12,15 +12,20 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators; namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
use Chill\MainBundle\Export\AggregatorInterface; use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Repository\UserRepository; use Chill\MainBundle\Repository\UserRepository;
use Chill\MainBundle\Templating\Entity\UserRender; use Chill\MainBundle\Templating\Entity\UserRender;
use Chill\PersonBundle\Export\Declarations; use Chill\PersonBundle\Export\Declarations;
use DateTimeImmutable;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use function in_array;
final class ReferrerAggregator implements AggregatorInterface final class ReferrerAggregator implements AggregatorInterface
{ {
private const A = 'acp_ref_agg_uhistory';
private const P = 'acp_ref_agg_date';
private UserRender $userRender; private UserRender $userRender;
private UserRepository $userRepository; private UserRepository $userRepository;
@ -40,12 +45,23 @@ final class ReferrerAggregator implements AggregatorInterface
public function alterQuery(QueryBuilder $qb, $data) public function alterQuery(QueryBuilder $qb, $data)
{ {
if (!in_array('acpuser', $qb->getAllAliases(), true)) { $qb
$qb->leftJoin('acp.user', 'acpuser'); ->addSelect('IDENTITY(' . self::A . '.user) AS referrer_aggregator')
} ->addGroupBy('referrer_aggregator')
->leftJoin('acp.userHistories', self::A)
$qb->addSelect('acpuser.id AS referrer_aggregator'); ->andWhere(
$qb->addGroupBy('referrer_aggregator'); $qb->expr()->orX(
$qb->expr()->isNull(self::A),
$qb->expr()->andX(
$qb->expr()->lte(self::A . '.startDate', ':' . self::P),
$qb->expr()->orX(
$qb->expr()->isNull(self::A . '.endDate'),
$qb->expr()->gt(self::A . '.endDate', ':' . self::P)
)
)
)
)
->setParameter(self::P, $data['date_calc']);
} }
public function applyOn(): string public function applyOn(): string
@ -55,7 +71,13 @@ final class ReferrerAggregator implements AggregatorInterface
public function buildForm(FormBuilderInterface $builder) public function buildForm(FormBuilderInterface $builder)
{ {
// no form $builder
->add('date_calc', ChillDateType::class, [
'input' => 'datetime_immutable',
'data' => new DateTimeImmutable('now'),
'label' => 'export.aggregator.course.by_referrer.Computation date for referrer',
'required' => true,
]);
} }
public function getLabels($key, array $values, $data) public function getLabels($key, array $values, $data)

View File

@ -12,20 +12,21 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators; namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
use Chill\MainBundle\Export\AggregatorInterface; use Chill\MainBundle\Export\AggregatorInterface;
//use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Export\Declarations; use Chill\PersonBundle\Export\Declarations;
use DateTime; use DateTime;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Query\Expr\Andx;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use LogicException;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
use function in_array;
final class StepAggregator implements AggregatorInterface //, FilterInterface final class StepAggregator implements AggregatorInterface
{ {
private const A = 'acpstephistories';
private const P = 'acp_step_agg_date';
private TranslatorInterface $translator; private TranslatorInterface $translator;
public function __construct( public function __construct(
@ -41,30 +42,26 @@ final class StepAggregator implements AggregatorInterface //, FilterInterface
public function alterQuery(QueryBuilder $qb, $data) public function alterQuery(QueryBuilder $qb, $data)
{ {
$qb->addSelect('acp.step AS step_aggregator'); if (!in_array(self::A, $qb->getAllAliases(), true)) {
$qb->addGroupBy('step_aggregator'); $qb->leftJoin('acp.stepHistories', self::A);
/*
// add date in where clause
$where = $qb->getDQLPart('where');
$clause = $qb->expr()->andX(
$qb->expr()->lte('acp.openingDate', ':ondate'),
$qb->expr()->orX(
$qb->expr()->gt('acp.closingDate', ':ondate'),
$qb->expr()->isNull('acp.closingDate')
)
);
if ($where instanceof Andx) {
$where->add($clause);
} else {
$where = $qb->expr()->andX($clause);
} }
$qb->add('where', $where); $qb
$qb->setParameter('ondate', $data['on_date'], Types::DATE_MUTABLE); ->addSelect(self::A . '.step AS step_aggregator')
*/ ->andWhere(
$qb->expr()->orX(
$qb->expr()->isNull(self::A . '.step'),
$qb->expr()->andX(
$qb->expr()->lte(self::A . '.startDate', ':' . self::P),
$qb->expr()->orX(
$qb->expr()->isNull(self::A . '.endDate'),
$qb->expr()->lt(self::A . '.endDate', ':' . self::P)
)
)
)
)
->setParameter(self::P, $data['on_date'])
->addGroupBy('step_aggregator');
} }
public function applyOn(): string public function applyOn(): string
@ -95,8 +92,11 @@ final class StepAggregator implements AggregatorInterface //, FilterInterface
case '_header': case '_header':
return 'Step'; return 'Step';
case null:
return '';
default: default:
throw new LogicException(sprintf('The value %s is not valid', $value)); return $value;
} }
}; };
} }

View File

@ -23,6 +23,7 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
use function in_array;
final class CountryOfBirthAggregator implements AggregatorInterface, ExportElementValidatedInterface final class CountryOfBirthAggregator implements AggregatorInterface, ExportElementValidatedInterface
{ {
@ -83,7 +84,9 @@ final class CountryOfBirthAggregator implements AggregatorInterface, ExportEleme
. ' is not known.'); . ' is not known.');
} }
$qb->leftJoin('person.countryOfBirth', 'countryOfBirth'); if (!in_array('countryOfBirth', $qb->getAllAliases(), true)) {
$qb->leftJoin('person.countryOfBirth', 'countryOfBirth');
}
// add group by // add group by
$qb->addGroupBy('country_of_birth_aggregator'); $qb->addGroupBy('country_of_birth_aggregator');

View File

@ -99,6 +99,7 @@ class CountAccompanyingCourse implements ExportInterface, GroupedExportInterface
$qb = $this->repository->createQueryBuilder('acp'); $qb = $this->repository->createQueryBuilder('acp');
$qb $qb
->andWhere('acp.step != :count_acp_step')
->andWhere( ->andWhere(
$qb->expr()->exists( $qb->expr()->exists(
'SELECT 1 FROM ' . AccompanyingPeriodParticipation::class . ' acl_count_part 'SELECT 1 FROM ' . AccompanyingPeriodParticipation::class . ' acl_count_part
@ -107,6 +108,7 @@ class CountAccompanyingCourse implements ExportInterface, GroupedExportInterface
' '
) )
) )
->setParameter('count_acp_step', AccompanyingPeriod::STEP_DRAFT)
->setParameter('authorized_centers', $centers); ->setParameter('authorized_centers', $centers);
$qb->select('COUNT(DISTINCT acp.id) AS export_result'); $qb->select('COUNT(DISTINCT acp.id) AS export_result');

View File

@ -50,9 +50,6 @@ class AdministrativeLocationFilter implements FilterInterface
{ {
$builder->add('accepted_locations', PickUserLocationType::class, [ $builder->add('accepted_locations', PickUserLocationType::class, [
'label' => 'Accepted locations', 'label' => 'Accepted locations',
'label_attr' => [
//'class' => 'd-none'
],
'multiple' => true, 'multiple' => true,
]); ]);
} }

View File

@ -1,90 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\Query\Expr\Andx;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Security\Core\Security;
class CurrentUserJobFilter implements FilterInterface
{
private Security $security;
private TranslatableStringHelper $translatableStringHelper;
public function __construct(
TranslatableStringHelper $translatableStringHelper,
Security $security
) {
$this->translatableStringHelper = $translatableStringHelper;
$this->security = $security;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$where = $qb->getDQLPart('where');
$clause = $qb->expr()->eq('acp.job', ':userjob');
if ($where instanceof Andx) {
$where->add($clause);
} else {
$where = $qb->expr()->andX($clause);
}
$qb->add('where', $where);
$qb->setParameter('userjob', $this->getUserJob());
}
public function applyOn()
{
return Declarations::ACP_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
}
public function describeAction($data, $format = 'string')
{
return [
'Filtered by user job: only %job%', [
'%job%' => $this->translatableStringHelper->localize(
$this->getUserJob()->getLabel()
),
],
];
}
public function getTitle()
{
return 'Filter by user job';
}
private function getUserJob(): UserJob
{
/** @var User $user */
$user = $this->security->getUser();
return $user->getUserJob();
}
}

View File

@ -1,95 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\Query\Expr\Andx;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Security\Core\Security;
use function in_array;
class CurrentUserScopeFilter implements FilterInterface
{
private Security $security;
private TranslatableStringHelper $translatableStringHelper;
public function __construct(
TranslatableStringHelper $translatableStringHelper,
Security $security
) {
$this->translatableStringHelper = $translatableStringHelper;
$this->security = $security;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
if (!in_array('acpscope', $qb->getAllAliases(), true)) {
$qb->join('acp.scopes', 'acpscope');
}
$where = $qb->getDQLPart('where');
$clause = $qb->expr()->eq('acpscope.id', ':userscope');
if ($where instanceof Andx) {
$where->add($clause);
} else {
$where = $qb->expr()->andX($clause);
}
$qb->add('where', $where);
$qb->setParameter('userscope', $this->getUserMainScope());
}
public function applyOn()
{
return Declarations::ACP_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
}
public function describeAction($data, $format = 'string')
{
return [
'Filtered by user main scope: only %scope%', [
'%scope%' => $this->translatableStringHelper->localize(
$this->getUserMainScope()->getName()
),
],
];
}
public function getTitle()
{
return 'Filter by user scope';
}
private function getUserMainScope(): Scope
{
/** @var User $user */
$user = $this->security->getUser();
return $user->getMainScope();
}
}

View File

@ -15,7 +15,7 @@ use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\SocialWork\Evaluation; use Chill\PersonBundle\Entity\SocialWork\Evaluation;
use Chill\PersonBundle\Export\Declarations; use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\Query\Expr\Andx; use Chill\PersonBundle\Repository\SocialWork\EvaluationRepositoryInterface;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
@ -23,11 +23,15 @@ use function in_array;
class EvaluationFilter implements FilterInterface class EvaluationFilter implements FilterInterface
{ {
private EvaluationRepositoryInterface $evaluationRepository;
private TranslatableStringHelper $translatableStringHelper; private TranslatableStringHelper $translatableStringHelper;
public function __construct( public function __construct(
EvaluationRepositoryInterface $evaluationRepository,
TranslatableStringHelper $translatableStringHelper TranslatableStringHelper $translatableStringHelper
) { ) {
$this->evaluationRepository = $evaluationRepository;
$this->translatableStringHelper = $translatableStringHelper; $this->translatableStringHelper = $translatableStringHelper;
} }
@ -50,16 +54,8 @@ class EvaluationFilter implements FilterInterface
$qb->join('workeval.evaluation', 'eval'); $qb->join('workeval.evaluation', 'eval');
} }
$where = $qb->getDQLPart('where');
$clause = $qb->expr()->in('eval.id', ':evaluations'); $clause = $qb->expr()->in('eval.id', ':evaluations');
$qb->andWhere($clause);
if ($where instanceof Andx) {
$where->add($clause);
} else {
$where = $qb->expr()->andX($clause);
}
$qb->add('where', $where);
$qb->setParameter('evaluations', $data['accepted_evaluations']); $qb->setParameter('evaluations', $data['accepted_evaluations']);
} }
@ -72,11 +68,13 @@ class EvaluationFilter implements FilterInterface
{ {
$builder->add('accepted_evaluations', EntityType::class, [ $builder->add('accepted_evaluations', EntityType::class, [
'class' => Evaluation::class, 'class' => Evaluation::class,
'choices' => $this->evaluationRepository->findAllActive(),
'choice_label' => function (Evaluation $ev) { 'choice_label' => function (Evaluation $ev) {
return $this->translatableStringHelper->localize($ev->getTitle()); return $this->translatableStringHelper->localize($ev->getTitle());
}, },
'multiple' => true, 'multiple' => true,
'expanded' => true, 'expanded' => false,
'attr' => ['class' => 'select2'],
]); ]);
} }

View File

@ -11,17 +11,23 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters; namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Templating\Entity\UserRender; use Chill\MainBundle\Templating\Entity\UserRender;
use Chill\PersonBundle\Export\Declarations; use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\Query\Expr\Andx; use DateTimeImmutable;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
class ReferrerFilter implements FilterInterface class ReferrerFilter implements FilterInterface
{ {
private const A = 'acp_referrer_filter_uhistory';
private const P = 'acp_referrer_filter_date';
private const PU = 'acp_referrer_filter_users';
private UserRender $userRender; private UserRender $userRender;
public function __construct(UserRender $userRender) public function __construct(UserRender $userRender)
@ -36,17 +42,22 @@ class ReferrerFilter implements FilterInterface
public function alterQuery(QueryBuilder $qb, $data) public function alterQuery(QueryBuilder $qb, $data)
{ {
$where = $qb->getDQLPart('where'); $qb
$clause = $qb->expr()->in('acp.user', ':referrers'); ->join('acp.userHistories', self::A)
->andWhere(
if ($where instanceof Andx) { $qb->expr()->andX(
$where->add($clause); $qb->expr()->lte(self::A . '.startDate', ':' . self::P),
} else { $qb->expr()->orX(
$where = $qb->expr()->andX($clause); $qb->expr()->isNull(self::A . '.endDate'),
} $qb->expr()->gt(self::A . '.endDate', ':' . self::P)
)
$qb->add('where', $where); )
$qb->setParameter('referrers', $data['accepted_referrers']); )
->andWhere(
$qb->expr()->in(self::A . '.user', ':' . self::PU)
)
->setParameter(self::PU, $data['accepted_referrers'])
->setParameter(self::P, $data['date_calc']);
} }
public function applyOn(): string public function applyOn(): string
@ -56,14 +67,16 @@ class ReferrerFilter implements FilterInterface
public function buildForm(FormBuilderInterface $builder) public function buildForm(FormBuilderInterface $builder)
{ {
$builder->add('accepted_referrers', EntityType::class, [ $builder
'class' => User::class, ->add('accepted_referrers', PickUserDynamicType::class, [
'choice_label' => function (User $u) { 'multiple' => true,
return $this->userRender->renderString($u, []); ])
}, ->add('date_calc', ChillDateType::class, [
'multiple' => true, 'input' => 'datetime_immutable',
'expanded' => true, 'data' => new DateTimeImmutable('now'),
]); 'label' => 'export.filter.course.by_referrer.Computation date for referrer',
'required' => true,
]);
} }
public function describeAction($data, $format = 'string'): array public function describeAction($data, $format = 'string'): array

View File

@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Export\Declarations;
use DateTimeImmutable;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Security\Core\Security;
class UserJobFilter implements FilterInterface
{
private const A = 'acp_ujob_filter_uhistory';
private const AU = 'acp_ujob_filter_uhistory_user';
private const P = 'acp_ujob_filter_date';
private const PJ = 'acp_ujob_filter_job';
private Security $security;
private TranslatableStringHelper $translatableStringHelper;
private UserJobRepositoryInterface $userJobRepository;
public function __construct(
Security $security,
TranslatableStringHelper $translatableStringHelper,
UserJobRepositoryInterface $userJobRepository
) {
$this->security = $security;
$this->translatableStringHelper = $translatableStringHelper;
$this->userJobRepository = $userJobRepository;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$qb
->join('acp.userHistories', self::A)
->andWhere(
$qb->expr()->andX(
$qb->expr()->lte(self::A . '.startDate', ':' . self::P),
$qb->expr()->orX(
$qb->expr()->isNull(self::A . '.endDate'),
$qb->expr()->gt(self::A . '.endDate', ':' . self::P)
)
)
)
->setParameter(self::P, $data['date_calc'])
->join(self::A . '.user', self::AU)
->andWhere(
$qb->expr()->in(self::AU . '.userJob', ':' . self::PJ)
)
->setParameter(self::PJ, $data['jobs']);
}
public function applyOn()
{
return Declarations::ACP_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('jobs', EntityType::class, [
'class' => UserJob::class,
'choices' => $this->userJobRepository->findAllActive(),
'multiple' => true,
'expanded' => true,
'choice_label' => fn (UserJob $job) => $this->translatableStringHelper->localize($job->getLabel()),
'label' => 'Job',
])
->add('date_calc', ChillDateType::class, [
'input' => 'datetime_immutable',
'data' => new DateTimeImmutable('now'),
'label' => 'export.filter.course.by_user_scope.Computation date for referrer',
'required' => true,
]);
}
public function describeAction($data, $format = 'string')
{
return [
'Filtered by user job: only %job%', [
'%job%' => implode(
', ',
array_map(
fn (UserJob $job) => $this->translatableStringHelper->localize($job->getLabel()),
$data['jobs']->toArray()
)
),
],
];
}
public function getTitle()
{
return 'Filter by user job';
}
private function getUserJob(): UserJob
{
/** @var User $user */
$user = $this->security->getUser();
return $user->getUserJob();
}
}

View File

@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Export\Declarations;
use DateTimeImmutable;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Security\Core\Security;
class UserScopeFilter implements FilterInterface
{
private const A = 'acp_uscope_filter_uhistory';
private const AU = 'acp_uscope_filter_uhistory_user';
private const P = 'acp_uscope_filter_date';
private const PS = 'acp_uscope_filter_scopes';
private ScopeRepositoryInterface $scopeRepository;
private Security $security;
private TranslatableStringHelper $translatableStringHelper;
public function __construct(
ScopeRepositoryInterface $scopeRepository,
Security $security,
TranslatableStringHelper $translatableStringHelper
) {
$this->scopeRepository = $scopeRepository;
$this->security = $security;
$this->translatableStringHelper = $translatableStringHelper;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$qb
->join('acp.userHistories', self::A)
->andWhere(
$qb->expr()->andX(
$qb->expr()->lte(self::A . '.startDate', ':' . self::P),
$qb->expr()->orX(
$qb->expr()->isNull(self::A . '.endDate'),
$qb->expr()->gt(self::A . '.endDate', ':' . self::P)
)
)
)
->setParameter(self::P, $data['date_calc'])
->join(self::A . '.user', self::AU)
->andWhere(
$qb->expr()->in(self::AU . '.mainScope', ':' . self::PS)
)
->setParameter(self::PS, $data['scopes']);
}
public function applyOn()
{
return Declarations::ACP_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('scopes', EntityType::class, [
'class' => Scope::class,
'choices' => $this->scopeRepository->findAllActive(),
'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()),
'multiple' => true,
'expanded' => true,
])
->add('date_calc', ChillDateType::class, [
'input' => 'datetime_immutable',
'data' => new DateTimeImmutable('now'),
'label' => 'export.filter.course.by_user_scope.Computation date for referrer',
'required' => true,
]);
}
public function describeAction($data, $format = 'string')
{
return [
'Filtered by user main scope: only %scope%', [
'%scope%' => implode(
', ',
array_map(
fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()),
$data['scopes']->toArray()
)
),
],
];
}
public function getTitle()
{
return 'Filter by user scope';
}
private function getUserMainScope(): Scope
{
/** @var User $user */
$user = $this->security->getUser();
return $user->getMainScope();
}
}

View File

@ -35,7 +35,7 @@ class AgeFilter implements ExportElementValidatedInterface, FilterInterface
$where = $qb->getDQLPart('where'); $where = $qb->getDQLPart('where');
$min = null !== $data['min_age'] ? $data['min_age'] : 0; $min = null !== $data['min_age'] ? $data['min_age'] : 0;
$max = null !== $data['max_age'] ? $data['max_age'] : 3000; $max = null !== $data['max_age'] ? $data['max_age'] : 150;
$calc = $data['date_calc']; $calc = $data['date_calc'];
$minDate = $calc->sub(new DateInterval('P' . $max . 'Y')); $minDate = $calc->sub(new DateInterval('P' . $max . 'Y'));

View File

@ -12,12 +12,9 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Export\Filter\PersonFilters; namespace Chill\PersonBundle\Export\Filter\PersonFilters;
use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\MaritalStatus; use Chill\PersonBundle\Entity\MaritalStatus;
use Chill\PersonBundle\Export\Declarations; use Chill\PersonBundle\Export\Declarations;
use DateTime;
use Doctrine\ORM\Query\Expr\Andx;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Form\Type\EntityType;
class MaritalStatusFilter implements FilterInterface class MaritalStatusFilter implements FilterInterface
@ -37,25 +34,10 @@ class MaritalStatusFilter implements FilterInterface
public function alterQuery(\Doctrine\ORM\QueryBuilder $qb, $data) public function alterQuery(\Doctrine\ORM\QueryBuilder $qb, $data)
{ {
$where = $qb->getDQLPart('where'); $qb->andWhere(
$qb->expr()->in('person.maritalStatus', ':maritalStatus')
$clause = $qb->expr()->andX(
$qb->expr()->in('person.maritalStatus', ':maritalStatus'),
$qb->expr()->orX(
$qb->expr()->eq('person.maritalStatusDate', ':calc_date'),
$qb->expr()->isNull('person.maritalStatusDate')
)
); );
if ($where instanceof Andx) {
$where->add($clause);
} else {
$where = $qb->expr()->andX($clause);
}
$qb->add('where', $where);
$qb->setParameter('maritalStatus', $data['maritalStatus']); $qb->setParameter('maritalStatus', $data['maritalStatus']);
$qb->setParameter('calc_date', $data['calc_date']);
} }
public function applyOn() public function applyOn()
@ -75,11 +57,6 @@ class MaritalStatusFilter implements FilterInterface
'multiple' => true, 'multiple' => true,
'expanded' => true, 'expanded' => true,
]); ]);
$builder->add('calc_date', ChillDateType::class, [
'label' => 'Marital status at this time',
'data' => new DateTime(),
]);
} }
public function describeAction($data, $format = 'string') public function describeAction($data, $format = 'string')

View File

@ -16,6 +16,7 @@ use Chill\MainBundle\Form\Type\TranslatableStringFormType;
use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\SocialWork\Evaluation; use Chill\PersonBundle\Entity\SocialWork\Evaluation;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\UrlType; use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
@ -52,6 +53,14 @@ class EvaluationType extends AbstractType
->add('notificationDelay', DateIntervalType::class, [ ->add('notificationDelay', DateIntervalType::class, [
'label' => 'evaluation.notificationDelay', 'label' => 'evaluation.notificationDelay',
'required' => false, 'required' => false,
])
->add('active', ChoiceType::class, [
'label' => 'active',
'choices' => [
'active' => true,
'inactive' => false,
],
'required' => true,
]); ]);
} }

View File

@ -11,7 +11,9 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Repository; namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Entity\UserJob;
@ -19,10 +21,14 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface; use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use DateTime; use DateTime;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
use function count; use function count;
@ -49,7 +55,12 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC
$this->centerResolverDispatcher = $centerResolverDispatcher; $this->centerResolverDispatcher = $centerResolverDispatcher;
} }
public function buildQueryOpenedAccompanyingCourseByUser(?User $user) /**
* @param array|PostalCode[]
*
* @return QueryBuilder
*/
public function buildQueryOpenedAccompanyingCourseByUser(?User $user, array $postalCodes = [])
{ {
$qb = $this->accompanyingPeriodRepository->createQueryBuilder('ap'); $qb = $this->accompanyingPeriodRepository->createQueryBuilder('ap');
@ -65,6 +76,37 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC
->setParameter('now', new DateTime('now')) ->setParameter('now', new DateTime('now'))
->setParameter('draft', AccompanyingPeriod::STEP_DRAFT); ->setParameter('draft', AccompanyingPeriod::STEP_DRAFT);
if ([] !== $postalCodes) {
$qb->join('ap.locationHistories', 'location_history')
->leftJoin(PersonHouseholdAddress::class, 'person_address', Join::WITH, 'IDENTITY(location_history.personLocation) = IDENTITY(person_address.person)')
->join(
Address::class,
'address',
Join::WITH,
'COALESCE(IDENTITY(location_history.addressLocation), IDENTITY(person_address.address)) = address.id'
)
->andWhere(
$qb->expr()->orX(
$qb->expr()->isNull('person_address'),
$qb->expr()->andX(
$qb->expr()->lte('person_address.validFrom', ':now'),
$qb->expr()->orX(
$qb->expr()->isNull('person_address.validTo'),
$qb->expr()->lt('person_address.validTo', ':now')
)
)
)
)
->andWhere(
$qb->expr()->isNull('location_history.endDate')
)
->andWhere(
$qb->expr()->in('address.postcode', ':postal_codes')
)
->setParameter('now', new DateTimeImmutable('now'), Types::DATE_IMMUTABLE)
->setParameter('postal_codes', $postalCodes);
}
return $qb; return $qb;
} }
@ -77,6 +119,18 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC
return $qb->getQuery()->getSingleScalarResult(); return $qb->getQuery()->getSingleScalarResult();
} }
public function countByUserAndPostalCodesOpenedAccompanyingPeriod(?User $user, array $postalCodes): int
{
if (null === $user) {
return 0;
}
return $this->buildQueryOpenedAccompanyingCourseByUser($user, $postalCodes)
->select('COUNT(ap)')
->getQuery()
->getSingleScalarResult();
}
public function countByUserOpenedAccompanyingPeriod(?User $user): int public function countByUserOpenedAccompanyingPeriod(?User $user): int
{ {
if (null === $user) { if (null === $user) {
@ -158,6 +212,24 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC
return $qb->getQuery()->getResult(); return $qb->getQuery()->getResult();
} }
public function findByUserAndPostalCodesOpenedAccompanyingPeriod(?User $user, array $postalCodes, array $orderBy = [], int $limit = 0, int $offset = 50): array
{
if (null === $user) {
return [];
}
$qb = $this->buildQueryOpenedAccompanyingCourseByUser($user);
$qb->setFirstResult($offset)
->setMaxResults($limit);
foreach ($orderBy as $field => $direction) {
$qb->addOrderBy('ap.' . $field, $direction);
}
return $qb->getQuery()->getResult();
}
/** /**
* @return array|AccompanyingPeriod[] * @return array|AccompanyingPeriod[]
*/ */

View File

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Repository; namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Entity\UserJob;
@ -25,6 +26,11 @@ interface AccompanyingPeriodACLAwareRepositoryInterface
*/ */
public function countByUnDispatched(array $jobs, array $services, array $administrativeLocations): int; public function countByUnDispatched(array $jobs, array $services, array $administrativeLocations): int;
/**
* @param array|PostalCode[] $postalCodes
*/
public function countByUserAndPostalCodesOpenedAccompanyingPeriod(?User $user, array $postalCodes): int;
public function countByUserOpenedAccompanyingPeriod(?User $user): int; public function countByUserOpenedAccompanyingPeriod(?User $user): int;
public function findByPerson( public function findByPerson(
@ -43,5 +49,10 @@ interface AccompanyingPeriodACLAwareRepositoryInterface
*/ */
public function findByUnDispatched(array $jobs, array $services, array $administrativeLocations, ?int $limit = null, ?int $offset = null): array; public function findByUnDispatched(array $jobs, array $services, array $administrativeLocations, ?int $limit = null, ?int $offset = null): array;
/**
* @param array|PostalCode[] $postalCodes
*/
public function findByUserAndPostalCodesOpenedAccompanyingPeriod(?User $user, array $postalCodes, array $orderBy = [], int $limit = 0, int $offset = 50): array;
public function findByUserOpenedAccompanyingPeriod(?User $user, array $orderBy = [], int $limit = 0, int $offset = 50): array; public function findByUserOpenedAccompanyingPeriod(?User $user, array $orderBy = [], int $limit = 0, int $offset = 50): array;
} }

View File

@ -14,9 +14,8 @@ namespace Chill\PersonBundle\Repository\SocialWork;
use Chill\PersonBundle\Entity\SocialWork\Evaluation; use Chill\PersonBundle\Entity\SocialWork\Evaluation;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
final class EvaluationRepository implements ObjectRepository final class EvaluationRepository implements EvaluationRepositoryInterface
{ {
private EntityRepository $repository; private EntityRepository $repository;
@ -38,6 +37,11 @@ final class EvaluationRepository implements ObjectRepository
return $this->repository->findAll(); return $this->repository->findAll();
} }
public function findAllActive(): array
{
return $this->findBy(['active' => true]);
}
/** /**
* @param mixed|null $limit * @param mixed|null $limit
* @param mixed|null $offset * @param mixed|null $offset

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Repository\SocialWork;
use Chill\PersonBundle\Entity\SocialWork\Evaluation;
use Doctrine\Persistence\ObjectRepository;
interface EvaluationRepositoryInterface extends ObjectRepository
{
public function find($id, ?int $lockMode = null, ?int $lockVersion = null): ?Evaluation;
/**
* @return array<int, Evaluation>
*/
public function findAll(): array;
/**
* @return array<int, Evaluation>
*/
public function findAllActive(): array;
/**
* @param mixed|null $limit
* @param mixed|null $offset
*
* @return array<int, Evaluation>
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array;
public function findOneBy(array $criteria, ?array $orderBy = null): ?Evaluation;
/**
* @return class-string
*/
public function getClassName(): string;
}

View File

@ -13,6 +13,7 @@ namespace Chill\PersonBundle\Repository\SocialWork;
use Chill\PersonBundle\Entity\SocialWork\Goal; use Chill\PersonBundle\Entity\SocialWork\Goal;
use Chill\PersonBundle\Entity\SocialWork\SocialAction; use Chill\PersonBundle\Entity\SocialWork\SocialAction;
use DateTime;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
@ -62,17 +63,21 @@ final class GoalRepository implements ObjectRepository
} }
/** /**
* @param mixed|null $orderBy
* @param mixed|null $limit
* @param mixed|null $offset
*
* @return Goal[] * @return Goal[]
*/ */
public function findBySocialActionWithDescendants(SocialAction $action, $orderBy = null, $limit = null, $offset = null): array public function findBySocialActionWithDescendants(SocialAction $action, array $orderBy = [], ?int $limit = null, ?int $offset = null): array
{ {
$qb = $this->buildQueryBySocialActionWithDescendants($action); $qb = $this->buildQueryBySocialActionWithDescendants($action);
$qb->select('g'); $qb->select('g');
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()->isNull('g.desactivationDate'),
$qb->expr()->gt('g.desactivationDate', ':now')
)
)
->setParameter('now', new DateTime('now'));
foreach ($orderBy as $sort => $order) { foreach ($orderBy as $sort => $order) {
$qb->addOrderBy('g.' . $sort, $order); $qb->addOrderBy('g.' . $sort, $order);
} }

View File

@ -96,13 +96,9 @@ final class ResultRepository implements ObjectRepository
} }
/** /**
* @param mixed|null $orderBy
* @param mixed|null $limit
* @param mixed|null $offset
*
* @return Result[] * @return Result[]
*/ */
public function findBySocialActionWithDescendants(SocialAction $action, $orderBy = null, $limit = null, $offset = null): array public function findBySocialActionWithDescendants(SocialAction $action, array $orderBy = [], ?int $limit = null, ?int $offset = null): array
{ {
$qb = $this->buildQueryBySocialActionWithDescendants($action); $qb = $this->buildQueryBySocialActionWithDescendants($action);
$qb->select('r'); $qb->select('r');

View File

@ -76,7 +76,7 @@
<div class="flex-table"> <div class="flex-table">
{% for period in periods %} {% for period in periods %}
{% include '@ChillPerson/AccompanyingPeriod/_list_item.html.twig' with {'period': period, {% include '@ChillPerson/AccompanyingPeriod/_list_item.html.twig' with {'period': period,
'recordAction': m.period_actions(period), 'itemMeta': m.period_meta(period) } %} 'recordAction': m.period_actions(period), 'itemMeta': m.period_meta(period), 'show_address': true } %}
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}

View File

@ -113,6 +113,16 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if show_address|default(false) and period.location is not null %}
<div class="wl-row">
<div class="wl-col title"><h3>{{ 'Accompanying course location'|trans }}</h3></div>
<div class="wl-col list">
<p class="wl-item">
{{ period.location|chill_entity_render_string }}
</p>
</div>
</div>
{% endif %}
</div> </div>
</div> </div>
{% endif %} {% endif %}

View File

@ -5,11 +5,13 @@
{% block js %} {% block js %}
{{ encore_entry_script_tags('mod_set_referrer') }} {{ encore_entry_script_tags('mod_set_referrer') }}
{{ encore_entry_script_tags('mod_pickentity_type') }} {{ encore_entry_script_tags('mod_pickentity_type') }}
{{ encore_entry_script_tags('mod_pick_postal_code') }}
{% endblock %} {% endblock %}
{% block css %} {% block css %}
{{ encore_entry_link_tags('mod_set_referrer') }} {{ encore_entry_link_tags('mod_set_referrer') }}
{{ encore_entry_link_tags('mod_pickentity_type') }} {{ encore_entry_link_tags('mod_pickentity_type') }}
{{ encore_entry_link_tags('mod_pick_postal_code') }}
{% endblock %} {% endblock %}
{% macro period_meta(period) %} {% macro period_meta(period) %}
@ -48,6 +50,8 @@
{{ form_start(form) }} {{ form_start(form) }}
{{ form_label(form.user ) }} {{ form_label(form.user ) }}
{{ form_widget(form.user, {'attr': {'class': 'select2'}}) }} {{ form_widget(form.user, {'attr': {'class': 'select2'}}) }}
{{ form_label(form.postal_code) }}
{{ form_widget(form.postal_code) }}
<ul class="record_actions"> <ul class="record_actions">
<li> <li>
<button type="submit" class="btn btn-misc"> <button type="submit" class="btn btn-misc">
@ -87,7 +91,7 @@
<div class="flex-table"> <div class="flex-table">
{% for period in periods %} {% for period in periods %}
{% include '@ChillPerson/AccompanyingPeriod/_list_item.html.twig' with {'period': period, {% include '@ChillPerson/AccompanyingPeriod/_list_item.html.twig' with {'period': period,
'recordAction': m.period_actions(period), 'itemMeta': m.period_meta(period) } %} 'recordAction': m.period_actions(period), 'itemMeta': m.period_meta(period), 'show_address': true } %}
{% else %} {% else %}
{% if userFrom is same as(null) %} {% if userFrom is same as(null) %}
<p class="chill-no-data-statement">{{ 'period_by_user_list.Pick a user'|trans }}</p> <p class="chill-no-data-statement">{{ 'period_by_user_list.Pick a user'|trans }}</p>

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Service\DocGenerator; namespace Chill\PersonBundle\Service\DocGenerator;
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithAdminFormInterface; use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithAdminFormInterface;
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
use Chill\DocGeneratorBundle\Context\Exception\UnexpectedTypeException; use Chill\DocGeneratorBundle\Context\Exception\UnexpectedTypeException;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocGeneratorBundle\Service\Context\BaseContextData; use Chill\DocGeneratorBundle\Service\Context\BaseContextData;
@ -19,46 +20,72 @@ use Chill\DocStoreBundle\Entity\DocumentCategory;
use Chill\DocStoreBundle\Entity\PersonDocument; use Chill\DocStoreBundle\Entity\PersonDocument;
use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\DocumentCategoryRepository; use Chill\DocStoreBundle\Repository\DocumentCategoryRepository;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Form\Type\ScopePickerType;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use DateTime; use DateTime;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use LogicException;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
use function array_key_exists; use function array_key_exists;
use function count;
class PersonContext implements DocGeneratorContextWithAdminFormInterface class PersonContext implements DocGeneratorContextWithAdminFormInterface, DocGeneratorContextWithPublicFormInterface
{ {
private AuthorizationHelperInterface $authorizationHelper;
private BaseContextData $baseContextData; private BaseContextData $baseContextData;
private CenterResolverManagerInterface $centerResolverManager;
private DocumentCategoryRepository $documentCategoryRepository; private DocumentCategoryRepository $documentCategoryRepository;
private EntityManagerInterface $em; private EntityManagerInterface $em;
private NormalizerInterface $normalizer; private NormalizerInterface $normalizer;
private Security $security;
private bool $showScopes;
private TranslatableStringHelperInterface $translatableStringHelper; private TranslatableStringHelperInterface $translatableStringHelper;
private TranslatorInterface $translator; private TranslatorInterface $translator;
public function __construct( public function __construct(
AuthorizationHelperInterface $authorizationHelper,
BaseContextData $baseContextData,
CenterResolverManagerInterface $centerResolverManager,
DocumentCategoryRepository $documentCategoryRepository, DocumentCategoryRepository $documentCategoryRepository,
NormalizerInterface $normalizer,
TranslatableStringHelperInterface $translatableStringHelper,
EntityManagerInterface $em, EntityManagerInterface $em,
NormalizerInterface $normalizer,
ParameterBagInterface $parameterBag,
Security $security,
TranslatorInterface $translator, TranslatorInterface $translator,
BaseContextData $baseContextData TranslatableStringHelperInterface $translatableStringHelper
) { ) {
$this->documentCategoryRepository = $documentCategoryRepository; $this->authorizationHelper = $authorizationHelper;
$this->normalizer = $normalizer; $this->centerResolverManager = $centerResolverManager;
$this->translatableStringHelper = $translatableStringHelper;
$this->em = $em;
$this->baseContextData = $baseContextData; $this->baseContextData = $baseContextData;
$this->documentCategoryRepository = $documentCategoryRepository;
$this->em = $em;
$this->normalizer = $normalizer;
$this->security = $security;
$this->showScopes = $parameterBag->get('chill_main')['acl']['form_show_scopes'];
$this->translator = $translator; $this->translator = $translator;
$this->translatableStringHelper = $translatableStringHelper;
} }
public function adminFormReverseTransform(array $data): array public function adminFormReverseTransform(array $data): array
@ -105,6 +132,18 @@ class PersonContext implements DocGeneratorContextWithAdminFormInterface
]); ]);
} }
/**
* @param Person $entity
*/
public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void
{
$builder->add('scope', ScopePickerType::class, [
'center' => $this->centerResolverManager->resolveCenters($entity),
'role' => PersonDocumentVoter::CREATE,
'label' => 'Scope',
]);
}
public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array
{ {
if (!$entity instanceof Person) { if (!$entity instanceof Person) {
@ -156,6 +195,14 @@ class PersonContext implements DocGeneratorContextWithAdminFormInterface
return true; return true;
} }
/**
* @param Person $entity
*/
public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool
{
return $this->isScopeNecessary($entity);
}
/** /**
* @param Person $entity * @param Person $entity
*/ */
@ -178,6 +225,36 @@ class PersonContext implements DocGeneratorContextWithAdminFormInterface
); );
} }
if ($this->isScopeNecessary($entity)) {
$doc->setScope($contextGenerationData['scope']);
} elseif ($this->showScopes) {
// in this case, it should have only one scope possible, we get it through AuthorizationHelper::getReachableScopes
$scopes = $this->authorizationHelper->getReachableScopes(
$this->security->getUser(),
PersonDocumentVoter::CREATE,
$this->centerResolverManager->resolveCenters($entity)
);
if (1 !== count($scopes)) {
throw new LogicException('at this step, it should have only one scope');
}
$doc->setScope($scopes[0]);
}
$this->em->persist($doc); $this->em->persist($doc);
} }
private function isScopeNecessary(Person $person): bool
{
if ($this->showScopes && 1 < $this->authorizationHelper->getReachableScopes(
$this->security->getUser(),
PersonDocumentVoter::CREATE,
$this->centerResolverManager->resolveCenters($person)
)) {
return true;
}
return false;
}
} }

View File

@ -193,7 +193,7 @@ final class SocialWorkMetadata implements SocialWorkMetadataInterface
/** @var Evaluation $eval */ /** @var Evaluation $eval */
$eval = $this->getOrCreateEntity($this->evaluationRepository, 'title', ['fr' => $evaluationTitle]); $eval = $this->getOrCreateEntity($this->evaluationRepository, 'title', ['fr' => $evaluationTitle]);
$eval->setTitle(['fr' => $evaluationTitle]); $eval->setTitle(['fr' => $evaluationTitle]);
$eval->addSocialAction($socialAction); $socialAction->addEvaluation($eval);
$this->entityManager->persist($eval); $this->entityManager->persist($eval);

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Tests\Controller;
use Chill\MainBundle\Test\PrepareClientTrait;
use Chill\PersonBundle\Entity\SocialWork\Evaluation;
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
/**
* @internal
* @coversNothing
*/
final class SocialWorkEvaluationApiControllerTest extends WebTestCase
{
use PrepareClientTrait;
private EntityManagerInterface $em;
private ?Evaluation $evaluationToReset = null;
protected function tearDown(): void
{
if (null === $this->evaluationToReset) {
return;
}
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
$evaluation = $em->find(Evaluation::class, $this->evaluationToReset->getId());
$evaluation->setActive(true);
$em->flush();
}
public function dataGenerateSocialActionWithEvaluations(): iterable
{
self::bootKernel();
$this->em = self::$container->get(EntityManagerInterface::class);
/** @var SocialAction $socialAction */
$socialAction = $this->em->createQuery(
'SELECT s FROM ' . SocialAction::class . ' s WHERE SIZE(s.evaluations) >= 2'
)
->setMaxResults(1)
->getSingleResult();
// set the first evaluation as inactive and save
$this->evaluationToReset = $socialAction->getEvaluations()->first();
$this->evaluationToReset->setActive(false);
$this->em->flush();
yield [$socialAction, $this->evaluationToReset];
}
/**
* @dataProvider dataGenerateSocialActionWithEvaluations
*/
public function testListEvaluationBySocialAction(SocialAction $action, Evaluation $inactiveEvaluation): void
{
$client = $this->getClientAuthenticated();
$client->request('GET', sprintf('/api/1.0/person/social-work/evaluation/by-social-action/%d.json', $action->getId()));
$this->assertResponseIsSuccessful();
$content = json_decode($client->getResponse()->getContent(), true);
$ids = array_map(static fn (array $item) => $item['id'], $content['results']);
$this->assertNotContains($inactiveEvaluation->getId(), $ids);
}
}

View File

@ -29,6 +29,38 @@ use function count;
*/ */
final class AccompanyingPeriodTest extends \PHPUnit\Framework\TestCase final class AccompanyingPeriodTest extends \PHPUnit\Framework\TestCase
{ {
public function testChangeStepKeepHistory()
{
$period = new AccompanyingPeriod();
$this->assertCount(0, $period->getStepHistories(), 'at initialization, period should not have any step history');
$period->setStep(AccompanyingPeriod::STEP_DRAFT);
$this->assertCount(0, $period->getStepHistories(), 're applying a draft should not create a history');
$period->setStep(AccompanyingPeriod::STEP_CONFIRMED);
$this->assertCount(1, $period->getStepHistories());
$this->assertEquals(AccompanyingPeriod::STEP_CONFIRMED, $period->getStepHistories()->first()->getStep());
$period->setOpeningDate($aMonthAgo = new DateTime('1 month ago'));
$this->assertCount(1, $period->getStepHistories());
$this->assertEquals($aMonthAgo, $period->getStepHistories()->first()->getStartDate(), 'when changing the opening date, the start date of the first history should change');
$period->setOpeningDate($tenDaysAgo = new DateTime('10 days ago'));
$this->assertCount(1, $period->getStepHistories());
$this->assertEquals($tenDaysAgo, $period->getStepHistories()->first()->getStartDate(), 'when changing the opening date, the start date of the first history should change');
$period->setStep(AccompanyingPeriod::STEP_CLOSED);
$this->assertCount(2, $period->getStepHistories());
$period->setOpeningDate($tomorrow = new DateTime('tomorrow'));
$this->assertEquals($tenDaysAgo, $period->getStepHistories()->first()->getStartDate(), 'when changing the opening date to a later one and no history after, start date should change');
}
public function testClosingEqualOpening() public function testClosingEqualOpening()
{ {
$datetime = new DateTime('now'); $datetime = new DateTime('now');

View File

@ -39,7 +39,9 @@ final class DurationAggregatorTest extends AbstractAggregatorTest
public function getFormData(): array public function getFormData(): array
{ {
return [ return [
[], ['precision' => 'day'],
['precision' => 'week'],
['precision' => 'month'],
]; ];
} }

View File

@ -14,6 +14,7 @@ namespace Chill\PersonBundle\Tests\Export\Aggregator\AccompanyingCourseAggregato
use Chill\MainBundle\Test\Export\AbstractAggregatorTest; use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\GeographicalUnitStatAggregator; use Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\GeographicalUnitStatAggregator;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
/** /**
@ -38,8 +39,9 @@ final class GeographicalUnitStatAggregatorTest extends AbstractAggregatorTest
public function getFormData(): array public function getFormData(): array
{ {
// TODO: add geographical unit stat into fixtures and provide a level
return [ return [
[], ['date_calc' => new DateTimeImmutable('today'), 'level' => null],
]; ];
} }

View File

@ -14,6 +14,7 @@ namespace Chill\PersonBundle\Tests\Export\Aggregator\AccompanyingCourseAggregato
use Chill\MainBundle\Test\Export\AbstractAggregatorTest; use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ReferrerAggregator; use Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ReferrerAggregator;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
/** /**
@ -39,7 +40,7 @@ final class ReferrerAggregatorTest extends AbstractAggregatorTest
public function getFormData(): array public function getFormData(): array
{ {
return [ return [
[], ['date_calc' => new DateTimeImmutable('now')],
]; ];
} }

View File

@ -12,7 +12,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Tests\Export\Filter\AccompanyingCourseFilters; namespace Chill\PersonBundle\Tests\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Test\Export\AbstractFilterTest; use Chill\MainBundle\Test\Export\AbstractFilterTest;
use Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\CurrentUserJobFilter; use Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\UserJobFilter;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
/** /**
@ -21,7 +21,7 @@ use Doctrine\ORM\EntityManagerInterface;
*/ */
final class CurrentUserJobFilterTest extends AbstractFilterTest final class CurrentUserJobFilterTest extends AbstractFilterTest
{ {
private CurrentUserJobFilter $filter; private UserJobFilter $filter;
protected function setUp(): void protected function setUp(): void
{ {

View File

@ -12,7 +12,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Tests\Export\Filter\AccompanyingCourseFilters; namespace Chill\PersonBundle\Tests\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Test\Export\AbstractFilterTest; use Chill\MainBundle\Test\Export\AbstractFilterTest;
use Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\CurrentUserScopeFilter; use Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\UserScopeFilter;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
/** /**
@ -21,7 +21,7 @@ use Doctrine\ORM\EntityManagerInterface;
*/ */
final class CurrentUserScopeFilterTest extends AbstractFilterTest final class CurrentUserScopeFilterTest extends AbstractFilterTest
{ {
private CurrentUserScopeFilter $filter; private UserScopeFilter $filter;
protected function setUp(): void protected function setUp(): void
{ {

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Tests\Serializer\Normalizer;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Origin;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* @internal
* @coversNothing
*/
final class AccompanyingPeriodOriginNormalizerTest extends KernelTestCase
{
private NormalizerInterface $normalizer;
protected function setUp(): void
{
self::bootKernel();
$this->normalizer = self::$container->get(NormalizerInterface::class);
}
public function testNormalization()
{
$o = new Origin();
$normalized = $this->normalizer->normalize(
$o,
'json',
['groups' => ['read']]
);
$this->assertIsArray($normalized);
$this->assertArrayHasKey('type', $normalized);
$this->assertEquals('origin', $normalized['type']);
}
}

View File

@ -60,7 +60,7 @@ final class AccompanyingPeriodWorkDocGenNormalizerTest extends KernelTestCase
} }
} }
public function testNormlalize() public function testNormalize()
{ {
$work = new AccompanyingPeriodWork(); $work = new AccompanyingPeriodWork();
$work $work

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Tests\Serializer\Normalizer;
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* @internal
* @coversNothing
*/
final class SocialActionNormalizerTest extends KernelTestCase
{
private NormalizerInterface $normalizer;
protected function setUp(): void
{
self::bootKernel();
$this->normalizer = self::$container->get(NormalizerInterface::class);
}
public function testNormalization()
{
$sa = new SocialAction();
$normalized = $this->normalizer->normalize(
$sa,
'json',
['groups' => ['read']]
);
$this->assertIsArray($normalized);
$this->assertArrayHasKey('type', $normalized);
$this->assertEquals('social_work_social_action', $normalized['type']);
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Tests\Serializer\Normalizer;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* @internal
* @coversNothing
*/
final class SocialIssueNormalizerTest extends KernelTestCase
{
private NormalizerInterface $normalizer;
protected function setUp(): void
{
self::bootKernel();
$this->normalizer = self::$container->get(NormalizerInterface::class);
}
public function testNormalization()
{
$si = new SocialIssue();
$normalized = $this->normalizer->normalize(
$si,
'json',
['groups' => ['read']]
);
$this->assertIsArray($normalized);
$this->assertArrayHasKey('type', $normalized);
$this->assertEquals('social_issue', $normalized['type']);
}
}

View File

@ -0,0 +1,265 @@
<?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 Service\DocGenerator;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocGeneratorBundle\Service\Context\BaseContextData;
use Chill\DocStoreBundle\Entity\DocumentCategory;
use Chill\DocStoreBundle\Entity\PersonDocument;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\DocumentCategoryRepository;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Service\DocGenerator\PersonContext;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Exception\Prediction\FailedPredictionException;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function count;
/**
* @internal
* @coversNothing
*/
final class PersonContextTest extends TestCase
{
use ProphecyTrait;
/**
* Test that the build person context works in the case when 'form_show_scope' is false.
*/
public function testScopeDoNotShowScopeInForms()
{
$person = new Person();
$docGen = (new DocGeneratorTemplate())
->setName(['fr' => 'template']);
$parameter = new ParameterBag(['chill_main' => ['acl' => ['form_show_scopes' => false]]]);
$em = $this->prophesize(EntityManagerInterface::class);
$em->persist(Argument::type(PersonDocument::class))
->should(static function ($calls, $object, $method) {
if (1 !== count($calls)) {
throw new FailedPredictionException(sprintf('the persist should be called exactly once, %d receivved', count($calls)));
}
/** @var PersonDocument $personDocument */
$personDocument = $calls[0]->getArguments()[0];
if (null !== $personDocument->getScope()) {
throw new FailedPredictionException('the person document should not have any scope');
}
});
$personContext = $this->buildPersonContext(
null,
null,
null,
null,
$em->reveal(),
null,
$parameter
);
$this->assertFalse($personContext->hasPublicForm($docGen, $person));
$personContext->storeGenerated(
$docGen,
new StoredObject(),
$person,
[]
);
}
public function testScopeScopeMustBeShownInFormsAndUserAccessMultipleScope()
{
$person = new Person();
$docGen = (new DocGeneratorTemplate())
->setName(['fr' => 'template']);
$scope = new Scope();
$em = $this->prophesize(EntityManagerInterface::class);
$em->persist(Argument::type(PersonDocument::class))
->should(static function ($calls, $object, $method) use ($scope) {
if (1 !== count($calls)) {
throw new FailedPredictionException(sprintf('the persist should be called exactly once, %d receivved', count($calls)));
}
/** @var PersonDocument $personDocument */
$personDocument = $calls[0]->getArguments()[0];
if ($personDocument->getScope() !== $scope) {
throw new FailedPredictionException('the person document should show the exactly prepared scope');
}
});
$authorizationHelper = $this->prophesize(AuthorizationHelperInterface::class);
$authorizationHelper->getReachableScopes(Argument::type(UserInterface::class), PersonDocumentVoter::CREATE, Argument::type('array'))
->willReturn([$scope, new Scope()]);
$personContext = $this->buildPersonContext(
$authorizationHelper->reveal(),
null,
null,
null,
$em->reveal(),
);
$this->assertTrue($personContext->hasPublicForm($docGen, $person));
$personContext->storeGenerated(
$docGen,
new StoredObject(),
$person,
['scope' => $scope]
);
}
public function testScopeScopeMustBeShownInFormsAndUserAccessOneScope()
{
$person = new Person();
$docGen = (new DocGeneratorTemplate())
->setName(['fr' => 'template']);
$scope = new Scope();
$em = $this->prophesize(EntityManagerInterface::class);
$em->persist(Argument::type(PersonDocument::class))
->should(static function ($calls, $object, $method) use ($scope) {
if (1 !== count($calls)) {
throw new FailedPredictionException(sprintf('the persist should be called exactly once, %d receivved', count($calls)));
}
/** @var PersonDocument $personDocument */
$personDocument = $calls[0]->getArguments()[0];
if ($personDocument->getScope() !== $scope) {
throw new FailedPredictionException('the person document should show the exactly prepared scope');
}
});
$authorizationHelper = $this->prophesize(AuthorizationHelperInterface::class);
$authorizationHelper->getReachableScopes(Argument::type(UserInterface::class), PersonDocumentVoter::CREATE, Argument::type('array'))
->willReturn([$scope]);
$personContext = $this->buildPersonContext(
$authorizationHelper->reveal(),
null,
null,
null,
$em->reveal(),
);
$this->assertTrue($personContext->hasPublicForm($docGen, $person));
$personContext->storeGenerated(
$docGen,
new StoredObject(),
$person,
['scope' => $scope]
);
}
private function buildPersonContext(
?AuthorizationHelperInterface $authorizationHelper = null,
?BaseContextData $baseContextData = null,
?CenterResolverManagerInterface $centerResolverManager = null,
?DocumentCategoryRepository $documentCategoryRepository = null,
?EntityManagerInterface $em = null,
?NormalizerInterface $normalizer = null,
?ParameterBagInterface $parameterBag = null,
?Security $security = null,
?TranslatorInterface $translator = null,
?TranslatableStringHelperInterface $translatableStringHelper = null
): PersonContext {
if (null === $authorizationHelper) {
$authorizationHelper = $this->prophesize(AuthorizationHelperInterface::class)->reveal();
}
if (null === $baseContextData) {
$baseContextData = $this->prophesize(BaseContextData::class)->reveal();
}
if (null === $centerResolverManager) {
$centerResolverManager = $this->prophesize(CenterResolverManagerInterface::class);
$centerResolverManager->resolveCenters(Argument::any(), Argument::any())
->willReturn([new Center()]);
$centerResolverManager = $centerResolverManager->reveal();
}
if (null === $documentCategoryRepository) {
$documentCategoryRepository = $this->prophesize(DocumentCategoryRepository::class);
$documentCategoryRepository->find(Argument::type('integer'))->willReturn(
new DocumentCategory(PersonDocument::class, 1)
);
$documentCategoryRepository = $documentCategoryRepository->reveal();
}
if (null === $em) {
$em = $this->prophesize(EntityManagerInterface::class)->reveal();
}
if (null === $normalizer) {
$normalizer = $this->prophesize(NormalizerInterface::class);
$normalizer->normalize(Argument::type(Person::class), 'docgen', Argument::any())
->willReturn(['type' => 'person']);
$normalizer = $normalizer->reveal();
}
if (null === $parameterBag) {
$parameterBag = new ParameterBag(['chill_main' => ['acl' => ['form_show_scopes' => true]]]);
}
if (null === $security) {
$security = $this->prophesize(Security::class);
$security->getUser()->willReturn(new User());
$security = $security->reveal();
}
if (null === $translator) {
$translator = $this->prophesize(TranslatorInterface::class)->reveal();
}
if (null === $translatableStringHelper) {
$translatableStringHelper = $this->prophesize(TranslatableStringHelperInterface::class);
// return only the 'fr' key
$translatableStringHelper->localize(Argument::type('array'))->will(static function ($args) {
return $args[0]['fr'];
});
$translatableStringHelper = $translatableStringHelper->reveal();
}
return new PersonContext(
$authorizationHelper,
$baseContextData,
$centerResolverManager,
$documentCategoryRepository,
$em,
$normalizer,
$parameterBag,
$security,
$translator,
$translatableStringHelper
);
}
}

View File

@ -16,19 +16,17 @@ services:
- { name: chill.export, alias: avg_accompanyingcourse_duration } - { name: chill.export, alias: avg_accompanyingcourse_duration }
## Filters ## Filters
chill.person.export.filter_current_userscope: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\UserScopeFilter:
class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\CurrentUserScopeFilter
autowire: true autowire: true
autoconfigure: true autoconfigure: true
tags: tags:
- { name: chill.export_filter, alias: accompanyingcourse_current_userscope_filter } - { name: chill.export_filter, alias: accompanyingcourse_userscope_filter }
chill.person.export.filter_current_userjob: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\UserJobFilter:
class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\CurrentUserJobFilter
autowire: true autowire: true
autoconfigure: true autoconfigure: true
tags: tags:
- { name: chill.export_filter, alias: accompanyingcourse_current_userjob_filter } - { name: chill.export_filter, alias: accompanyingcourse_userjob_filter }
chill.person.export.filter_socialissue: chill.person.export.filter_socialissue:
class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\SocialIssueFilter class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\SocialIssueFilter

View File

@ -0,0 +1,33 @@
<?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\Migrations\Person;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20221013131221 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_social_work_evaluation DROP active');
}
public function getDescription(): string
{
return 'Add an active column on evaluation';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_social_work_evaluation ADD active BOOLEAN DEFAULT true NOT NULL');
}
}

View File

@ -0,0 +1,69 @@
<?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\Migrations\Person;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20221014115500 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('DROP SEQUENCE chill_person_accompanying_period_step_history_id_seq CASCADE');
$this->addSql('DROP TABLE chill_person_accompanying_period_step_history');
}
public function getDescription(): string
{
return 'Add step history on accompanying periods';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE SEQUENCE chill_person_accompanying_period_step_history_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE chill_person_accompanying_period_step_history (id INT NOT NULL, period_id INT DEFAULT NULL,
endDate DATE DEFAULT NULL, startDate DATE NOT NULL, step TEXT NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL,
updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, createdBy_id INT DEFAULT NULL,
updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))
');
$this->addSql('ALTER TABLE chill_person_accompanying_period_step_history ADD CHECK (startDate <= endDate)');
$this->addSql('ALTER TABLE chill_person_accompanying_period_step_history ADD CONSTRAINT ' .
'chill_internal_acp_steps_not_overlaps EXCLUDE USING GIST(
-- extension btree_gist required to include comparaison with integer
period_id WITH =,
daterange(startDate, endDate, \'[)\') WITH &&
)
INITIALLY DEFERRED');
$this->addSql('CREATE INDEX IDX_84D514ACEC8B7ADE ON chill_person_accompanying_period_step_history (period_id)');
$this->addSql('CREATE INDEX IDX_84D514AC3174800F ON chill_person_accompanying_period_step_history (createdBy_id)');
$this->addSql('COMMENT ON COLUMN chill_person_accompanying_period_step_history.endDate IS \'(DC2Type:date_immutable)\'');
$this->addSql('COMMENT ON COLUMN chill_person_accompanying_period_step_history.startDate IS \'(DC2Type:date_immutable)\'');
$this->addSql('COMMENT ON COLUMN chill_person_accompanying_period_step_history.createdAt IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN chill_person_accompanying_period_step_history.updatedAt IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE chill_person_accompanying_period_step_history ADD CONSTRAINT FK_84D514ACEC8B7ADE FOREIGN KEY (period_id) REFERENCES chill_person_accompanying_period (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_person_accompanying_period_step_history ADD CONSTRAINT FK_84D514AC3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX IDX_84D514AC65FF1AEC ON chill_person_accompanying_period_step_history (updatedBy_id)');
// fill the tables with current state
$this->addSql(
'INSERT INTO chill_person_accompanying_period_step_history (id, period_id, startDate, endDate, step, createdAt, updatedAt)
SELECT nextval(\'chill_person_accompanying_period_step_history_id_seq\'), id, openingDate, null, step, NOW(), NOW() FROM chill_person_accompanying_period WHERE step = \'CONFIRMED\'
UNION
SELECT nextval(\'chill_person_accompanying_period_step_history_id_seq\'), id, openingDate, closingDate, \'CONFIRMED\', NOW(), NOW() FROM chill_person_accompanying_period WHERE step = \'CLOSED\'
UNION
SELECT nextval(\'chill_person_accompanying_period_step_history_id_seq\'), id, closingDate, null, \'CLOSED\', NOW(), NOW() FROM chill_person_accompanying_period WHERE step = \'CLOSED\'
'
);
}
}

View File

@ -939,6 +939,8 @@ reassign:
All periods on this list will be reassigned to this user, excepted the one you manually reassigned before: Tous les parcours visibles sur cette page seront assignés à cet utilisateur, sauf ceux que vous aurez assigné à un utilisateur manuellement. All periods on this list will be reassigned to this user, excepted the one you manually reassigned before: Tous les parcours visibles sur cette page seront assignés à cet utilisateur, sauf ceux que vous aurez assigné à un utilisateur manuellement.
Reassign: Assigner le référent Reassign: Assigner le référent
List periods to be able to reassign them: Choisissez un utilisateur et cliquez sur "Filtrer" pour visualiser ses parcours. Vous pourrez ensuite les réassigner. List periods to be able to reassign them: Choisissez un utilisateur et cliquez sur "Filtrer" pour visualiser ses parcours. Vous pourrez ensuite les réassigner.
Filter by postal code: Filtrer par code postal
Filter course which are located inside a postal code: Afficher uniquement les parcours localisés auprès de ce code postal (une commune peut comporter plusieurs codes postaux).
notification: notification:
Notify referrer: Notifier le référent Notify referrer: Notifier le référent
@ -947,11 +949,24 @@ notification:
export: export:
aggregator: aggregator:
course: course:
by_referrer:
Computation date for referrer: Date à laquelle le référent était actif
by_user_scope: by_user_scope:
Group course by referrer's scope: Grouper les parcours par service du référent Group course by referrer's scope: Grouper les parcours par service du référent
Computation date for referrer: Date à laquelle le référent était actif Computation date for referrer: Date à laquelle le référent était actif
Referrer's scope: Service du référent de parcours Referrer's scope: Service du référent de parcours
duration:
day: Durée du parcours en jours
week: Durée du parcours en semaines
month: Durée du parcours en mois
Precision: Unité de la durée
filter:
course:
by_user_scope:
Computation date for referrer: Date à laquelle le référent était actif
by_referrer:
Computation date for referrer: Date à laquelle le référent était actif
social_action: social_action:
and children: et dérivés and children: et dérivés