mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-10 16:55:00 +00:00
Compare commits
34 Commits
2.14.1
...
async-uplo
Author | SHA1 | Date | |
---|---|---|---|
3fb04594eb
|
|||
1065706e60
|
|||
f131572344
|
|||
45e1ce034a
|
|||
82ca321715
|
|||
28d09a8206
|
|||
1195767eb3
|
|||
4fd5a37df3
|
|||
450e7c348b
|
|||
a70572266f
|
|||
d688022825
|
|||
264fff5c36
|
|||
91e6b035bd
|
|||
2adb6105eb | |||
33d187f329 | |||
8cc93a8b07
|
|||
f7184ca7bb
|
|||
dab80a84d8 | |||
68e00dc42f | |||
5fae49821f | |||
0e599a99a7 | |||
f0605c6b08
|
|||
54606403b4
|
|||
47d829d72d
|
|||
fa3b305ab9 | |||
b26b7a2706 | |||
fe54e51362
|
|||
179e3e92ed
|
|||
c00c6066a9 | |||
f424c5464f
|
|||
b17b2a8cfb
|
|||
00de657cae
|
|||
673518e0eb | |||
4700a0fef7 |
5
.changes/unreleased/Feature-20231212-154841.yaml
Normal file
5
.changes/unreleased/Feature-20231212-154841.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
kind: Feature
|
||||
body: '[DX] move async-upload-bundle features into chill-bundles'
|
||||
time: 2023-12-12T15:48:41.954970271+01:00
|
||||
custom:
|
||||
Issue: "221"
|
11
.changes/v2.15.0.md
Normal file
11
.changes/v2.15.0.md
Normal file
@@ -0,0 +1,11 @@
|
||||
## v2.15.0 - 2023-12-11
|
||||
### Feature
|
||||
* ([#191](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/191)) Add export "number of household associate with an exchange"
|
||||
* ([#235](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/235)) Export: add dates on the filter "filter course by activity type"
|
||||
### Fixed
|
||||
* ([#214](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/214)) Fix error when posting an empty comment on an accompanying period.
|
||||
* ([#233](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/233)) Fix "filter evaluation by evaluation type" (and add select2 to the list of evaluation types to pick)
|
||||
* ([#234](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/234)) Fix "filter aside activity by date"
|
||||
|
||||
* ([#228](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/228)) Fix export of activity for people created before the introduction of the createdAt column on person (during v1)
|
||||
* ([#246](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/246)) Do not show activities, evaluations and social work when associated to a confidential accompanying period, except for the users which are allowed to see them
|
18
CHANGELOG.md
18
CHANGELOG.md
@@ -6,6 +6,24 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
|
||||
|
||||
## v2.15.0 - 2023-12-11
|
||||
### Feature
|
||||
* ([#191](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/191)) Add export "number of household associate with an exchange"
|
||||
* ([#235](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/235)) Export: add dates on the filter "filter course by activity type"
|
||||
### Fixed
|
||||
* ([#214](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/214)) Fix error when posting an empty comment on an accompanying period.
|
||||
* ([#233](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/233)) Fix "filter evaluation by evaluation type" (and add select2 to the list of evaluation types to pick)
|
||||
* ([#234](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/234)) Fix "filter aside activity by date"
|
||||
|
||||
* ([#228](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/228)) Fix export of activity for people created before the introduction of the createdAt column on person (during v1)
|
||||
* ([#246](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/246)) Do not show activities, evaluations and social work when associated to a confidential accompanying period, except for the users which are allowed to see them
|
||||
|
||||
## v2.14.1 - 2023-11-29
|
||||
### Fixed
|
||||
* Export: fix list person with custom fields
|
||||
* ([#100](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/100)) Add a paginator to budget elements (resource and charge types) in the admin
|
||||
* Fix error in ListEvaluation when "handling agents" are alone
|
||||
|
||||
## v2.14.0 - 2023-11-24
|
||||
### Feature
|
||||
* ([#161](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/161)) Export: in filter "Filter accompanying period work (social action) by type, goal and result", order the items alphabetically or with the defined order
|
||||
|
@@ -12,7 +12,6 @@
|
||||
"ext-json": "*",
|
||||
"ext-openssl": "*",
|
||||
"ext-redis": "*",
|
||||
"champs-libres/async-uploader-bundle": "dev-sf4#d57134aee8e504a83c902ff0cf9f8d36ac418290",
|
||||
"champs-libres/wopi-bundle": "dev-master@dev",
|
||||
"champs-libres/wopi-lib": "dev-master@dev",
|
||||
"doctrine/doctrine-bundle": "^2.1",
|
||||
|
@@ -0,0 +1,147 @@
|
||||
<?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\Export\LinkedToACP;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\ActivityBundle\Export\Declarations;
|
||||
use Chill\ActivityBundle\Security\Authorization\ActivityStatsVoter;
|
||||
use Chill\MainBundle\Export\AccompanyingCourseExportHelper;
|
||||
use Chill\MainBundle\Export\ExportInterface;
|
||||
use Chill\MainBundle\Export\FormatterInterface;
|
||||
use Chill\MainBundle\Export\GroupedExportInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
|
||||
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
||||
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
|
||||
use Chill\PersonBundle\Export\Declarations as PersonDeclarations;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Query;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
final readonly class CountHouseholdOnActivity implements ExportInterface, GroupedExportInterface
|
||||
{
|
||||
private EntityRepository $repository;
|
||||
|
||||
private bool $filterStatsByCenters;
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $em,
|
||||
ParameterBagInterface $parameterBag,
|
||||
) {
|
||||
$this->repository = $em->getRepository(Activity::class);
|
||||
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder) {}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getAllowedFormattersTypes(): array
|
||||
{
|
||||
return [FormatterInterface::TYPE_TABULAR];
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'export.export.count_household_on_activity.description';
|
||||
}
|
||||
|
||||
public function getGroup(): string
|
||||
{
|
||||
return 'Exports of activities linked to an accompanying period';
|
||||
}
|
||||
|
||||
public function getLabels($key, array $values, $data)
|
||||
{
|
||||
if ('export_count_activity' !== $key) {
|
||||
throw new \LogicException("the key {$key} is not used by this export");
|
||||
}
|
||||
|
||||
return static fn ($value) => '_header' === $value ? 'export.export.count_household_on_activity.header' : $value;
|
||||
}
|
||||
|
||||
public function getQueryKeys($data): array
|
||||
{
|
||||
return ['export_count_activity'];
|
||||
}
|
||||
|
||||
public function getResult($query, $data)
|
||||
{
|
||||
return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'export.export.count_household_on_activity.title';
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return Declarations::ACTIVITY;
|
||||
}
|
||||
|
||||
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
|
||||
{
|
||||
$centers = array_map(static fn ($el) => $el['center'], $acl);
|
||||
|
||||
$qb = $this->repository
|
||||
->createQueryBuilder('activity')
|
||||
->join('activity.persons', 'person')
|
||||
->join('activity.accompanyingPeriod', 'acp')
|
||||
->join(
|
||||
HouseholdMember::class,
|
||||
'householdmember',
|
||||
Query\Expr\Join::WITH,
|
||||
'person.id = IDENTITY(householdmember.person) AND householdmember.startDate <= activity.date AND (householdmember.endDate IS NULL OR householdmember.endDate > activity.date)'
|
||||
)
|
||||
->join('householdmember.household', 'household');
|
||||
|
||||
if ($this->filterStatsByCenters) {
|
||||
$qb
|
||||
->andWhere(
|
||||
$qb->expr()->exists(
|
||||
'SELECT 1 FROM '.AccompanyingPeriodParticipation::class.' acl_count_part
|
||||
JOIN '.PersonCenterHistory::class.' acl_count_person_history WITH IDENTITY(acl_count_person_history.person) = IDENTITY(acl_count_part.person)
|
||||
WHERE acl_count_part.accompanyingPeriod = acp.id AND acl_count_person_history.center IN (:authorized_centers)
|
||||
'
|
||||
)
|
||||
)
|
||||
->setParameter('authorized_centers', $centers);
|
||||
}
|
||||
|
||||
AccompanyingCourseExportHelper::addClosingMotiveExclusionClause($qb);
|
||||
|
||||
$qb->select('COUNT(DISTINCT household.id) as export_count_activity');
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function requiredRole(): string
|
||||
{
|
||||
return ActivityStatsVoter::STATS;
|
||||
}
|
||||
|
||||
public function supportsModifiers(): array
|
||||
{
|
||||
return [
|
||||
Declarations::ACTIVITY,
|
||||
Declarations::ACTIVITY_ACP,
|
||||
PersonDeclarations::ACP_TYPE,
|
||||
PersonDeclarations::PERSON_TYPE,
|
||||
PersonDeclarations::HOUSEHOLD_TYPE,
|
||||
];
|
||||
}
|
||||
}
|
@@ -20,23 +20,18 @@ use Chill\MainBundle\Export\AccompanyingCourseExportHelper;
|
||||
use Chill\MainBundle\Export\GroupedExportInterface;
|
||||
use Chill\MainBundle\Export\Helper\TranslatableStringExportLabelHelper;
|
||||
use Chill\MainBundle\Export\ListInterface;
|
||||
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
|
||||
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelperInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class ListActivity implements ListInterface, GroupedExportInterface
|
||||
final readonly class ListActivity implements ListInterface, GroupedExportInterface
|
||||
{
|
||||
private readonly bool $filterStatsByCenters;
|
||||
|
||||
public function __construct(
|
||||
private readonly ListActivityHelper $helper,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly TranslatableStringExportLabelHelper $translatableStringExportLabelHelper,
|
||||
ParameterBagInterface $parameterBag,
|
||||
) {
|
||||
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
|
||||
}
|
||||
private ListActivityHelper $helper,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private TranslatableStringExportLabelHelper $translatableStringExportLabelHelper,
|
||||
private FilterListAccompanyingPeriodHelperInterface $filterListAccompanyingPeriodHelper,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
@@ -119,19 +114,7 @@ class ListActivity implements ListInterface, GroupedExportInterface
|
||||
->leftJoin('acppart.person', 'person')
|
||||
->andWhere('acppart.startDate != acppart.endDate OR acppart.endDate IS NULL');
|
||||
|
||||
if ($this->filterStatsByCenters) {
|
||||
$qb
|
||||
->andWhere(
|
||||
$qb->expr()->exists(
|
||||
'SELECT 1
|
||||
FROM '.PersonCenterHistory::class.' acl_count_person_history
|
||||
WHERE acl_count_person_history.person = person
|
||||
AND acl_count_person_history.center IN (:authorized_centers)
|
||||
'
|
||||
)
|
||||
)
|
||||
->setParameter('authorized_centers', $centers);
|
||||
}
|
||||
$this->filterListAccompanyingPeriodHelper->addFilterAccompanyingPeriods($qb, $requiredModifiers, $acl, $data);
|
||||
|
||||
$qb
|
||||
// some grouping are necessary
|
||||
|
@@ -0,0 +1,138 @@
|
||||
<?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\Export\LinkedToPerson;
|
||||
|
||||
use Chill\ActivityBundle\Export\Declarations;
|
||||
use Chill\ActivityBundle\Repository\ActivityRepository;
|
||||
use Chill\ActivityBundle\Security\Authorization\ActivityStatsVoter;
|
||||
use Chill\MainBundle\Export\ExportInterface;
|
||||
use Chill\MainBundle\Export\FormatterInterface;
|
||||
use Chill\MainBundle\Export\GroupedExportInterface;
|
||||
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
||||
use Chill\PersonBundle\Export\Declarations as PersonDeclarations;
|
||||
use Doctrine\ORM\Query;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
final readonly class CountHouseholdOnActivity implements ExportInterface, GroupedExportInterface
|
||||
{
|
||||
private bool $filterStatsByCenters;
|
||||
|
||||
public function __construct(
|
||||
private ActivityRepository $activityRepository,
|
||||
ParameterBagInterface $parameterBag,
|
||||
) {
|
||||
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder) {}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getAllowedFormattersTypes()
|
||||
{
|
||||
return [FormatterInterface::TYPE_TABULAR];
|
||||
}
|
||||
|
||||
public function getDescription()
|
||||
{
|
||||
return 'export.export.count_household_on_activity_person.description';
|
||||
}
|
||||
|
||||
public function getGroup(): string
|
||||
{
|
||||
return 'Exports of activities linked to a person';
|
||||
}
|
||||
|
||||
public function getLabels($key, array $values, $data)
|
||||
{
|
||||
if ('export_count_activity' !== $key) {
|
||||
throw new \LogicException("the key {$key} is not used by this export");
|
||||
}
|
||||
|
||||
return static fn ($value) => '_header' === $value ? 'export.export.count_household_on_activity_person.header' : $value;
|
||||
}
|
||||
|
||||
public function getQueryKeys($data)
|
||||
{
|
||||
return ['export_count_activity'];
|
||||
}
|
||||
|
||||
public function getResult($query, $data)
|
||||
{
|
||||
return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return 'export.export.count_household_on_activity_person.title';
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return Declarations::ACTIVITY;
|
||||
}
|
||||
|
||||
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
|
||||
{
|
||||
$centers = array_map(static fn ($el) => $el['center'], $acl);
|
||||
|
||||
$qb = $this->activityRepository
|
||||
->createQueryBuilder('activity')
|
||||
->join('activity.person', 'person')
|
||||
->join(
|
||||
HouseholdMember::class,
|
||||
'householdmember',
|
||||
Query\Expr\Join::WITH,
|
||||
'person = householdmember.person AND householdmember.startDate <= activity.date AND (householdmember.endDate IS NULL OR householdmember.endDate > activity.date)'
|
||||
)
|
||||
->join('householdmember.household', 'household');
|
||||
|
||||
$qb->select('COUNT(DISTINCT household.id) as export_count_activity');
|
||||
|
||||
if ($this->filterStatsByCenters) {
|
||||
$qb
|
||||
->join('person.centerHistory', 'centerHistory')
|
||||
->where(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->lte('centerHistory.startDate', 'activity.date'),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isNull('centerHistory.endDate'),
|
||||
$qb->expr()->gt('centerHistory.endDate', 'activity.date')
|
||||
)
|
||||
)
|
||||
)
|
||||
->andWhere($qb->expr()->in('centerHistory.center', ':centers'))
|
||||
->setParameter('centers', $centers);
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function requiredRole(): string
|
||||
{
|
||||
return ActivityStatsVoter::STATS;
|
||||
}
|
||||
|
||||
public function supportsModifiers()
|
||||
{
|
||||
return [
|
||||
Declarations::ACTIVITY,
|
||||
Declarations::ACTIVITY_PERSON,
|
||||
PersonDeclarations::PERSON_TYPE,
|
||||
PersonDeclarations::HOUSEHOLD_TYPE,
|
||||
];
|
||||
}
|
||||
}
|
@@ -15,17 +15,22 @@ use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\ActivityBundle\Entity\ActivityType;
|
||||
use Chill\ActivityBundle\Repository\ActivityTypeRepositoryInterface;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Chill\PersonBundle\Export\Declarations;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class ActivityTypeFilter implements FilterInterface
|
||||
final readonly class ActivityTypeFilter implements FilterInterface
|
||||
{
|
||||
private const BASE_EXISTS = 'SELECT 1 FROM '.Activity::class.' act_type_filter_activity WHERE act_type_filter_activity.accompanyingPeriod = acp';
|
||||
|
||||
public function __construct(
|
||||
private readonly ActivityTypeRepositoryInterface $activityTypeRepository,
|
||||
private readonly TranslatableStringHelperInterface $translatableStringHelper
|
||||
private ActivityTypeRepositoryInterface $activityTypeRepository,
|
||||
private TranslatableStringHelperInterface $translatableStringHelper,
|
||||
private RollingDateConverterInterface $rollingDateConverter,
|
||||
) {}
|
||||
|
||||
public function addRole(): ?string
|
||||
@@ -35,13 +40,26 @@ class ActivityTypeFilter implements FilterInterface
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data)
|
||||
{
|
||||
$qb->andWhere(
|
||||
$qb->expr()->exists(
|
||||
'SELECT 1 FROM '.Activity::class.' act_type_filter_activity
|
||||
WHERE act_type_filter_activity.activityType IN (:act_type_filter_activity_types) AND act_type_filter_activity.accompanyingPeriod = acp'
|
||||
)
|
||||
);
|
||||
$qb->setParameter('act_type_filter_activity_types', $data['accepted_activitytypes']);
|
||||
$exists = self::BASE_EXISTS;
|
||||
|
||||
if (count($data['accepted_activitytypes']) > 0) {
|
||||
$exists .= ' AND act_type_filter_activity.activityType IN (:act_type_filter_activity_types)';
|
||||
$qb->setParameter('act_type_filter_activity_types', $data['accepted_activitytypes']);
|
||||
}
|
||||
|
||||
if (null !== $data['date_after']) {
|
||||
$exists .= ' AND act_type_filter_activity.date >= :act_type_filter_activity_date_after';
|
||||
$qb->setParameter('act_type_filter_activity_date_after', $this->rollingDateConverter->convert($data['date_after']));
|
||||
}
|
||||
|
||||
if (null !== $data['date_before']) {
|
||||
$exists .= ' AND act_type_filter_activity.date >= :act_type_filter_activity_date_before';
|
||||
$qb->setParameter('act_type_filter_activity_date_before', $this->rollingDateConverter->convert($data['date_before']));
|
||||
}
|
||||
|
||||
if (self::BASE_EXISTS !== $exists) {
|
||||
$qb->andWhere($qb->expr()->exists($exists));
|
||||
}
|
||||
}
|
||||
|
||||
public function applyOn()
|
||||
@@ -60,11 +78,27 @@ class ActivityTypeFilter implements FilterInterface
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
]);
|
||||
|
||||
$builder->add('date_after', PickRollingDateType::class, [
|
||||
'label' => 'export.filter.activity.acp_by_activity_type.activity after',
|
||||
'help' => 'export.filter.activity.acp_by_activity_type.activity after help',
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
$builder->add('date_before', PickRollingDateType::class, [
|
||||
'label' => 'export.filter.activity.acp_by_activity_type.activity before',
|
||||
'help' => 'export.filter.activity.acp_by_activity_type.activity before help',
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [];
|
||||
return [
|
||||
'accepted_activitytypes' => [],
|
||||
'date_after' => null,
|
||||
'date_before' => null,
|
||||
];
|
||||
}
|
||||
|
||||
public function describeAction($data, $format = 'string'): array
|
||||
@@ -75,8 +109,12 @@ class ActivityTypeFilter implements FilterInterface
|
||||
$types[] = $this->translatableStringHelper->localize($aty->getName());
|
||||
}
|
||||
|
||||
return ['export.filter.activity.acp_by_activity_type.acp_containing_at_least_one_%activitytypes%', [
|
||||
'%activitytypes%' => implode(', ', $types),
|
||||
return ['export.filter.activity.acp_by_activity_type.acp_containing_at_least_one_activitytypes', [
|
||||
'activitytypes' => implode(', ', $types),
|
||||
'has_date_after' => null !== $data['date_after'] ? 1 : 0,
|
||||
'date_after' => $this->rollingDateConverter->convert($data['date_after']),
|
||||
'has_date_before' => null !== $data['date_before'] ? 1 : 0,
|
||||
'date_before' => $this->rollingDateConverter->convert($data['date_before']),
|
||||
]];
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,60 @@
|
||||
<?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\Tests\Export\Export\LinkedToACP;
|
||||
|
||||
use Chill\ActivityBundle\Export\Declarations;
|
||||
use Chill\ActivityBundle\Export\Export\LinkedToACP\CountHouseholdOnActivity;
|
||||
use Chill\MainBundle\Test\Export\AbstractExportTest;
|
||||
use Chill\PersonBundle\Export\Declarations as PersonDeclarations;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class CountHouseholdOnActivityTest extends AbstractExportTest
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->entityManager = self::$container->get(EntityManagerInterface::class);
|
||||
}
|
||||
|
||||
public function getExport()
|
||||
{
|
||||
yield new CountHouseholdOnActivity($this->entityManager, $this->getParameters(true));
|
||||
yield new CountHouseholdOnActivity($this->entityManager, $this->getParameters(false));
|
||||
}
|
||||
|
||||
public function getFormData()
|
||||
{
|
||||
return [
|
||||
[],
|
||||
];
|
||||
}
|
||||
|
||||
public function getModifiersCombination()
|
||||
{
|
||||
return [
|
||||
[
|
||||
Declarations::ACTIVITY,
|
||||
Declarations::ACTIVITY_ACP,
|
||||
PersonDeclarations::ACP_TYPE,
|
||||
PersonDeclarations::PERSON_TYPE,
|
||||
PersonDeclarations::HOUSEHOLD_TYPE,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@@ -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\ActivityBundle\Tests\Export\Export\LinkedToPerson;
|
||||
|
||||
use Chill\ActivityBundle\Export\Declarations;
|
||||
use Chill\ActivityBundle\Export\Export\LinkedToPerson\CountHouseholdOnActivity;
|
||||
use Chill\ActivityBundle\Repository\ActivityRepository;
|
||||
use Chill\MainBundle\Test\Export\AbstractExportTest;
|
||||
use Chill\PersonBundle\Export\Declarations as PersonDeclarations;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class CountHouseholdOnActivityTest extends AbstractExportTest
|
||||
{
|
||||
private ActivityRepository $activityRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->activityRepository = self::$container->get(ActivityRepository::class);
|
||||
}
|
||||
|
||||
public function getExport()
|
||||
{
|
||||
yield new CountHouseholdOnActivity($this->activityRepository, $this->getParameters(true));
|
||||
yield new CountHouseholdOnActivity($this->activityRepository, $this->getParameters(false));
|
||||
}
|
||||
|
||||
public function getFormData()
|
||||
{
|
||||
return [
|
||||
[],
|
||||
];
|
||||
}
|
||||
|
||||
public function getModifiersCombination()
|
||||
{
|
||||
return [
|
||||
[
|
||||
Declarations::ACTIVITY,
|
||||
Declarations::ACTIVITY_PERSON,
|
||||
PersonDeclarations::PERSON_TYPE,
|
||||
PersonDeclarations::HOUSEHOLD_TYPE,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@@ -13,6 +13,7 @@ namespace Chill\ActivityBundle\Tests\Export\Filter\ACPFilters;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\ActivityBundle\Entity\ActivityType;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||
use Chill\MainBundle\Test\Export\AbstractFilterTest;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
@@ -55,8 +56,30 @@ final class ActivityTypeFilterTest extends AbstractFilterTest
|
||||
$data = [];
|
||||
|
||||
foreach ($array as $a) {
|
||||
$data[] = [
|
||||
'accepted_activitytypes' => [],
|
||||
'date_after' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START),
|
||||
'date_before' => new RollingDate(RollingDate::T_TODAY),
|
||||
];
|
||||
$data[] = [
|
||||
'accepted_activitytypes' => new ArrayCollection([$a]),
|
||||
'date_after' => null,
|
||||
'date_before' => null,
|
||||
];
|
||||
$data[] = [
|
||||
'accepted_activitytypes' => [$a],
|
||||
'date_after' => null,
|
||||
'date_before' => null,
|
||||
];
|
||||
$data[] = [
|
||||
'accepted_activitytypes' => [$a],
|
||||
'date_after' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START),
|
||||
'date_before' => new RollingDate(RollingDate::T_TODAY),
|
||||
];
|
||||
$data[] = [
|
||||
'accepted_activitytypes' => [],
|
||||
'date_after' => null,
|
||||
'date_before' => null,
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -20,6 +20,14 @@ services:
|
||||
tags:
|
||||
- { name: chill.export, alias: 'count_person_on_activity' }
|
||||
|
||||
Chill\ActivityBundle\Export\Export\LinkedToACP\CountHouseholdOnActivity:
|
||||
tags:
|
||||
- { name: chill.export, alias: 'count_household_on_activity_acp' }
|
||||
|
||||
Chill\ActivityBundle\Export\Export\LinkedToPerson\CountHouseholdOnActivity:
|
||||
tags:
|
||||
- { name: chill.export, alias: 'count_household_on_activity_person' }
|
||||
|
||||
chill.activity.export.count_activity_linked_to_acp:
|
||||
class: Chill\ActivityBundle\Export\Export\LinkedToACP\CountActivity
|
||||
tags:
|
||||
|
@@ -3,7 +3,12 @@ export:
|
||||
activity:
|
||||
course_having_activity_between_date:
|
||||
Only course having an activity between from and to: Seulement les parcours ayant reçu au moins un échange entre le {from, date, short} et le {to, date, short}
|
||||
person_between_dates:
|
||||
|
||||
acp_by_activity_type:
|
||||
'acp_containing_at_least_one_activitytypes': >-
|
||||
Parcours filtrés: uniquement ceux qui contiennent au moins un échange d'un des types suivants: {activitytypes}
|
||||
{has_date_after, select, 1 {, après le {date_after, date}} other {}}
|
||||
{has_date_before, select, 1 {, avant le {date_before, date}} other {}}
|
||||
describe_action_with_no_subject: >-
|
||||
Filtré par personne ayant eu un échange entre le {date_from, date} et le {date_to, date}
|
||||
describe_action_with_subject: >-
|
||||
|
@@ -337,6 +337,14 @@ export:
|
||||
title: Nombre d'usagers concernés par les échanges
|
||||
description: Compte le nombre d'usagers concernés par les échanges. Si un usager est présent dans plusieurs échanges, il n'est comptabilisé qu'une seule fois.
|
||||
header: Nombre d'usagers concernés par des échanges
|
||||
count_household_on_activity:
|
||||
title: Nombre de ménages concernés par les échanges
|
||||
description: Compte le nombre de ménages concernés par les échanges. Si un ménage est présent dans plusieurs échanges, il n'est comptabilisé qu'une seule fois. Les usagers sans ménages ne sont pas comptabilisés.
|
||||
header: Nombre de ménage concernés par des échanges
|
||||
count_household_on_activity_person:
|
||||
title: Nombre de ménages concernés par les échanges
|
||||
description: Compte le nombre de ménages concernés par les échanges. Si un ménage est présent dans plusieurs échanges, il n'est comptabilisé qu'une seule fois. Les usagers sans ménages ne sont pas comptabilisés. Lorsqu'un usager change de ménage, chaque ménage est comptabilisé une fois.
|
||||
header: Nombre de ménage concernés par des échanges
|
||||
list:
|
||||
activity:
|
||||
users name: Nom des utilisateurs
|
||||
@@ -371,7 +379,10 @@ export:
|
||||
Receiving an activity after: Ayant reçu un échange après le
|
||||
Receiving an activity before: Ayant reçu un échange avant le
|
||||
acp_by_activity_type:
|
||||
'acp_containing_at_least_one_%activitytypes%': 'Parcours filtrés: uniquement ceux qui contiennent au moins un échange d''un des types suivants: %activitytypes%'
|
||||
'activity after': Échanges après le
|
||||
activity after help: Si laissé vide, ne sera pas pris en compte
|
||||
activity before: Echanges avant le
|
||||
activity before help: Si laissé vide, ne sera pas pris en compte
|
||||
person_between_dates:
|
||||
Implied in an activity after this date: Impliqué dans un échange après cette date
|
||||
Implied in an activity before this date: Impliqué dans un échange avant cette date
|
||||
|
@@ -13,15 +13,11 @@ namespace Chill\AsideActivityBundle\Export\Filter;
|
||||
|
||||
use Chill\AsideActivityBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Form\Type\Export\FilterType;
|
||||
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ByDateFilter implements FilterInterface
|
||||
@@ -66,51 +62,14 @@ class ByDateFilter implements FilterInterface
|
||||
->add('date_to', PickRollingDateType::class, [
|
||||
'label' => 'export.filter.Aside activities before this date',
|
||||
]);
|
||||
|
||||
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
|
||||
/** @var \Symfony\Component\Form\FormInterface $filterForm */
|
||||
$filterForm = $event->getForm()->getParent();
|
||||
$enabled = $filterForm->get(FilterType::ENABLED_FIELD)->getData();
|
||||
|
||||
if (true === $enabled) {
|
||||
// if the filter is enabled, add some validation
|
||||
$form = $event->getForm();
|
||||
$date_from = $form->get('date_from')->getData();
|
||||
$date_to = $form->get('date_to')->getData();
|
||||
|
||||
// check that fields are not empty
|
||||
if (null === $date_from) {
|
||||
$form->get('date_from')->addError(new FormError(
|
||||
$this->translator->trans('This field '
|
||||
.'should not be empty')
|
||||
));
|
||||
}
|
||||
|
||||
if (null === $date_to) {
|
||||
$form->get('date_to')->addError(new FormError(
|
||||
$this->translator->trans('This field '
|
||||
.'should not be empty')
|
||||
));
|
||||
}
|
||||
|
||||
// check that date_from is before date_to
|
||||
if (
|
||||
(null !== $date_from && null !== $date_to)
|
||||
&& $date_from >= $date_to
|
||||
) {
|
||||
$form->get('date_to')->addError(new FormError(
|
||||
$this->translator->trans('export.filter.This date should be after '
|
||||
.'the date given in "Implied in an aside activity after '
|
||||
.'this date" field')
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return ['date_from' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), 'date_to' => new RollingDate(RollingDate::T_TODAY)];
|
||||
return [
|
||||
'date_from' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START),
|
||||
'date_to' => new RollingDate(RollingDate::T_TODAY),
|
||||
];
|
||||
}
|
||||
|
||||
public function describeAction($data, $format = 'string'): array
|
||||
|
@@ -0,0 +1,90 @@
|
||||
<?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\DocStoreBundle\AsyncUpload\Command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class ConfigureOpenstackObjectStorageCommand extends Command
|
||||
{
|
||||
private readonly string $basePath;
|
||||
|
||||
private readonly string $tempUrlKey;
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $client, ParameterBagInterface $parameterBag)
|
||||
{
|
||||
$config = $parameterBag->get('chill_doc_store')['openstack']['temp_url'];
|
||||
|
||||
$this->tempUrlKey = $config['temp_url_key'];
|
||||
$this->basePath = $config['temp_url_base_path'];
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this->setDescription('Configure openstack container to store documents')
|
||||
->setName('chill:doc-store:configure-openstack')
|
||||
->addOption('os_token', 'o', InputOption::VALUE_REQUIRED, 'Openstack token')
|
||||
->addOption('domain', 'd', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Domain name')
|
||||
;
|
||||
}
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
if (!$input->hasOption('os_token')) {
|
||||
$output->writeln('The option os_token is required');
|
||||
|
||||
throw new \RuntimeException('The option os_token is required');
|
||||
}
|
||||
|
||||
if (0 === count($input->getOption('domain'))) {
|
||||
$output->writeln('At least one occurence of option domain is required');
|
||||
|
||||
throw new \RuntimeException('At least one occurence of option domain is required');
|
||||
}
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$domains = trim(implode(' ', $input->getOption('domain')));
|
||||
|
||||
if ($output->isVerbose()) {
|
||||
$output->writeln(['Domains configured will be', $domains]);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->client->request('POST', $this->basePath, [
|
||||
'headers' => [
|
||||
'X-Auth-Token' => $input->getOption('os_token'),
|
||||
'X-Container-Meta-Access-Control-Allow-Origin' => $domains,
|
||||
'X-Container-Meta-Temp-URL-Key' => $this->tempUrlKey,
|
||||
],
|
||||
]);
|
||||
$response->getContent();
|
||||
} catch (HttpExceptionInterface $e) {
|
||||
$output->writeln('Error');
|
||||
$output->writeln($e->getMessage());
|
||||
|
||||
if ($output->isVerbose()) {
|
||||
$output->writeln($e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
@@ -0,0 +1,204 @@
|
||||
<?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\DocStoreBundle\AsyncUpload\Driver\OpenstackObjectStore;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\Event\TempUrlGenerateEvent;
|
||||
use Chill\DocStoreBundle\AsyncUpload\Exception\TempUrlGeneratorException;
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrlPost;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* Generate a temp url.
|
||||
*/
|
||||
final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterface
|
||||
{
|
||||
private const CHARACTERS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
private string $base_url;
|
||||
private string $key;
|
||||
private int $max_expire_delay;
|
||||
private int $max_submit_delay;
|
||||
private int $max_post_file_size;
|
||||
private int $max_file_count;
|
||||
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
private EventDispatcherInterface $event_dispatcher,
|
||||
private ClockInterface $clock,
|
||||
ParameterBagInterface $parameterBag,
|
||||
) {
|
||||
$config = $parameterBag->get('chill_doc_store')['openstack']['temp_url'];
|
||||
|
||||
$this->key = $config['temp_url_key'];
|
||||
$this->base_url = $config['temp_url_base_path'];
|
||||
$this->max_expire_delay = $config['max_expires_delay'];
|
||||
$this->max_submit_delay = $config['max_submit_delay'];
|
||||
$this->max_post_file_size = $config['max_post_file_size'];
|
||||
$this->max_file_count = $config['max_post_file_count'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TempUrlGeneratorException
|
||||
*/
|
||||
public function generatePost(
|
||||
int $expire_delay = null,
|
||||
int $submit_delay = null,
|
||||
int $max_file_count = 1,
|
||||
): SignedUrlPost {
|
||||
$delay = $expire_delay ?? $this->max_expire_delay;
|
||||
$submit_delay ??= $this->max_submit_delay;
|
||||
|
||||
if ($delay < 2) {
|
||||
throw new TempUrlGeneratorException("The delay of {$delay} is too ".'short (<2 sec) to properly use this token');
|
||||
}
|
||||
|
||||
if ($delay > $this->max_expire_delay) {
|
||||
throw new TempUrlGeneratorException('The given delay is greater than the max delay authorized.');
|
||||
}
|
||||
|
||||
if ($submit_delay < 15) {
|
||||
throw new TempUrlGeneratorException("The submit delay of {$delay} is too ".'short (<15 sec) to properly use this token');
|
||||
}
|
||||
|
||||
if ($submit_delay > $this->max_submit_delay) {
|
||||
throw new TempUrlGeneratorException('The given submit delay is greater than the max submit delay authorized.');
|
||||
}
|
||||
|
||||
if ($max_file_count > $this->max_file_count) {
|
||||
throw new TempUrlGeneratorException('The number of files is greater than the authorized number of files');
|
||||
}
|
||||
|
||||
$expires = $this->clock->now()->add(new \DateInterval('PT'.(string) $delay.'S'));
|
||||
|
||||
$object_name = $this->generateObjectName();
|
||||
|
||||
$g = new SignedUrlPost(
|
||||
$url = $this->generateUrl($object_name),
|
||||
$expires,
|
||||
$this->max_post_file_size,
|
||||
$max_file_count,
|
||||
$submit_delay,
|
||||
'',
|
||||
$object_name,
|
||||
$this->generateSignaturePost($url, $expires)
|
||||
);
|
||||
|
||||
$this->event_dispatcher->dispatch(
|
||||
new TempUrlGenerateEvent($g),
|
||||
);
|
||||
|
||||
$this->logger->info(
|
||||
'generate signature for url',
|
||||
(array) $g
|
||||
);
|
||||
|
||||
return $g;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an absolute public url for a GET request on the object.
|
||||
*/
|
||||
public function generate(string $method, string $object_name, int $expire_delay = null): SignedUrl
|
||||
{
|
||||
if ($expire_delay > $this->max_expire_delay) {
|
||||
throw new TempUrlGeneratorException(sprintf('The expire delay (%d) is greater than the max_expire_delay (%d)', $expire_delay, $this->max_expire_delay));
|
||||
}
|
||||
$url = $this->generateUrl($object_name);
|
||||
|
||||
$expires = $this->clock->now()
|
||||
->add(new \DateInterval(sprintf('PT%dS', $expire_delay ?? $this->max_expire_delay)));
|
||||
$args = [
|
||||
'temp_url_sig' => $this->generateSignature($method, $url, $expires),
|
||||
'temp_url_expires' => $expires->format('U'),
|
||||
];
|
||||
$url = $url.'?'.\http_build_query($args);
|
||||
|
||||
$signature = new SignedUrl(strtoupper($method), $url, $expires);
|
||||
|
||||
$this->event_dispatcher->dispatch(
|
||||
new TempUrlGenerateEvent($signature)
|
||||
);
|
||||
|
||||
return $signature;
|
||||
}
|
||||
|
||||
private function generateUrl($relative_path): string
|
||||
{
|
||||
return match (str_ends_with($this->base_url, '/')) {
|
||||
true => $this->base_url.$relative_path,
|
||||
false => $this->base_url.'/'.$relative_path
|
||||
};
|
||||
}
|
||||
|
||||
private function generateObjectName()
|
||||
{
|
||||
// inspiration from https://stackoverflow.com/a/4356295/1572236
|
||||
$charactersLength = strlen(self::CHARACTERS);
|
||||
$randomString = '';
|
||||
for ($i = 0; $i < 21; ++$i) {
|
||||
$randomString .= self::CHARACTERS[random_int(0, $charactersLength - 1)];
|
||||
}
|
||||
|
||||
return $randomString;
|
||||
}
|
||||
|
||||
private function generateSignaturePost($url, \DateTimeImmutable $expires)
|
||||
{
|
||||
$path = \parse_url((string) $url, PHP_URL_PATH);
|
||||
|
||||
$body = sprintf(
|
||||
"%s\n%s\n%s\n%s\n%s",
|
||||
$path,
|
||||
'', // redirect is empty
|
||||
(string) $this->max_post_file_size,
|
||||
(string) $this->max_file_count,
|
||||
$expires->format('U')
|
||||
)
|
||||
;
|
||||
|
||||
$this->logger->debug(
|
||||
'generate signature post',
|
||||
['url' => $body, 'method' => 'POST']
|
||||
);
|
||||
|
||||
return \hash_hmac('sha512', $body, $this->key, false);
|
||||
}
|
||||
|
||||
private function generateSignature($method, $url, \DateTimeImmutable $expires)
|
||||
{
|
||||
if ('POST' === $method) {
|
||||
return $this->generateSignaturePost($url, $expires);
|
||||
}
|
||||
|
||||
$path = \parse_url((string) $url, PHP_URL_PATH);
|
||||
|
||||
$body = sprintf(
|
||||
"%s\n%s\n%s",
|
||||
$method,
|
||||
$expires->format('U'),
|
||||
$path
|
||||
)
|
||||
;
|
||||
|
||||
$this->logger->debug(
|
||||
'generate signature GET',
|
||||
['url' => $body, 'method' => 'GET']
|
||||
);
|
||||
|
||||
return \hash_hmac('sha512', $body, $this->key, false);
|
||||
}
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
<?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\DocStoreBundle\AsyncUpload\Event;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
|
||||
final class TempUrlGenerateEvent extends \Symfony\Contracts\EventDispatcher\Event
|
||||
{
|
||||
final public const NAME_GENERATE = 'async_uploader.generate_url';
|
||||
|
||||
public function __construct(private readonly SignedUrl $data) {}
|
||||
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->data->method;
|
||||
}
|
||||
|
||||
public function getData(): SignedUrl
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
<?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\DocStoreBundle\AsyncUpload\Exception;
|
||||
|
||||
class BadCallToRemoteServer extends \LogicException
|
||||
{
|
||||
public function __construct(string $content, int $statusCode, int $code = 0, \Throwable $previous = null)
|
||||
{
|
||||
parent::__construct("Bad call to remote server: {$statusCode}, {$content}", $code, $previous);
|
||||
}
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
<?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\DocStoreBundle\AsyncUpload\Exception;
|
||||
|
||||
class TempUrlGeneratorException extends \RuntimeException {}
|
@@ -0,0 +1,20 @@
|
||||
<?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\DocStoreBundle\AsyncUpload\Exception;
|
||||
|
||||
class TempUrlRemoteServerException extends \RuntimeException
|
||||
{
|
||||
public function __construct(int $statusCode, int $code = 0, \Throwable $previous = null)
|
||||
{
|
||||
parent::__construct('Could not reach remote server: '.(string) $statusCode, $code, $previous);
|
||||
}
|
||||
}
|
38
src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php
Normal file
38
src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?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\DocStoreBundle\AsyncUpload;
|
||||
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
|
||||
readonly class SignedUrl
|
||||
{
|
||||
public function __construct(
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public string $method,
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public string $url,
|
||||
public \DateTimeImmutable $expires,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public function getExpires(): int
|
||||
{
|
||||
return $this->expires->getTimestamp();
|
||||
}
|
||||
}
|
54
src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrlPost.php
Normal file
54
src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrlPost.php
Normal 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\DocStoreBundle\AsyncUpload;
|
||||
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
|
||||
readonly class SignedUrlPost extends SignedUrl
|
||||
{
|
||||
public function __construct(
|
||||
string $url,
|
||||
\DateTimeImmutable $expires,
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public int $max_file_size,
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public int $max_file_count,
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public int $submit_delay,
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public string $redirect,
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public string $prefix,
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public string $signature,
|
||||
) {
|
||||
parent::__construct('POST', $url, $expires);
|
||||
}
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
<?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\DocStoreBundle\AsyncUpload;
|
||||
|
||||
interface TempUrlGeneratorInterface
|
||||
{
|
||||
public function generatePost(
|
||||
int $expire_delay = null,
|
||||
int $submit_delay = null,
|
||||
int $max_file_count = 1
|
||||
): SignedUrlPost;
|
||||
|
||||
public function generate(string $method, string $object_name, int $expire_delay = null): SignedUrl;
|
||||
}
|
@@ -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\DocStoreBundle\AsyncUpload\Templating;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
|
||||
/**
|
||||
* This class extends the AbstractExtension class and provides Twig filter functions for generating URLs for asynchronous
|
||||
* file uploads.
|
||||
*/
|
||||
class AsyncUploadExtension extends AbstractExtension
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TempUrlGeneratorInterface $tempUrlGenerator,
|
||||
private readonly UrlGeneratorInterface $routingUrlGenerator
|
||||
) {}
|
||||
|
||||
public function getFilters()
|
||||
{
|
||||
return [
|
||||
new TwigFilter('file_url', $this->computeSignedUrl(...)),
|
||||
new TwigFilter('generate_url', $this->computeGenerateUrl(...)),
|
||||
];
|
||||
}
|
||||
|
||||
public function computeSignedUrl(StoredObject|string $file, string $method = 'GET', int $expiresDelay = null): string
|
||||
{
|
||||
if ($file instanceof StoredObject) {
|
||||
$object_name = $file->getFilename();
|
||||
} else {
|
||||
$object_name = $file;
|
||||
}
|
||||
|
||||
return $this->tempUrlGenerator->generate($method, $object_name, $expiresDelay)->url;
|
||||
}
|
||||
|
||||
public function computeGenerateUrl(StoredObject|string $file, string $method = 'GET', int $expiresDelay = null): string
|
||||
{
|
||||
if ($file instanceof StoredObject) {
|
||||
$object_name = $file->getFilename();
|
||||
} else {
|
||||
$object_name = $file;
|
||||
}
|
||||
|
||||
$args = [
|
||||
'method' => $method,
|
||||
'object_name' => $object_name,
|
||||
];
|
||||
|
||||
if (null !== $expiresDelay) {
|
||||
$args['expires_delay'] = $expiresDelay;
|
||||
}
|
||||
|
||||
return $this->routingUrlGenerator->generate('async_upload.generate_url', $args);
|
||||
}
|
||||
}
|
@@ -0,0 +1,100 @@
|
||||
<?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\DocStoreBundle\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\Exception\TempUrlGeneratorException;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\AsyncUploadVoter;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
final readonly class AsyncUploadController
|
||||
{
|
||||
public function __construct(
|
||||
private TempUrlGeneratorInterface $tempUrlGenerator,
|
||||
private SerializerInterface $serializer,
|
||||
private Security $security,
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @Route("/asyncupload/temp_url/generate/{method}",
|
||||
* name="async_upload.generate_url")
|
||||
*/
|
||||
public function getSignedUrl(string $method, Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
switch (strtolower($method)) {
|
||||
case 'post':
|
||||
$p = $this->tempUrlGenerator
|
||||
->generatePost(
|
||||
$request->query->has('expires_delay') ? $request->query->getInt('expires_delay') : null,
|
||||
$request->query->has('submit_delay') ? $request->query->getInt('submit_delay') : null
|
||||
)
|
||||
;
|
||||
break;
|
||||
case 'get':
|
||||
case 'head':
|
||||
$object_name = $request->query->get('object_name', null);
|
||||
|
||||
if (null === $object_name) {
|
||||
return (new JsonResponse((object) [
|
||||
'message' => 'the object_name is null',
|
||||
]))
|
||||
->setStatusCode(JsonResponse::HTTP_BAD_REQUEST);
|
||||
}
|
||||
$p = $this->tempUrlGenerator->generate(
|
||||
$method,
|
||||
$object_name,
|
||||
$request->query->has('expires_delay') ? $request->query->getInt('expires_delay') : null
|
||||
);
|
||||
break;
|
||||
default:
|
||||
return (new JsonResponse((object) ['message' => 'the method '
|
||||
."{$method} is not valid"]))
|
||||
->setStatusCode(JsonResponse::HTTP_BAD_REQUEST);
|
||||
}
|
||||
} catch (TempUrlGeneratorException $e) {
|
||||
$this->logger->warning('The client requested a temp url'
|
||||
.' which sparkle an error.', [
|
||||
'message' => $e->getMessage(),
|
||||
'expire_delay' => $request->query->getInt('expire_delay', 0),
|
||||
'file_count' => $request->query->getInt('file_count', 1),
|
||||
'method' => $method,
|
||||
]);
|
||||
|
||||
$p = new \stdClass();
|
||||
$p->message = $e->getMessage();
|
||||
$p->status = JsonResponse::HTTP_BAD_REQUEST;
|
||||
|
||||
return new JsonResponse($p, JsonResponse::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted(AsyncUploadVoter::GENERATE_SIGNATURE, $p)) {
|
||||
throw new AccessDeniedHttpException('not allowed to generate this signature');
|
||||
}
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize($p, 'json', [AbstractNormalizer::GROUPS => ['read']]),
|
||||
Response::HTTP_OK,
|
||||
[],
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
@@ -32,9 +32,10 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
|
||||
$configuration = new Configuration();
|
||||
$config = $this->processConfiguration($configuration, $configs);
|
||||
|
||||
$container->setParameter('chill_doc_store', $config);
|
||||
|
||||
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
|
||||
$loader->load('services.yaml');
|
||||
$loader->load('services/media.yaml');
|
||||
$loader->load('services/controller.yaml');
|
||||
$loader->load('services/menu.yaml');
|
||||
$loader->load('services/fixtures.yaml');
|
||||
@@ -94,7 +95,6 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
|
||||
'routing' => [
|
||||
'resources' => [
|
||||
'@ChillDocStoreBundle/config/routes.yaml',
|
||||
'@ChampsLibresAsyncUploaderBundle/config/routes.yaml',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
|
||||
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
|
||||
use Symfony\Component\Config\Definition\ConfigurationInterface;
|
||||
|
||||
@@ -24,11 +25,68 @@ class Configuration implements ConfigurationInterface
|
||||
public function getConfigTreeBuilder()
|
||||
{
|
||||
$treeBuilder = new TreeBuilder('chill_doc_store');
|
||||
/** @var ArrayNodeDefinition $rootNode */
|
||||
$rootNode = $treeBuilder->getRootNode();
|
||||
|
||||
// Here you should define the parameters that are allowed to
|
||||
// configure your bundle. See the documentation linked above for
|
||||
// more information on that topic.
|
||||
/* @phpstan-ignore-next-line As there are inconsistencies in return types, but the code works... */
|
||||
$rootNode->children()
|
||||
// openstack node
|
||||
->arrayNode('openstack')
|
||||
->info('parameters to authenticate and generate temp url against the openstack object storage service')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
// openstack.temp_url
|
||||
->arrayNode('temp_url')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
// openstack.temp_url.temp_url_key
|
||||
->scalarNode('temp_url_key')
|
||||
->isRequired()->cannotBeEmpty()
|
||||
->info('the temp url key')
|
||||
->end()
|
||||
|
||||
->scalarNode('temp_url_base_path')
|
||||
->isRequired()->cannotBeEmpty()
|
||||
->info('the base path to add **before** the path to media. Must contains the container')
|
||||
->end()
|
||||
|
||||
->scalarNode('container')
|
||||
->info('the container name')
|
||||
->isRequired()->cannotBeEmpty()
|
||||
->end()
|
||||
|
||||
->integerNode('max_post_file_size')
|
||||
->defaultValue(15_000_000)
|
||||
->info('Maximum size of the posted file, in bytes')
|
||||
->end()
|
||||
|
||||
->integerNode('max_post_file_count')
|
||||
->defaultValue(1)
|
||||
->info('Maximum number of files which may be posted at once using a POST operation using async upload')
|
||||
->end()
|
||||
|
||||
->integerNode('max_expires_delay')
|
||||
->defaultValue(180)
|
||||
->info('the maximum of seconds a cryptographic signature '
|
||||
.'will be valid for submitting a file. This should be '
|
||||
.'short, to avoid uploading multiple files')
|
||||
->end()
|
||||
|
||||
->integerNode('max_submit_delay')
|
||||
->defaultValue(3600)
|
||||
->info('the maximum of seconds between the upload of a file and '
|
||||
.'a the submission of the form. This delay will also prevent '
|
||||
.'the check of persistence of uploaded file. Should be long '
|
||||
.'enough for keeping user-friendly forms')
|
||||
->end()
|
||||
|
||||
->end() // end of children 's temp_url
|
||||
->end() // end array temp_url
|
||||
|
||||
->end() // end of children's openstack
|
||||
->end() // end of openstack
|
||||
->end() // end of root's children
|
||||
->end();
|
||||
|
||||
return $treeBuilder;
|
||||
}
|
||||
|
@@ -11,8 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\Entity;
|
||||
|
||||
use ChampsLibres\AsyncUploaderBundle\Model\AsyncFileInterface;
|
||||
use ChampsLibres\AsyncUploaderBundle\Validator\Constraints\AsyncFileExists;
|
||||
use Chill\DocStoreBundle\Validator\Constraints\AsyncFileExists;
|
||||
use ChampsLibres\WopiLib\Contract\Entity\Document;
|
||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
@@ -33,7 +32,7 @@ use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
* message="The file is not stored properly"
|
||||
* )
|
||||
*/
|
||||
class StoredObject implements AsyncFileInterface, Document, TrackCreationInterface
|
||||
class StoredObject implements Document, TrackCreationInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
final public const STATUS_READY = 'ready';
|
||||
|
@@ -14,13 +14,10 @@ namespace Chill\DocStoreBundle\Form;
|
||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||
use Chill\DocStoreBundle\Entity\Document;
|
||||
use Chill\DocStoreBundle\Entity\DocumentCategory;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
@@ -29,33 +26,9 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class AccompanyingCourseDocumentType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @var AuthorizationHelper
|
||||
*/
|
||||
protected $authorizationHelper;
|
||||
|
||||
/**
|
||||
* @var ObjectManager
|
||||
*/
|
||||
protected $om;
|
||||
|
||||
/**
|
||||
* @var TranslatableStringHelper
|
||||
*/
|
||||
protected $translatableStringHelper;
|
||||
|
||||
/**
|
||||
* the user running this form.
|
||||
*
|
||||
* @var User
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
public function __construct(
|
||||
TranslatableStringHelper $translatableStringHelper
|
||||
) {
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
}
|
||||
private readonly TranslatableStringHelperInterface $translatableStringHelper
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
|
@@ -20,23 +20,15 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class DocumentCategoryType extends AbstractType
|
||||
{
|
||||
private $chillBundlesFlipped;
|
||||
|
||||
public function __construct($kernelBundles)
|
||||
{
|
||||
// TODO faire un service dans CHillMain
|
||||
foreach ($kernelBundles as $key => $value) {
|
||||
if (str_starts_with((string) $key, 'Chill')) {
|
||||
$this->chillBundlesFlipped[$value] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$bundles = [
|
||||
'chill-doc-store' => 'chill-doc-store',
|
||||
];
|
||||
|
||||
$builder
|
||||
->add('bundleId', ChoiceType::class, [
|
||||
'choices' => $this->chillBundlesFlipped,
|
||||
'choices' => $bundles,
|
||||
'disabled' => false,
|
||||
])
|
||||
->add('idInsideBundle', null, [
|
||||
@@ -44,7 +36,7 @@ class DocumentCategoryType extends AbstractType
|
||||
])
|
||||
->add('documentClass', null, [
|
||||
'disabled' => false,
|
||||
]) // cahcerh par default PersonDocument
|
||||
])
|
||||
->add('name', TranslatableStringFormType::class);
|
||||
}
|
||||
|
||||
|
@@ -11,7 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\Form;
|
||||
|
||||
use ChampsLibres\AsyncUploaderBundle\Form\Type\AsyncUploaderType;
|
||||
use Chill\DocStoreBundle\Form\Type\AsyncUploaderType;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
@@ -26,15 +26,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
*/
|
||||
class StoredObjectType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
protected $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
public function __construct(private readonly EntityManagerInterface $em) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
|
@@ -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\DocStoreBundle\Form\Type;
|
||||
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class AsyncUploaderType extends AbstractType
|
||||
{
|
||||
private readonly int $expires_delay;
|
||||
private readonly int $max_submit_delay;
|
||||
private readonly int $max_post_file_size;
|
||||
|
||||
public function __construct(
|
||||
private readonly UrlGeneratorInterface $url_generator,
|
||||
ParameterBagInterface $parameters,
|
||||
) {
|
||||
$config = $parameters->get('chill_doc_store')['openstack']['temp_url'];
|
||||
|
||||
$this->expires_delay = $config['max_expires_delay'];
|
||||
$this->max_submit_delay = $config['max_submit_delay'];
|
||||
$this->max_post_file_size = $config['max_post_file_size'];
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'expires_delay' => $this->expires_delay,
|
||||
'max_post_size' => $this->max_post_file_size,
|
||||
'submit_delay' => $this->max_submit_delay,
|
||||
'max_files' => 1,
|
||||
'error_bubbling' => false,
|
||||
]);
|
||||
|
||||
$resolver->setAllowedTypes('expires_delay', ['int']);
|
||||
$resolver->setAllowedTypes('max_post_size', ['int']);
|
||||
$resolver->setAllowedTypes('max_files', ['int']);
|
||||
$resolver->setAllowedTypes('submit_delay', ['int']);
|
||||
}
|
||||
|
||||
public function buildView(
|
||||
FormView $view,
|
||||
FormInterface $form,
|
||||
array $options
|
||||
) {
|
||||
$view->vars['attr']['data-async-file-upload'] = true;
|
||||
$view->vars['attr']['data-generate-temp-url-post'] = $this
|
||||
->url_generator->generate('async_upload.generate_url', [
|
||||
'expires_delay' => $options['expires_delay'],
|
||||
'method' => 'post',
|
||||
'submit_delay' => $options['submit_delay'],
|
||||
]);
|
||||
$view->vars['attr']['data-temp-url-get'] = $this->url_generator
|
||||
->generate('async_upload.generate_url', ['method' => 'GET']);
|
||||
$view->vars['attr']['data-max-files'] = $options['max_files'];
|
||||
$view->vars['attr']['data-max-post-size'] = $options['max_post_size'];
|
||||
}
|
||||
|
||||
public function getParent()
|
||||
{
|
||||
return HiddenType::class;
|
||||
}
|
||||
}
|
@@ -1,47 +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\DocStoreBundle\Object;
|
||||
|
||||
use ChampsLibres\AsyncUploaderBundle\Form\AsyncFileTransformer\AsyncFileTransformerInterface;
|
||||
use ChampsLibres\AsyncUploaderBundle\Model\AsyncFileInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class ObjectToAsyncFileTransformer implements AsyncFileTransformerInterface
|
||||
{
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
protected $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
public function toAsyncFile($data)
|
||||
{
|
||||
if ($data instanceof StoredObject) {
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
public function toData(AsyncFileInterface $asyncFile)
|
||||
{
|
||||
$object = $this->em
|
||||
->getRepository(StoredObject::class)
|
||||
->findByFilename($asyncFile->getObjectName());
|
||||
|
||||
return $object ?? (new StoredObject())
|
||||
->setFilename($asyncFile->getObjectName());
|
||||
}
|
||||
}
|
@@ -1,40 +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\DocStoreBundle\Object;
|
||||
|
||||
use ChampsLibres\AsyncUploaderBundle\Persistence\PersistenceCheckerInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class PersistenceChecker implements PersistenceCheckerInterface
|
||||
{
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
protected $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
public function isPersisted($object_name): bool
|
||||
{
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
$qb->select('COUNT(m)')
|
||||
->from(StoredObject::class, 'm')
|
||||
->where($qb->expr()->eq('m.filename', ':object_name'))
|
||||
->setParameter('object_name', $object_name);
|
||||
|
||||
return 1 === $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
<?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\DocStoreBundle\Security\Authorization;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
final class AsyncUploadVoter extends Voter
|
||||
{
|
||||
public const GENERATE_SIGNATURE = 'CHILL_DOC_GENERATE_ASYNC_SIGNATURE';
|
||||
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
protected function supports($attribute, $subject): bool
|
||||
{
|
||||
return self::GENERATE_SIGNATURE === $attribute && $subject instanceof SignedUrl;
|
||||
}
|
||||
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||
{
|
||||
/** @var SignedUrl $subject */
|
||||
if (!in_array($subject->method, ['POST', 'GET', 'HEAD'], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN');
|
||||
}
|
||||
}
|
@@ -12,7 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\DocStoreBundle\Service;
|
||||
|
||||
use Base64Url\Base64Url;
|
||||
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -27,7 +27,10 @@ final class StoredObjectManager implements StoredObjectManagerInterface
|
||||
|
||||
private array $inMemory = [];
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $client, private readonly TempUrlGeneratorInterface $tempUrlGenerator) {}
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $client,
|
||||
private readonly TempUrlGeneratorInterface $tempUrlGenerator
|
||||
) {}
|
||||
|
||||
public function getLastModified(StoredObject $document): \DateTimeInterface
|
||||
{
|
||||
|
@@ -0,0 +1,65 @@
|
||||
<?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 AsyncUpload\Command;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\Command\ConfigureOpenstackObjectStorageCommand;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class ConfigureOpenstackObjectStorageCommandTest extends TestCase
|
||||
{
|
||||
public function testRun(): void
|
||||
{
|
||||
$client = new MockHttpClient(function ($method, $url, $options): MockResponse {
|
||||
self::assertSame('POST', $method);
|
||||
self::assertSame($url, 'https://object.store.example/v1/AUTH/container');
|
||||
|
||||
$headers = $options['headers'];
|
||||
|
||||
self::assertContains('X-Auth-Token: abc', $headers);
|
||||
self::assertContains('X-Container-Meta-Temp-URL-Key: 12345679801234567890', $headers);
|
||||
self::assertContains('X-Container-Meta-Access-Control-Allow-Origin: https://chill.domain.social https://chill2.domain.social', $headers);
|
||||
|
||||
return new MockResponse('', ['http_code' => 204]);
|
||||
});
|
||||
|
||||
$parameters = new ParameterBag([
|
||||
'chill_doc_store' => [
|
||||
'openstack' => [
|
||||
'temp_url' => [
|
||||
'temp_url_key' => '12345679801234567890',
|
||||
'temp_url_base_path' => 'https://object.store.example/v1/AUTH/container',
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$command = new ConfigureOpenstackObjectStorageCommand($client, $parameters);
|
||||
|
||||
$tester = new CommandTester($command);
|
||||
|
||||
$status = $tester->execute([
|
||||
'--os_token' => 'abc',
|
||||
'--domain' => ['https://chill.domain.social', 'https://chill2.domain.social'],
|
||||
]);
|
||||
|
||||
self::assertSame(0, $status);
|
||||
}
|
||||
}
|
@@ -0,0 +1,176 @@
|
||||
<?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 AsyncUpload\Driver\OpenstackObjectStore;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\Driver\OpenstackObjectStore\TempUrlOpenstackGenerator;
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrlPost;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class TempUrlOpenstackGeneratorTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider dataProviderGenerate
|
||||
*/
|
||||
public function testGenerate(string $baseUrl, \DateTimeImmutable $now, string $key, string $method, string $objectName, int $expireDelay, SignedUrl $expected): void
|
||||
{
|
||||
$logger = new NullLogger();
|
||||
$eventDispatcher = new EventDispatcher();
|
||||
$clock = new MockClock($now);
|
||||
$parameters = new ParameterBag(
|
||||
[
|
||||
'chill_doc_store' => [
|
||||
'openstack' => [
|
||||
'temp_url' => [
|
||||
'temp_url_key' => $key,
|
||||
'temp_url_base_path' => $baseUrl,
|
||||
'max_post_file_size' => 150,
|
||||
'max_post_file_count' => 1,
|
||||
'max_expires_delay' => 1800,
|
||||
'max_submit_delay' => 1800,
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$generator = new TempUrlOpenstackGenerator(
|
||||
$logger,
|
||||
$eventDispatcher,
|
||||
$clock,
|
||||
$parameters,
|
||||
);
|
||||
|
||||
$signedUrl = $generator->generate($method, $objectName, $expireDelay);
|
||||
|
||||
self::assertEquals($expected, $signedUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderGeneratePost
|
||||
*/
|
||||
public function testGeneratePost(string $baseUrl, \DateTimeImmutable $now, string $key, string $method, string $objectName, int $expireDelay, SignedUrl $expected): void
|
||||
{
|
||||
$logger = new NullLogger();
|
||||
$eventDispatcher = new EventDispatcher();
|
||||
$clock = new MockClock($now);
|
||||
$parameters = new ParameterBag(
|
||||
[
|
||||
'chill_doc_store' => [
|
||||
'openstack' => [
|
||||
'temp_url' => [
|
||||
'temp_url_key' => $key,
|
||||
'temp_url_base_path' => $baseUrl,
|
||||
'max_post_file_size' => 150,
|
||||
'max_post_file_count' => 1,
|
||||
'max_expires_delay' => 1800,
|
||||
'max_submit_delay' => 1800,
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$generator = new TempUrlOpenstackGenerator(
|
||||
$logger,
|
||||
$eventDispatcher,
|
||||
$clock,
|
||||
$parameters,
|
||||
);
|
||||
|
||||
$signedUrl = $generator->generatePost();
|
||||
|
||||
self::assertEquals('POST', $signedUrl->method);
|
||||
self::assertEquals((int) $clock->now()->format('U') + 1800, $signedUrl->expires->getTimestamp());
|
||||
self::assertEquals(150, $signedUrl->max_file_size);
|
||||
self::assertEquals(1, $signedUrl->max_file_count);
|
||||
self::assertEquals(1800, $signedUrl->submit_delay);
|
||||
self::assertEquals('', $signedUrl->redirect);
|
||||
self::assertGreaterThanOrEqual(20, strlen($signedUrl->prefix));
|
||||
}
|
||||
|
||||
public function dataProviderGenerate(): iterable
|
||||
{
|
||||
$now = \DateTimeImmutable::createFromFormat('U', '1702041743');
|
||||
$expireDelay = 1800;
|
||||
$baseUrls = [
|
||||
'https://objectstore.example/v1/my_account/container/',
|
||||
'https://objectstore.example/v1/my_account/container',
|
||||
];
|
||||
$objectName = 'object';
|
||||
$method = 'GET';
|
||||
$key = 'MYKEY';
|
||||
|
||||
$signedUrl = new SignedUrl(
|
||||
'GET',
|
||||
'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
|
||||
\DateTimeImmutable::createFromFormat('U', '1702043543')
|
||||
);
|
||||
|
||||
foreach ($baseUrls as $baseUrl) {
|
||||
yield [
|
||||
$baseUrl,
|
||||
$now,
|
||||
$key,
|
||||
$method,
|
||||
$objectName,
|
||||
$expireDelay,
|
||||
$signedUrl,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function dataProviderGeneratePost(): iterable
|
||||
{
|
||||
$now = \DateTimeImmutable::createFromFormat('U', '1702041743');
|
||||
$expireDelay = 1800;
|
||||
$baseUrls = [
|
||||
'https://objectstore.example/v1/my_account/container/',
|
||||
'https://objectstore.example/v1/my_account/container',
|
||||
];
|
||||
$objectName = 'object';
|
||||
$method = 'GET';
|
||||
$key = 'MYKEY';
|
||||
|
||||
$signedUrl = new SignedUrlPost(
|
||||
'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
|
||||
\DateTimeImmutable::createFromFormat('U', '1702043543'),
|
||||
150,
|
||||
1,
|
||||
1800,
|
||||
'',
|
||||
'abc',
|
||||
'abc'
|
||||
);
|
||||
|
||||
foreach ($baseUrls as $baseUrl) {
|
||||
yield [
|
||||
$baseUrl,
|
||||
$now,
|
||||
$key,
|
||||
$method,
|
||||
$objectName,
|
||||
$expireDelay,
|
||||
$signedUrl,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 AsyncUpload\Templating;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\Templating\AsyncUploadExtension;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class AsyncUploadExtensionTest extends KernelTestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private AsyncUploadExtension $asyncUploadExtension;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$generator = $this->prophesize(TempUrlGeneratorInterface::class);
|
||||
$generator->generate(Argument::in(['GET', 'POST']), Argument::type('string'), Argument::any())
|
||||
->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours')));
|
||||
|
||||
$urlGenerator = $this->prophesize(UrlGeneratorInterface::class);
|
||||
$urlGenerator->generate('async_upload.generate_url', Argument::type('array'))
|
||||
->willReturn('url');
|
||||
|
||||
$this->asyncUploadExtension = new AsyncUploadExtension(
|
||||
$generator->reveal(),
|
||||
$urlGenerator->reveal()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderStoredObject
|
||||
*/
|
||||
public function testComputeSignedUrl(StoredObject|string $storedObject): void
|
||||
{
|
||||
$actual = $this->asyncUploadExtension->computeSignedUrl($storedObject);
|
||||
|
||||
self::assertStringContainsString('https://object.store.example/container', $actual);
|
||||
self::assertStringContainsString(is_string($storedObject) ? $storedObject : $storedObject->getFilename(), $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderStoredObject
|
||||
*/
|
||||
public function testComputeGenerateUrl(StoredObject|string $storedObject): void
|
||||
{
|
||||
$actual = $this->asyncUploadExtension->computeGenerateUrl($storedObject);
|
||||
|
||||
self::assertEquals('url', $actual);
|
||||
}
|
||||
|
||||
public function dataProviderStoredObject(): iterable
|
||||
{
|
||||
yield [(new StoredObject())->setFilename('blabla')];
|
||||
|
||||
yield ['blabla'];
|
||||
}
|
||||
}
|
@@ -0,0 +1,110 @@
|
||||
<?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\DocStoreBundle\Tests\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrlPost;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Controller\AsyncUploadController;
|
||||
use Chill\DocStoreBundle\Security\Authorization\AsyncUploadVoter;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class AsyncUploadControllerTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testGenerateWhenUserIsNotGranted(): void
|
||||
{
|
||||
$this->expectException(AccessDeniedHttpException::class);
|
||||
$controller = $this->buildAsyncUploadController(false);
|
||||
|
||||
$controller->getSignedUrl('POST', new Request());
|
||||
}
|
||||
|
||||
public function testGeneratePost(): void
|
||||
{
|
||||
$controller = $this->buildAsyncUploadController(true);
|
||||
|
||||
$actual = $controller->getSignedUrl('POST', new Request());
|
||||
$decodedActual = json_decode($actual->getContent(), true, JSON_THROW_ON_ERROR, JSON_THROW_ON_ERROR);
|
||||
|
||||
self::assertArrayHasKey('method', $decodedActual);
|
||||
self::assertEquals('POST', $decodedActual['method']);
|
||||
}
|
||||
|
||||
public function testGenerateGet(): void
|
||||
{
|
||||
$controller = $this->buildAsyncUploadController(true);
|
||||
|
||||
$actual = $controller->getSignedUrl('GET', new Request(['object_name' => 'abc']));
|
||||
$decodedActual = json_decode($actual->getContent(), true, JSON_THROW_ON_ERROR, JSON_THROW_ON_ERROR);
|
||||
|
||||
self::assertArrayHasKey('method', $decodedActual);
|
||||
self::assertEquals('GET', $decodedActual['method']);
|
||||
}
|
||||
|
||||
private function buildAsyncUploadController(
|
||||
bool $isGranted,
|
||||
): AsyncUploadController {
|
||||
$tempUrlGenerator = new class () implements TempUrlGeneratorInterface {
|
||||
public function generatePost(int $expire_delay = null, int $submit_delay = null, int $max_file_count = 1): SignedUrlPost
|
||||
{
|
||||
return new SignedUrlPost(
|
||||
'https://object.store.example',
|
||||
new \DateTimeImmutable('1 hour'),
|
||||
150,
|
||||
1,
|
||||
1800,
|
||||
'',
|
||||
'abc',
|
||||
'abc'
|
||||
);
|
||||
}
|
||||
|
||||
public function generate(string $method, string $object_name, int $expire_delay = null): SignedUrl
|
||||
{
|
||||
return new SignedUrl(
|
||||
$method,
|
||||
'https://object.store.example',
|
||||
new \DateTimeImmutable('1 hour')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
$serializer = $this->prophesize(SerializerInterface::class);
|
||||
$serializer->serialize(Argument::type(SignedUrl::class), 'json', Argument::type('array'))
|
||||
->will(fn (array $args): string => json_encode(['method' => $args[0]->method], JSON_THROW_ON_ERROR, 3));
|
||||
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->isGranted(AsyncUploadVoter::GENERATE_SIGNATURE, Argument::type(SignedUrl::class))
|
||||
->willReturn($isGranted);
|
||||
|
||||
return new AsyncUploadController(
|
||||
$tempUrlGenerator,
|
||||
$serializer->reveal(),
|
||||
$security->reveal(),
|
||||
new NullLogger()
|
||||
);
|
||||
}
|
||||
}
|
@@ -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\DocStoreBundle\Tests\Serializer\Normalizer;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class SignedUrlNormalizerTest extends KernelTestCase
|
||||
{
|
||||
public static NormalizerInterface $normalizer;
|
||||
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
self::bootKernel();
|
||||
self::$normalizer = self::$container->get(NormalizerInterface::class);
|
||||
}
|
||||
|
||||
public function testNormalizerSignedUrl(): void
|
||||
{
|
||||
$signedUrl = new SignedUrl(
|
||||
'GET',
|
||||
'https://object.store.example/container/object',
|
||||
\DateTimeImmutable::createFromFormat('U', '1700000')
|
||||
);
|
||||
|
||||
$actual = self::$normalizer->normalize($signedUrl, 'json', [AbstractNormalizer::GROUPS => ['read']]);
|
||||
|
||||
self::assertEqualsCanonicalizing(
|
||||
[
|
||||
'method' => 'GET',
|
||||
'expires' => 1_700_000,
|
||||
'url' => 'https://object.store.example/container/object',
|
||||
],
|
||||
$actual
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
<?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\DocStoreBundle\Tests\Serializer\Normalizer;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrlPost;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class SignedUrlPostNormalizerTest extends KernelTestCase
|
||||
{
|
||||
public static NormalizerInterface $normalizer;
|
||||
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
self::bootKernel();
|
||||
self::$normalizer = self::$container->get(NormalizerInterface::class);
|
||||
}
|
||||
|
||||
public function testNormalizerSignedUrl(): void
|
||||
{
|
||||
$signedUrl = new SignedUrlPost(
|
||||
'https://object.store.example/container/object',
|
||||
\DateTimeImmutable::createFromFormat('U', '1700000'),
|
||||
15000,
|
||||
1,
|
||||
180,
|
||||
'',
|
||||
'abc',
|
||||
'SiGnaTure'
|
||||
);
|
||||
|
||||
$actual = self::$normalizer->normalize($signedUrl, 'json', [AbstractNormalizer::GROUPS => ['read']]);
|
||||
|
||||
self::assertEqualsCanonicalizing(
|
||||
[
|
||||
'max_file_size' => 15000,
|
||||
'max_file_count' => 1,
|
||||
'submit_delay' => 180,
|
||||
'redirect' => '',
|
||||
'prefix' => 'abc',
|
||||
'signature' => 'SiGnaTure',
|
||||
'method' => 'POST',
|
||||
'expires' => 1_700_000,
|
||||
'url' => 'https://object.store.example/container/object',
|
||||
],
|
||||
$actual
|
||||
);
|
||||
}
|
||||
}
|
@@ -11,7 +11,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests;
|
||||
|
||||
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManager;
|
||||
@@ -163,8 +164,11 @@ final class StoredObjectManagerTest extends TestCase
|
||||
|
||||
private function getTempUrlGenerator(StoredObject $storedObject): TempUrlGeneratorInterface
|
||||
{
|
||||
$response = new \stdClass();
|
||||
$response->url = $storedObject->getFilename();
|
||||
$response = new SignedUrl(
|
||||
'PUT',
|
||||
'https://example.com/'.$storedObject->getFilename(),
|
||||
new \DateTimeImmutable('1 hours')
|
||||
);
|
||||
|
||||
$tempUrlGenerator = $this->createMock(TempUrlGeneratorInterface::class);
|
||||
|
||||
|
@@ -0,0 +1,67 @@
|
||||
<?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\DocStoreBundle\Tests\Validator\Constraints;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Validator\Constraints\AsyncFileExists;
|
||||
use Chill\DocStoreBundle\Validator\Constraints\AsyncFileExistsValidator;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class AsyncFileExistsValidatorTest extends ConstraintValidatorTestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
protected function createValidator()
|
||||
{
|
||||
$client = new MockHttpClient(function ($method, $url, $options): MockResponse {
|
||||
if (str_contains((string) $url, '404')) {
|
||||
return new MockResponse('', ['http_code' => 404]);
|
||||
}
|
||||
|
||||
return new MockResponse('', ['http_code' => 200]);
|
||||
});
|
||||
|
||||
$generator = $this->prophesize(TempUrlGeneratorInterface::class);
|
||||
$generator->generate(Argument::in(['GET', 'HEAD']), Argument::type('string'), Argument::any())
|
||||
->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours')));
|
||||
|
||||
return new AsyncFileExistsValidator($generator->reveal(), $client);
|
||||
}
|
||||
|
||||
public function testWhenFileExistsIsValid(): void
|
||||
{
|
||||
$this->validator->validate((new StoredObject())->setFilename('present'), new AsyncFileExists());
|
||||
|
||||
$this->assertNoViolation();
|
||||
}
|
||||
|
||||
public function testWhenFileIsNotPresent(): void
|
||||
{
|
||||
$this->validator->validate(
|
||||
(new StoredObject())->setFilename('is_404'),
|
||||
new AsyncFileExists(['message' => 'my_message'])
|
||||
);
|
||||
|
||||
$this->buildViolation('my_message')->setParameter('{{ filename }}', 'is_404')->assertRaised();
|
||||
}
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
<?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\DocStoreBundle\Validator\Constraints;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
*/
|
||||
final class AsyncFileExists extends Constraint
|
||||
{
|
||||
public string $message = "The file '{{ filename }}' is not stored properly.";
|
||||
|
||||
public function validatedBy()
|
||||
{
|
||||
return AsyncFileExistsValidator::class;
|
||||
}
|
||||
|
||||
public function getTargets()
|
||||
{
|
||||
return [Constraint::CLASS_CONSTRAINT, Constraint::PROPERTY_CONSTRAINT];
|
||||
}
|
||||
}
|
@@ -0,0 +1,76 @@
|
||||
<?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\DocStoreBundle\Validator\Constraints;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\Exception\BadCallToRemoteServer;
|
||||
use Chill\DocStoreBundle\AsyncUpload\Exception\TempUrlRemoteServerException;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedValueException;
|
||||
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
final class AsyncFileExistsValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TempUrlGeneratorInterface $tempUrlGenerator,
|
||||
private readonly HttpClientInterface $client
|
||||
) {}
|
||||
|
||||
public function validate($value, Constraint $constraint): void
|
||||
{
|
||||
if ($value instanceof StoredObject) {
|
||||
$this->validateObject($value->getFilename(), $constraint);
|
||||
} elseif (is_string($value)) {
|
||||
$this->validateObject($value, $constraint);
|
||||
} else {
|
||||
throw new UnexpectedValueException($value, StoredObject::class.' or string');
|
||||
}
|
||||
}
|
||||
|
||||
protected function validateObject(string $file, Constraint $constraint): void
|
||||
{
|
||||
if (!$constraint instanceof AsyncFileExists) {
|
||||
throw new UnexpectedTypeException($constraint, AsyncFileExists::class);
|
||||
}
|
||||
|
||||
$urlHead = $this->tempUrlGenerator->generate(
|
||||
'HEAD',
|
||||
$file,
|
||||
30
|
||||
);
|
||||
|
||||
try {
|
||||
$response = $this->client->request('HEAD', $urlHead->url);
|
||||
|
||||
if (404 === $status = $response->getStatusCode()) {
|
||||
$this->context->buildViolation($constraint->message)
|
||||
->setParameter('{{ filename }}', $file)
|
||||
->addViolation();
|
||||
} elseif (500 <= $status) {
|
||||
throw new TempUrlRemoteServerException($response->getStatusCode());
|
||||
} elseif (400 <= $status) {
|
||||
throw new BadCallToRemoteServer($response->getContent(false), $response->getStatusCode());
|
||||
}
|
||||
} catch (HttpExceptionInterface $exception) {
|
||||
if (404 !== $exception->getResponse()->getStatusCode()) {
|
||||
throw $exception;
|
||||
}
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
throw new TempUrlRemoteServerException(0, previous: $e);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,73 +1,58 @@
|
||||
parameters:
|
||||
# cl_chill_person.example.class: Chill\PersonBundle\Example
|
||||
|
||||
services:
|
||||
_defaults:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\DocStoreBundle\Repository\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: "../Repository/"
|
||||
|
||||
Chill\DocStoreBundle\Form\DocumentCategoryType:
|
||||
class: Chill\DocStoreBundle\Form\DocumentCategoryType
|
||||
arguments: [ "%kernel.bundles%" ]
|
||||
tags:
|
||||
- { name: form.type }
|
||||
- { name: doctrine.repository_service }
|
||||
|
||||
Chill\DocStoreBundle\Form\PersonDocumentType:
|
||||
class: Chill\DocStoreBundle\Form\PersonDocumentType
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
# arguments:
|
||||
# - "@chill.main.helper.translatable_string"
|
||||
tags:
|
||||
- { name: form.type, alias: chill_docstorebundle_form_document }
|
||||
|
||||
Chill\DocStoreBundle\Security\Authorization\:
|
||||
resource: "./../Security/Authorization"
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
tags:
|
||||
- { name: chill.role }
|
||||
|
||||
Chill\DocStoreBundle\Workflow\:
|
||||
resource: './../Workflow/'
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
|
||||
Chill\DocStoreBundle\Serializer\Normalizer\:
|
||||
autowire: true
|
||||
resource: '../Serializer/Normalizer/'
|
||||
tags:
|
||||
- { name: 'serializer.normalizer', priority: 16 }
|
||||
- { name: serializer.normalizer, priority: 16 }
|
||||
|
||||
Chill\DocStoreBundle\Service\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../Service/'
|
||||
|
||||
Chill\DocStoreBundle\GenericDoc\Manager:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
arguments:
|
||||
$providersForAccompanyingPeriod: !tagged_iterator chill_doc_store.generic_doc_accompanying_period_provider
|
||||
$providersForPerson: !tagged_iterator chill_doc_store.generic_doc_person_provider
|
||||
|
||||
Chill\DocStoreBundle\GenericDoc\Twig\GenericDocExtension:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
Chill\DocStoreBundle\GenericDoc\Twig\GenericDocExtension: ~
|
||||
|
||||
Chill\DocStoreBundle\GenericDoc\Twig\GenericDocExtensionRuntime:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
arguments:
|
||||
$renderers: !tagged_iterator chill_doc_store.generic_doc_renderer
|
||||
|
||||
Chill\DocStoreBundle\GenericDoc\Providers\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../GenericDoc/Providers/'
|
||||
|
||||
Chill\DocStoreBundle\GenericDoc\Renderer\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../GenericDoc/Renderer/'
|
||||
|
||||
Chill\DocStoreBundle\Validator\:
|
||||
resource: '../Validator'
|
||||
|
||||
Chill\DocStoreBundle\AsyncUpload\Driver\:
|
||||
resource: '../AsyncUpload/Driver/'
|
||||
|
||||
Chill\DocStoreBundle\AsyncUpload\Templating\:
|
||||
resource: '../AsyncUpload/Templating/'
|
||||
|
||||
Chill\DocStoreBundle\AsyncUpload\Command\:
|
||||
resource: '../AsyncUpload/Command/'
|
||||
|
||||
Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface:
|
||||
alias: Chill\DocStoreBundle\AsyncUpload\Driver\OpenstackObjectStore\TempUrlOpenstackGenerator
|
||||
|
@@ -1,13 +1,15 @@
|
||||
services:
|
||||
Chill\DocStoreBundle\Form\StoredObjectType:
|
||||
arguments:
|
||||
$em: '@Doctrine\ORM\EntityManagerInterface'
|
||||
tags:
|
||||
- { name: form.type }
|
||||
_defaults:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\DocStoreBundle\Form\AccompanyingCourseDocumentType:
|
||||
class: Chill\DocStoreBundle\Form\AccompanyingCourseDocumentType
|
||||
arguments:
|
||||
- "@chill.main.helper.translatable_string"
|
||||
tags:
|
||||
- { name: form.type, alias: chill_docstorebundle_form_document }
|
||||
Chill\DocStoreBundle\Form\:
|
||||
resource: '../../Form'
|
||||
|
||||
Chill\DocStoreBundle\Form\PersonDocumentType:
|
||||
tags:
|
||||
- { name: form.type, alias: chill_docstorebundle_form_document }
|
||||
|
||||
Chill\DocStoreBundle\Form\AccompanyingCourseDocumentType:
|
||||
tags:
|
||||
- { name: form.type, alias: chill_docstorebundle_form_document }
|
||||
|
@@ -1,9 +0,0 @@
|
||||
services:
|
||||
chill_doc_store.persistence_checker:
|
||||
class: Chill\DocStoreBundle\Object\PersistenceChecker
|
||||
arguments:
|
||||
$em: '@Doctrine\ORM\EntityManagerInterface'
|
||||
|
||||
Chill\DocStoreBundle\Object\ObjectToAsyncFileTransformer:
|
||||
arguments:
|
||||
$em: '@Doctrine\ORM\EntityManagerInterface'
|
@@ -58,7 +58,7 @@ class AddressReferenceFromBano
|
||||
foreach ($stmt as $record) {
|
||||
$this->baseImporter->importAddress(
|
||||
$record['refId'],
|
||||
substr($record['refId'], 0, 5), // extract insee from reference
|
||||
substr((string) $record['refId'], 0, 5), // extract insee from reference
|
||||
$record['postcode'],
|
||||
$record['street'],
|
||||
$record['streetNumber'],
|
||||
|
@@ -23,7 +23,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
*/
|
||||
class PostalCodeFRFromOpenData
|
||||
{
|
||||
private const CSV = 'https://datanova.legroupe.laposte.fr/explore/dataset/laposte_hexasmal/download/?format=csv&timezone=Europe/Berlin&lang=fr&use_labels_for_header=true&csv_separator=%3B';
|
||||
private const CSV = 'https://datanova.laposte.fr/data-fair/api/v1/datasets/laposte-hexasmal/data-files/019HexaSmal.csv';
|
||||
|
||||
public function __construct(private readonly PostalCodeBaseImporter $baseImporter, private readonly HttpClientInterface $client, private readonly LoggerInterface $logger) {}
|
||||
|
||||
|
@@ -41,7 +41,7 @@ class Comment implements TrackCreationInterface, TrackUpdateInterface
|
||||
private ?AccompanyingPeriod $accompanyingPeriod = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text")
|
||||
* @ORM\Column(type="text", nullable=false, options={"default":""})
|
||||
*
|
||||
* @Groups({"read", "write", "docgen:read"})
|
||||
*
|
||||
|
@@ -19,28 +19,22 @@ use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
|
||||
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
|
||||
use Chill\PersonBundle\Export\Declarations;
|
||||
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelperInterface;
|
||||
use Chill\PersonBundle\Export\Helper\ListAccompanyingPeriodHelper;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
final readonly class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface
|
||||
{
|
||||
private bool $filterStatsByCenters;
|
||||
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private RollingDateConverterInterface $rollingDateConverter,
|
||||
private ListAccompanyingPeriodHelper $listAccompanyingPeriodHelper,
|
||||
ParameterBagInterface $parameterBag,
|
||||
) {
|
||||
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
|
||||
}
|
||||
private FilterListAccompanyingPeriodHelperInterface $filterListAccompanyingPeriodHelper,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
@@ -101,8 +95,6 @@ final readonly class ListAccompanyingPeriod implements ListInterface, GroupedExp
|
||||
|
||||
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
|
||||
{
|
||||
$centers = array_map(static fn ($el) => $el['center'], $acl);
|
||||
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
|
||||
$qb
|
||||
@@ -110,18 +102,7 @@ final readonly class ListAccompanyingPeriod implements ListInterface, GroupedExp
|
||||
->andWhere('acp.step != :list_acp_step')
|
||||
->setParameter('list_acp_step', AccompanyingPeriod::STEP_DRAFT);
|
||||
|
||||
if ($this->filterStatsByCenters) {
|
||||
$qb
|
||||
->andWhere(
|
||||
$qb->expr()->exists(
|
||||
'SELECT 1 FROM '.AccompanyingPeriodParticipation::class.' acl_count_part
|
||||
JOIN '.PersonCenterHistory::class.' acl_count_person_history WITH IDENTITY(acl_count_person_history.person) = IDENTITY(acl_count_part.person)
|
||||
WHERE acl_count_part.accompanyingPeriod = acp.id AND acl_count_person_history.center IN (:authorized_centers)
|
||||
'
|
||||
)
|
||||
)
|
||||
->setParameter('authorized_centers', $centers);
|
||||
}
|
||||
$this->filterListAccompanyingPeriodHelper->addFilterAccompanyingPeriods($qb, $requiredModifiers, $acl, $data);
|
||||
|
||||
$this->listAccompanyingPeriodHelper->addSelectClauses($qb, $this->rollingDateConverter->convert($data['calc_date']));
|
||||
|
||||
|
@@ -28,11 +28,11 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkGoal;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkReferrerHistory;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
|
||||
use Chill\PersonBundle\Entity\SocialWork\Evaluation;
|
||||
use Chill\PersonBundle\Entity\SocialWork\Goal;
|
||||
use Chill\PersonBundle\Entity\SocialWork\Result;
|
||||
use Chill\PersonBundle\Export\Declarations;
|
||||
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelperInterface;
|
||||
use Chill\PersonBundle\Export\Helper\LabelPersonHelper;
|
||||
use Chill\PersonBundle\Repository\SocialWork\SocialActionRepository;
|
||||
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
|
||||
@@ -44,10 +44,9 @@ use Chill\ThirdPartyBundle\Export\Helper\LabelThirdPartyHelper;
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeriod implements ListInterface, GroupedExportInterface
|
||||
final readonly class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeriod implements ListInterface, GroupedExportInterface
|
||||
{
|
||||
private const FIELDS = [
|
||||
'id',
|
||||
@@ -79,8 +78,6 @@ class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeriod implements L
|
||||
'updatedBy',
|
||||
];
|
||||
|
||||
private readonly bool $filterStatsByCenters;
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly DateTimeHelper $dateTimeHelper,
|
||||
@@ -94,10 +91,8 @@ class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeriod implements L
|
||||
private readonly RollingDateConverterInterface $rollingDateConverter,
|
||||
private readonly AggregateStringHelper $aggregateStringHelper,
|
||||
private readonly SocialActionRepository $socialActionRepository,
|
||||
ParameterBagInterface $parameterBag,
|
||||
) {
|
||||
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
|
||||
}
|
||||
private readonly FilterListAccompanyingPeriodHelperInterface $filterListAccompanyingPeriodHelper,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
@@ -223,17 +218,7 @@ class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeriod implements L
|
||||
->andWhere('acppart.startDate <= :calc_date AND (acppart.endDate > :calc_date OR acppart.endDate IS NULL)')
|
||||
->setParameter('calc_date', $this->rollingDateConverter->convert($calcDate));
|
||||
|
||||
if ($this->filterStatsByCenters) {
|
||||
$qb
|
||||
->andWhere(
|
||||
$qb->expr()->exists(
|
||||
'SELECT 1 FROM '.PersonCenterHistory::class.' acl_count_person_history WHERE acl_count_person_history.person = person
|
||||
AND acl_count_person_history.center IN (:authorized_centers)
|
||||
'
|
||||
)
|
||||
)
|
||||
->setParameter('authorized_centers', $centers);
|
||||
}
|
||||
$this->filterListAccompanyingPeriodHelper->addFilterAccompanyingPeriods($qb, $requiredModifiers, $acl, $data);
|
||||
|
||||
AccompanyingCourseExportHelper::addClosingMotiveExclusionClause($qb);
|
||||
|
||||
|
@@ -28,11 +28,11 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkGoal;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkReferrerHistory;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
|
||||
use Chill\PersonBundle\Entity\SocialWork\Evaluation;
|
||||
use Chill\PersonBundle\Entity\SocialWork\Goal;
|
||||
use Chill\PersonBundle\Entity\SocialWork\Result;
|
||||
use Chill\PersonBundle\Export\Declarations;
|
||||
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelperInterface;
|
||||
use Chill\PersonBundle\Export\Helper\LabelPersonHelper;
|
||||
use Chill\PersonBundle\Repository\SocialWork\SocialActionRepository;
|
||||
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
|
||||
@@ -44,10 +44,9 @@ use Chill\ThirdPartyBundle\Export\Helper\LabelThirdPartyHelper;
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class ListAccompanyingPeriodWorkAssociatePersonOnWork implements ListInterface, GroupedExportInterface
|
||||
final readonly class ListAccompanyingPeriodWorkAssociatePersonOnWork implements ListInterface, GroupedExportInterface
|
||||
{
|
||||
private const FIELDS = [
|
||||
'id',
|
||||
@@ -79,8 +78,6 @@ class ListAccompanyingPeriodWorkAssociatePersonOnWork implements ListInterface,
|
||||
'updatedBy',
|
||||
];
|
||||
|
||||
private readonly bool $filterStatsByCenters;
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly DateTimeHelper $dateTimeHelper,
|
||||
@@ -94,10 +91,8 @@ class ListAccompanyingPeriodWorkAssociatePersonOnWork implements ListInterface,
|
||||
private readonly RollingDateConverterInterface $rollingDateConverter,
|
||||
private readonly AggregateStringHelper $aggregateStringHelper,
|
||||
private readonly SocialActionRepository $socialActionRepository,
|
||||
ParameterBagInterface $parameterBag,
|
||||
) {
|
||||
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
|
||||
}
|
||||
private FilterListAccompanyingPeriodHelperInterface $filterListAccompanyingPeriodHelper,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
@@ -218,17 +213,7 @@ class ListAccompanyingPeriodWorkAssociatePersonOnWork implements ListInterface,
|
||||
->join('acpw.persons', 'person')
|
||||
;
|
||||
|
||||
if ($this->filterStatsByCenters) {
|
||||
$qb
|
||||
->andWhere(
|
||||
$qb->expr()->exists(
|
||||
'SELECT 1 FROM '.PersonCenterHistory::class.' acl_count_person_history WHERE acl_count_person_history.person = person
|
||||
AND acl_count_person_history.center IN (:authorized_centers)
|
||||
'
|
||||
)
|
||||
)
|
||||
->setParameter('authorized_centers', $centers);
|
||||
}
|
||||
$this->filterListAccompanyingPeriodHelper->addFilterAccompanyingPeriods($qb, $requiredModifiers, $acl, $data);
|
||||
|
||||
AccompanyingCourseExportHelper::addClosingMotiveExclusionClause($qb);
|
||||
|
||||
|
@@ -26,8 +26,8 @@ use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
|
||||
use Chill\PersonBundle\Export\Declarations;
|
||||
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelperInterface;
|
||||
use Chill\PersonBundle\Export\Helper\LabelPersonHelper;
|
||||
use Chill\PersonBundle\Repository\SocialWork\SocialActionRepository;
|
||||
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
|
||||
@@ -37,10 +37,9 @@ use Chill\PersonBundle\Templating\Entity\SocialIssueRender;
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class ListEvaluation implements ListInterface, GroupedExportInterface
|
||||
final readonly class ListEvaluation implements ListInterface, GroupedExportInterface
|
||||
{
|
||||
private const FIELDS = [
|
||||
'id',
|
||||
@@ -69,8 +68,6 @@ class ListEvaluation implements ListInterface, GroupedExportInterface
|
||||
'updatedBy',
|
||||
];
|
||||
|
||||
private readonly bool $filterStatsByCenters;
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly SocialIssueRender $socialIssueRender,
|
||||
@@ -83,10 +80,8 @@ class ListEvaluation implements ListInterface, GroupedExportInterface
|
||||
private readonly TranslatableStringExportLabelHelper $translatableStringExportLabelHelper,
|
||||
private readonly AggregateStringHelper $aggregateStringHelper,
|
||||
private readonly RollingDateConverterInterface $rollingDateConverter,
|
||||
ParameterBagInterface $parameterBag,
|
||||
) {
|
||||
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
|
||||
}
|
||||
private FilterListAccompanyingPeriodHelperInterface $filterListAccompanyingPeriodHelper,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
@@ -211,17 +206,7 @@ class ListEvaluation implements ListInterface, GroupedExportInterface
|
||||
->andWhere('acppart.startDate <= :calc_date AND (acppart.endDate > :calc_date OR acppart.endDate IS NULL)')
|
||||
->setParameter('calc_date', $this->rollingDateConverter->convert($calcDate));
|
||||
|
||||
if ($this->filterStatsByCenters) {
|
||||
$qb
|
||||
->andWhere(
|
||||
$qb->expr()->exists(
|
||||
'SELECT 1 FROM '.PersonCenterHistory::class.' acl_count_person_history WHERE acl_count_person_history.person = person
|
||||
AND acl_count_person_history.center IN (:authorized_centers)
|
||||
'
|
||||
)
|
||||
)
|
||||
->setParameter('authorized_centers', $centers);
|
||||
}
|
||||
$this->filterListAccompanyingPeriodHelper->addFilterAccompanyingPeriods($qb, $requiredModifiers, $acl, $data);
|
||||
|
||||
AccompanyingCourseExportHelper::addClosingMotiveExclusionClause($qb);
|
||||
|
||||
|
@@ -20,15 +20,14 @@ use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
|
||||
use Chill\PersonBundle\Export\Declarations;
|
||||
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelperInterface;
|
||||
use Chill\PersonBundle\Export\Helper\ListAccompanyingPeriodHelper;
|
||||
use Chill\PersonBundle\Export\Helper\ListPersonHelper;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
/**
|
||||
@@ -36,17 +35,13 @@ use Symfony\Component\Form\FormBuilderInterface;
|
||||
*/
|
||||
final readonly class ListPersonWithAccompanyingPeriodDetails implements ListInterface, GroupedExportInterface
|
||||
{
|
||||
private bool $filterStatsByCenters;
|
||||
|
||||
public function __construct(
|
||||
private ListPersonHelper $listPersonHelper,
|
||||
private ListAccompanyingPeriodHelper $listAccompanyingPeriodHelper,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private RollingDateConverterInterface $rollingDateConverter,
|
||||
ParameterBagInterface $parameterBag,
|
||||
) {
|
||||
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
|
||||
}
|
||||
private FilterListAccompanyingPeriodHelperInterface $filterListAccompanyingPeriodHelper,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
@@ -122,14 +117,7 @@ final readonly class ListPersonWithAccompanyingPeriodDetails implements ListInte
|
||||
->join('acppart.accompanyingPeriod', 'acp')
|
||||
->andWhere($qb->expr()->neq('acp.step', "'".AccompanyingPeriod::STEP_DRAFT."'"));
|
||||
|
||||
if ($this->filterStatsByCenters) {
|
||||
$qb
|
||||
->andWhere(
|
||||
$qb->expr()->exists(
|
||||
'SELECT 1 FROM '.PersonCenterHistory::class.' pch WHERE pch.person = person.id AND pch.center IN (:authorized_centers)'
|
||||
)
|
||||
)->setParameter('authorized_centers', $centers);
|
||||
}
|
||||
$this->filterListAccompanyingPeriodHelper->addFilterAccompanyingPeriods($qb, $requiredModifiers, $acl, $data);
|
||||
|
||||
$this->listPersonHelper->addSelect($qb, ListPersonHelper::FIELDS, $this->rollingDateConverter->convert($data['address_date']));
|
||||
$this->listAccompanyingPeriodHelper->addSelectClauses($qb, $this->rollingDateConverter->convert($data['address_date']));
|
||||
|
@@ -15,14 +15,14 @@ use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Chill\PersonBundle\Entity\SocialWork\Evaluation;
|
||||
use Chill\PersonBundle\Export\Declarations;
|
||||
use Doctrine\ORM\Query\Expr\Andx;
|
||||
use Chill\PersonBundle\Repository\SocialWork\EvaluationRepositoryInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
final readonly class EvaluationTypeFilter implements FilterInterface
|
||||
{
|
||||
public function __construct(private TranslatableStringHelper $translatableStringHelper) {}
|
||||
public function __construct(private TranslatableStringHelper $translatableStringHelper, private EvaluationRepositoryInterface $evaluationRepository) {}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
@@ -31,16 +31,9 @@ final readonly class EvaluationTypeFilter implements FilterInterface
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data)
|
||||
{
|
||||
$where = $qb->getDQLPart('where');
|
||||
$clause = $qb->expr()->in('eval.evaluation', ':evaluationtype');
|
||||
|
||||
if ($where instanceof Andx) {
|
||||
$where->add($clause);
|
||||
} else {
|
||||
$where = $qb->expr()->andX($clause);
|
||||
}
|
||||
|
||||
$qb->add('where', $where);
|
||||
$qb->andWhere(
|
||||
$qb->expr()->in('workeval.evaluation', ':evaluationtype')
|
||||
);
|
||||
$qb->setParameter('evaluationtype', $data['accepted_evaluationtype']);
|
||||
}
|
||||
|
||||
@@ -51,11 +44,17 @@ final readonly class EvaluationTypeFilter implements FilterInterface
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
$evaluations = $this->evaluationRepository->findAllActive();
|
||||
|
||||
usort($evaluations, fn (Evaluation $a, Evaluation $b) => $this->translatableStringHelper->localize($a->getTitle()) <=> $this->translatableStringHelper->localize($b->getTitle()));
|
||||
|
||||
$builder->add('accepted_evaluationtype', EntityType::class, [
|
||||
'class' => Evaluation::class,
|
||||
'choices' => $evaluations,
|
||||
'choice_label' => fn (Evaluation $ev): string => $this->translatableStringHelper->localize($ev->getTitle()),
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'expanded' => false,
|
||||
'attr' => ['class' => 'select2'],
|
||||
]);
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,105 @@
|
||||
<?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\Helper;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\CenterRepositoryInterface;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
|
||||
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
|
||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
/**
|
||||
* Filter accompanying period list and related, removing confidential ones
|
||||
* based on ACL rules.
|
||||
*/
|
||||
final readonly class FilterListAccompanyingPeriodHelper implements FilterListAccompanyingPeriodHelperInterface
|
||||
{
|
||||
private bool $filterStatsByCenters;
|
||||
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private CenterRepositoryInterface $centerRepository,
|
||||
private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser,
|
||||
ParameterBagInterface $parameterBag,
|
||||
) {
|
||||
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
|
||||
}
|
||||
|
||||
public function addFilterAccompanyingPeriods(QueryBuilder &$qb, array $requiredModifiers, array $acl, array $data = []): void
|
||||
{
|
||||
$centers = match ($this->filterStatsByCenters) {
|
||||
true => array_map(static fn ($el) => $el['center'], $acl),
|
||||
false => $this->centerRepository->findAll(),
|
||||
};
|
||||
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
throw new \RuntimeException('only a regular user can run this export');
|
||||
}
|
||||
|
||||
// add filtering on confidential accompanying period. The confidential is applyed on the current status of
|
||||
// the accompanying period (we do not use the 'calc_date' here
|
||||
$aclConditionsOrX = $qb->expr()->orX(
|
||||
// either the current user is the refferer for the course
|
||||
'acp.user = :list_acp_current_user',
|
||||
);
|
||||
$qb->setParameter('list_acp_current_user', $user);
|
||||
|
||||
$i = 0;
|
||||
foreach ($centers as $center) {
|
||||
$scopes = $this->authorizationHelperForCurrentUser->getReachableScopes(AccompanyingPeriodVoter::SEE_DETAILS, $center);
|
||||
$scopesConfidential =
|
||||
$this->authorizationHelperForCurrentUser->getReachableScopes(AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, $center);
|
||||
$orScopes = $qb->expr()->orX();
|
||||
|
||||
foreach ($scopes as $scope) {
|
||||
$scopeCondition = match (in_array($scope, $scopesConfidential, true)) {
|
||||
true => ":scope_{$i} MEMBER OF acp.scopes",
|
||||
false => $qb->expr()->andX(
|
||||
'acp.confidential = FALSE',
|
||||
":scope_{$i} MEMBER OF acp.scopes",
|
||||
),
|
||||
};
|
||||
|
||||
$orScopes->add($scopeCondition);
|
||||
$qb->setParameter("scope_{$i}", $scope);
|
||||
++$i;
|
||||
}
|
||||
|
||||
if ($this->filterStatsByCenters) {
|
||||
$andX = $qb->expr()->andX(
|
||||
$qb->expr()->exists(
|
||||
'SELECT 1 FROM '.AccompanyingPeriodParticipation::class." acl_count_part_{$i}
|
||||
JOIN ".PersonCenterHistory::class." acl_count_person_history_{$i} WITH IDENTITY(acl_count_person_history_{$i}.person) = IDENTITY(acl_count_part_{$i}.person)
|
||||
WHERE acl_count_part_{$i}.accompanyingPeriod = acp.id AND acl_count_person_history_{$i}.center IN (:authorized_center_{$i})
|
||||
AND acl_count_person_history_{$i}.startDate <= CURRENT_DATE() AND (acl_count_person_history_{$i}.endDate IS NULL or acl_count_person_history_{$i}.endDate > CURRENT_DATE())
|
||||
"
|
||||
),
|
||||
$orScopes,
|
||||
);
|
||||
$qb->setParameter('authorized_center_'.$i, $center);
|
||||
$aclConditionsOrX->add($andX);
|
||||
} else {
|
||||
$aclConditionsOrX->add($orScopes);
|
||||
}
|
||||
|
||||
++$i;
|
||||
}
|
||||
|
||||
$qb->andWhere($aclConditionsOrX);
|
||||
}
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
<?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\Helper;
|
||||
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
/**
|
||||
* Filter accompanying period list and related, removing confidential ones
|
||||
* based on ACL rules.
|
||||
*/
|
||||
interface FilterListAccompanyingPeriodHelperInterface
|
||||
{
|
||||
public function addFilterAccompanyingPeriods(QueryBuilder &$qb, array $requiredModifiers, array $acl, array $data = []): void;
|
||||
}
|
@@ -26,6 +26,7 @@ class AccompanyingCourseCommentType extends AbstractType
|
||||
{
|
||||
$builder->add('content', ChillTextareaType::class, [
|
||||
'required' => false,
|
||||
'empty_data' => '',
|
||||
]);
|
||||
}
|
||||
|
||||
|
@@ -11,14 +11,23 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\PersonBundle\Tests\Export\Export;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\CenterRepositoryInterface;
|
||||
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Chill\MainBundle\Test\Export\AbstractExportTest;
|
||||
use Chill\PersonBundle\Export\Declarations;
|
||||
use Chill\PersonBundle\Export\Export\ListAccompanyingPeriod;
|
||||
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelper;
|
||||
use Chill\PersonBundle\Export\Helper\ListAccompanyingPeriodHelper;
|
||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -27,6 +36,8 @@ use Doctrine\ORM\EntityManagerInterface;
|
||||
*/
|
||||
class ListAccompanyingPeriodTest extends AbstractExportTest
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private readonly ListAccompanyingPeriod $listAccompanyingPeriod;
|
||||
|
||||
private readonly CenterRepositoryInterface $centerRepository;
|
||||
@@ -39,12 +50,54 @@ class ListAccompanyingPeriodTest extends AbstractExportTest
|
||||
|
||||
public function getExport()
|
||||
{
|
||||
/** @var EntityManagerInterface::class $em */
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
$rollingDateConverter = self::$container->get(RollingDateConverterInterface::class);
|
||||
$listAccompanyingPeriodHelper = self::$container->get(ListAccompanyingPeriodHelper::class);
|
||||
$centerRepository = self::$container->get(CenterRepositoryInterface::class);
|
||||
$scopeRepository = self::$container->get(ScopeRepositoryInterface::class);
|
||||
|
||||
yield new ListAccompanyingPeriod($em, $rollingDateConverter, $listAccompanyingPeriodHelper, $this->getParameters(true));
|
||||
yield new ListAccompanyingPeriod($em, $rollingDateConverter, $listAccompanyingPeriodHelper, $this->getParameters(false));
|
||||
// mock security
|
||||
$user = $em->createQuery('SELECT u FROM '.User::class.' u')
|
||||
->setMaxResults(1)->getSingleResult();
|
||||
if (null === $user) {
|
||||
throw new \RuntimeException('no user found');
|
||||
}
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->getUser()->willReturn($user);
|
||||
|
||||
// mock authorization helper
|
||||
$scopes = $scopeRepository->findAll();
|
||||
$scopesConfidentials = [] !== $scopes ? [$scopes[0]] : [];
|
||||
$authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class);
|
||||
$authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE_DETAILS, Argument::type(Center::class))
|
||||
->willReturn($scopes);
|
||||
$authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, Argument::type(Center::class))
|
||||
->willReturn($scopesConfidentials);
|
||||
|
||||
yield new ListAccompanyingPeriod(
|
||||
$em,
|
||||
$rollingDateConverter,
|
||||
$listAccompanyingPeriodHelper,
|
||||
new FilterListAccompanyingPeriodHelper(
|
||||
$security->reveal(),
|
||||
$centerRepository,
|
||||
$authorizationHelper->reveal(),
|
||||
$this->getParameters(true)
|
||||
)
|
||||
);
|
||||
|
||||
yield new ListAccompanyingPeriod(
|
||||
$em,
|
||||
$rollingDateConverter,
|
||||
$listAccompanyingPeriodHelper,
|
||||
new FilterListAccompanyingPeriodHelper(
|
||||
$security->reveal(),
|
||||
$centerRepository,
|
||||
$authorizationHelper->reveal(),
|
||||
$this->getParameters(false)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function getFormData()
|
||||
|
@@ -20,6 +20,7 @@ use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Chill\MainBundle\Test\Export\AbstractExportTest;
|
||||
use Chill\PersonBundle\Export\Declarations;
|
||||
use Chill\PersonBundle\Export\Export\ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeriod;
|
||||
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelperInterface;
|
||||
use Chill\PersonBundle\Export\Helper\LabelPersonHelper;
|
||||
use Chill\PersonBundle\Repository\SocialWork\SocialActionRepository;
|
||||
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
|
||||
@@ -27,6 +28,7 @@ use Chill\PersonBundle\Templating\Entity\SocialActionRender;
|
||||
use Chill\PersonBundle\Templating\Entity\SocialIssueRender;
|
||||
use Chill\ThirdPartyBundle\Export\Helper\LabelThirdPartyHelper;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -35,6 +37,8 @@ use Doctrine\ORM\EntityManagerInterface;
|
||||
*/
|
||||
class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeriodTest extends AbstractExportTest
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -55,6 +59,7 @@ class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeriodTest extends
|
||||
$rollingDateConverter = self::$container->get(RollingDateConverterInterface::class);
|
||||
$aggregateStringHelper = self::$container->get(AggregateStringHelper::class);
|
||||
$socialActionRepository = self::$container->get(SocialActionRepository::class);
|
||||
$filterListAccompanyingPeriodHelper = $this->prophesize(FilterListAccompanyingPeriodHelperInterface::class);
|
||||
|
||||
yield new ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeriod(
|
||||
$entityManager,
|
||||
@@ -69,23 +74,7 @@ class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeriodTest extends
|
||||
$rollingDateConverter,
|
||||
$aggregateStringHelper,
|
||||
$socialActionRepository,
|
||||
$this->getParameters(true),
|
||||
);
|
||||
|
||||
yield new ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeriod(
|
||||
$entityManager,
|
||||
$dateTimeHelper,
|
||||
$userHelper,
|
||||
$personHelper,
|
||||
$thirdPartyHelper,
|
||||
$translatableStringExportLabelHelper,
|
||||
$socialIssueRender,
|
||||
$socialIssueRepository,
|
||||
$socialActionRender,
|
||||
$rollingDateConverter,
|
||||
$aggregateStringHelper,
|
||||
$socialActionRepository,
|
||||
$this->getParameters(false),
|
||||
$filterListAccompanyingPeriodHelper->reveal(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -20,6 +20,7 @@ use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Chill\MainBundle\Test\Export\AbstractExportTest;
|
||||
use Chill\PersonBundle\Export\Declarations;
|
||||
use Chill\PersonBundle\Export\Export\ListAccompanyingPeriodWorkAssociatePersonOnWork;
|
||||
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelperInterface;
|
||||
use Chill\PersonBundle\Export\Helper\LabelPersonHelper;
|
||||
use Chill\PersonBundle\Repository\SocialWork\SocialActionRepository;
|
||||
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
|
||||
@@ -27,6 +28,7 @@ use Chill\PersonBundle\Templating\Entity\SocialActionRender;
|
||||
use Chill\PersonBundle\Templating\Entity\SocialIssueRender;
|
||||
use Chill\ThirdPartyBundle\Export\Helper\LabelThirdPartyHelper;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -35,6 +37,8 @@ use Doctrine\ORM\EntityManagerInterface;
|
||||
*/
|
||||
class ListAccompanyingPeriodWorkAssociatePersonOnWorkTest extends AbstractExportTest
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -55,6 +59,7 @@ class ListAccompanyingPeriodWorkAssociatePersonOnWorkTest extends AbstractExport
|
||||
$rollingDateConverter = self::$container->get(RollingDateConverterInterface::class);
|
||||
$aggregateStringHelper = self::$container->get(AggregateStringHelper::class);
|
||||
$socialActionRepository = self::$container->get(SocialActionRepository::class);
|
||||
$filterHelper = $this->prophesize(FilterListAccompanyingPeriodHelperInterface::class);
|
||||
|
||||
yield new ListAccompanyingPeriodWorkAssociatePersonOnWork(
|
||||
$entityManager,
|
||||
@@ -69,23 +74,7 @@ class ListAccompanyingPeriodWorkAssociatePersonOnWorkTest extends AbstractExport
|
||||
$rollingDateConverter,
|
||||
$aggregateStringHelper,
|
||||
$socialActionRepository,
|
||||
$this->getParameters(true),
|
||||
);
|
||||
|
||||
yield new ListAccompanyingPeriodWorkAssociatePersonOnWork(
|
||||
$entityManager,
|
||||
$dateTimeHelper,
|
||||
$userHelper,
|
||||
$personHelper,
|
||||
$thirdPartyHelper,
|
||||
$translatableStringExportLabelHelper,
|
||||
$socialIssueRender,
|
||||
$socialIssueRepository,
|
||||
$socialActionRender,
|
||||
$rollingDateConverter,
|
||||
$aggregateStringHelper,
|
||||
$socialActionRepository,
|
||||
$this->getParameters(false),
|
||||
$filterHelper->reveal(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -11,7 +11,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\PersonBundle\Tests\Export\Export;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Export\Helper\AggregateStringHelper;
|
||||
use Chill\MainBundle\Export\Helper\DateTimeHelper;
|
||||
use Chill\MainBundle\Export\Helper\TranslatableStringExportLabelHelper;
|
||||
@@ -22,13 +21,14 @@ use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Chill\MainBundle\Test\Export\AbstractExportTest;
|
||||
use Chill\PersonBundle\Export\Declarations;
|
||||
use Chill\PersonBundle\Export\Export\ListEvaluation;
|
||||
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelperInterface;
|
||||
use Chill\PersonBundle\Export\Helper\LabelPersonHelper;
|
||||
use Chill\PersonBundle\Repository\SocialWork\SocialActionRepository;
|
||||
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
|
||||
use Chill\PersonBundle\Templating\Entity\SocialActionRender;
|
||||
use Chill\PersonBundle\Templating\Entity\SocialIssueRender;
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -37,7 +37,7 @@ use Doctrine\ORM\EntityManagerInterface;
|
||||
*/
|
||||
class ListEvaluationTest extends AbstractExportTest
|
||||
{
|
||||
private ListEvaluation $listEvaluation;
|
||||
use ProphecyTrait;
|
||||
|
||||
private CenterRepositoryInterface $centerRepository;
|
||||
|
||||
@@ -46,7 +46,6 @@ class ListEvaluationTest extends AbstractExportTest
|
||||
parent::setUp();
|
||||
self::bootKernel();
|
||||
|
||||
$this->listEvaluation = self::$container->get(ListEvaluation::class);
|
||||
$this->centerRepository = self::$container->get(CenterRepositoryInterface::class);
|
||||
}
|
||||
|
||||
@@ -63,6 +62,7 @@ class ListEvaluationTest extends AbstractExportTest
|
||||
$rollingDateConverter = self::$container->get(RollingDateConverterInterface::class);
|
||||
$aggregateStringHelper = self::$container->get(AggregateStringHelper::class);
|
||||
$socialActionRepository = self::$container->get(SocialActionRepository::class);
|
||||
$filterListHelper = $this->prophesize(FilterListAccompanyingPeriodHelperInterface::class);
|
||||
|
||||
yield new ListEvaluation(
|
||||
$entityManager,
|
||||
@@ -76,22 +76,7 @@ class ListEvaluationTest extends AbstractExportTest
|
||||
$translatableStringExportLabelHelper,
|
||||
$aggregateStringHelper,
|
||||
$rollingDateConverter,
|
||||
$this->getParameters(true),
|
||||
);
|
||||
|
||||
yield new ListEvaluation(
|
||||
$entityManager,
|
||||
$socialIssueRender,
|
||||
$socialIssueRepository,
|
||||
$socialActionRender,
|
||||
$socialActionRepository,
|
||||
$userHelper,
|
||||
$personHelper,
|
||||
$dateTimeHelper,
|
||||
$translatableStringExportLabelHelper,
|
||||
$aggregateStringHelper,
|
||||
$rollingDateConverter,
|
||||
$this->getParameters(false),
|
||||
$filterListHelper->reveal(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,14 +89,4 @@ class ListEvaluationTest extends AbstractExportTest
|
||||
{
|
||||
return [[Declarations::ACP_TYPE]];
|
||||
}
|
||||
|
||||
public function testQuery(): void
|
||||
{
|
||||
$centers = $this->centerRepository->findAll();
|
||||
|
||||
$query = $this->listEvaluation->initiateQuery([], array_map(fn (Center $c) => ['center' => $c], $centers), ['calc_date' => new RollingDate(RollingDate::T_TODAY)]);
|
||||
$query->setMaxResults(1);
|
||||
|
||||
self::assertIsArray($query->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY));
|
||||
}
|
||||
}
|
||||
|
@@ -16,9 +16,11 @@ use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Chill\MainBundle\Test\Export\AbstractExportTest;
|
||||
use Chill\PersonBundle\Export\Declarations;
|
||||
use Chill\PersonBundle\Export\Export\ListPersonWithAccompanyingPeriodDetails;
|
||||
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelperInterface;
|
||||
use Chill\PersonBundle\Export\Helper\ListAccompanyingPeriodHelper;
|
||||
use Chill\PersonBundle\Export\Helper\ListPersonHelper;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -27,6 +29,8 @@ use Doctrine\ORM\EntityManagerInterface;
|
||||
*/
|
||||
class ListPersonWithAccompanyingPeriodDetailsTest extends AbstractExportTest
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -39,21 +43,14 @@ class ListPersonWithAccompanyingPeriodDetailsTest extends AbstractExportTest
|
||||
$listAccompanyingPeriodHelper = self::$container->get(ListAccompanyingPeriodHelper::class);
|
||||
$entityManager = self::$container->get(EntityManagerInterface::class);
|
||||
$rollingDateConverter = self::$container->get(RollingDateConverterInterface::class);
|
||||
$filterHelper = $this->prophesize(FilterListAccompanyingPeriodHelperInterface::class);
|
||||
|
||||
yield new ListPersonWithAccompanyingPeriodDetails(
|
||||
$listPersonHelper,
|
||||
$listAccompanyingPeriodHelper,
|
||||
$entityManager,
|
||||
$rollingDateConverter,
|
||||
$this->getParameters(true),
|
||||
);
|
||||
|
||||
yield new ListPersonWithAccompanyingPeriodDetails(
|
||||
$listPersonHelper,
|
||||
$listAccompanyingPeriodHelper,
|
||||
$entityManager,
|
||||
$rollingDateConverter,
|
||||
$this->getParameters(false),
|
||||
$filterHelper->reveal()
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -69,10 +69,10 @@ final class EvaluationTypeFilterTest extends AbstractFilterTest
|
||||
|
||||
return [
|
||||
$em->createQueryBuilder()
|
||||
->select('eval.id')
|
||||
->select('workeval.id')
|
||||
->from(AccompanyingPeriod::class, 'acp')
|
||||
->join('acp.works', 'acpw')
|
||||
->join('acpw.accompanyingPeriodWorkEvaluations', 'eval'),
|
||||
->join('acpw.accompanyingPeriodWorkEvaluations', 'workeval'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,137 @@
|
||||
<?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\Export\Helper;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\CenterRepositoryInterface;
|
||||
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelper;
|
||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
final class FilterListAccompanyingPeriodHelperTest extends KernelTestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private CenterRepositoryInterface $centerRepository;
|
||||
private ScopeRepositoryInterface $scopeRepository;
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
self::bootKernel();
|
||||
|
||||
$this->centerRepository = self::$container->get(CenterRepositoryInterface::class);
|
||||
$this->scopeRepository = self::$container->get(ScopeRepositoryInterface::class);
|
||||
$this->entityManager = self::$container->get(EntityManagerInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderTestAddFilterAccompanyingPeriod
|
||||
*/
|
||||
public function testAddFilterAccompanyingPeriod(QueryBuilder $qb, ParameterBagInterface $parameterBag): void
|
||||
{
|
||||
// mock security
|
||||
$user = $this->entityManager->createQuery('SELECT u FROM '.User::class.' u')
|
||||
->setMaxResults(1)->getSingleResult();
|
||||
if (null === $user) {
|
||||
throw new \RuntimeException('no user found');
|
||||
}
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->getUser()->willReturn($user);
|
||||
|
||||
// mock authorization helper
|
||||
$scopes = $this->scopeRepository->findAll();
|
||||
$scopesConfidentials = [] !== $scopes ? [$scopes[0]] : [];
|
||||
$authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class);
|
||||
$authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE_DETAILS, Argument::type(Center::class))
|
||||
->willReturn($scopes);
|
||||
$authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, Argument::type(Center::class))
|
||||
->willReturn($scopesConfidentials);
|
||||
|
||||
$filter = new FilterListAccompanyingPeriodHelper(
|
||||
$security->reveal(),
|
||||
$this->centerRepository,
|
||||
$authorizationHelper->reveal(),
|
||||
$parameterBag
|
||||
);
|
||||
|
||||
$filter->addFilterAccompanyingPeriods($qb, [], $this->getACL(), []);
|
||||
|
||||
$qb->setMaxResults(1);
|
||||
$result = $qb->getQuery()->getResult();
|
||||
|
||||
self::assertIsArray($result);
|
||||
}
|
||||
|
||||
public function dataProviderTestAddFilterAccompanyingPeriod(): iterable
|
||||
{
|
||||
self::setUp();
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
|
||||
$qb
|
||||
->select('acp.id')
|
||||
->from(AccompanyingPeriod::class, 'acp');
|
||||
|
||||
yield [
|
||||
$qb,
|
||||
new ParameterBag(['chill_main' => ['acl' => ['filter_stats_by_center' => true]]]),
|
||||
];
|
||||
|
||||
yield [
|
||||
$qb,
|
||||
new ParameterBag(['chill_main' => ['acl' => ['filter_stats_by_center' => false]]]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{center: Center, circles: list<Scope>}> the ACL, structured as an array
|
||||
*
|
||||
* @throws \RuntimeException when no center or circle is found
|
||||
*/
|
||||
private function getACL(): array
|
||||
{
|
||||
$centers = $this->centerRepository->findAll();
|
||||
$circles = $this->scopeRepository->findAll();
|
||||
|
||||
if (0 === \count($centers)) {
|
||||
throw new \RuntimeException('No center found. Did you forget to run `doctrine:fixtures:load` command before ?');
|
||||
}
|
||||
|
||||
if (0 === \count($circles)) {
|
||||
throw new \RuntimeException('No circle found. Did you forget to run `doctrine:fixtures:load` command before ?');
|
||||
}
|
||||
|
||||
return [[
|
||||
'center' => $centers[0],
|
||||
'circles' => [
|
||||
$circles,
|
||||
], ]];
|
||||
}
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
<?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 Version20231128143534 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Set a default for the content column of accompanying_period_comment';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql("UPDATE chill_person_accompanying_period_comment SET content='' WHERE content IS NULL");
|
||||
$this->addSql('ALTER TABLE chill_person_accompanying_period_comment ALTER COLUMN content SET NOT NULL');
|
||||
$this->addSql('ALTER TABLE chill_person_accompanying_period_comment ALTER COLUMN content SET DEFAULT \'\'');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_person_accompanying_period_comment ALTER COLUMN content DROP DEFAULT');
|
||||
$this->addSql('ALTER TABLE chill_person_accompanying_period_comment ALTER COLUMN content DROP NOT NULL');
|
||||
}
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Fix lines in chill_person_person_center_history for people created before the introduction of the createdAt column.
|
||||
*
|
||||
* This class represents a migration for fixing lines in the 'chill_person_person_center_history' table.
|
||||
*
|
||||
* It updates the 'startdate' column for people created before the introduction
|
||||
* of the 'createdAt' column, and set it at the first activity date. This migration is irreversible.
|
||||
*/
|
||||
final class Version20231207221700 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Fix lines in chill_person_person_center_history for people created before the introduction of the createdAt column';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('WITH first_history_line AS (SELECT *
|
||||
FROM (SELECT id,
|
||||
person_id,
|
||||
startdate,
|
||||
rank() OVER (PARTITION BY person_id ORDER BY startdate ASC, id ASC) AS r
|
||||
FROM chill_person_person_center_history) AS sk
|
||||
WHERE sk.r = 1),
|
||||
first_activity AS (SELECT *
|
||||
FROM (SELECT id, date, person_id, rank() OVER (PARTITION BY person_id ORDER BY date ASC, id ASC) AS r
|
||||
FROM activity
|
||||
WHERE person_id IS NOT NULL) sq
|
||||
WHERE sq.r = 1)
|
||||
UPDATE chill_person_person_center_history cppch SET startdate=first_activity.date
|
||||
FROM first_history_line, first_activity
|
||||
WHERE
|
||||
first_history_line.id = cppch.id
|
||||
AND first_activity.person_id = cppch.person_id
|
||||
AND first_activity.date < first_history_line.startDate');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->throwIrreversibleMigrationException();
|
||||
}
|
||||
}
|
@@ -11,7 +11,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
|
||||
|
||||
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
|
||||
use ChampsLibres\WopiBundle\Contracts\AuthorizationManagerInterface;
|
||||
use ChampsLibres\WopiBundle\Contracts\UserManagerInterface;
|
||||
use ChampsLibres\WopiBundle\Service\Wopi as CLWopi;
|
||||
@@ -60,8 +59,4 @@ return static function (ContainerConfigurator $container) {
|
||||
->set(UserManager::class);
|
||||
|
||||
$services->alias(UserManagerInterface::class, UserManager::class);
|
||||
|
||||
// TODO: Move this into the async bundle (low priority)
|
||||
$services
|
||||
->alias(TempUrlGeneratorInterface::class, 'async_uploader.temp_url_generator');
|
||||
};
|
||||
|
@@ -10,7 +10,6 @@ declare(strict_types=1);
|
||||
*/
|
||||
|
||||
return [
|
||||
ChampsLibres\AsyncUploaderBundle\ChampsLibresAsyncUploaderBundle::class => ['all' => true],
|
||||
Chill\ActivityBundle\ChillActivityBundle::class => ['all' => true],
|
||||
Chill\AsideActivityBundle\ChillAsideActivityBundle::class => ['all' => true],
|
||||
Chill\CalendarBundle\ChillCalendarBundle::class => ['all' => true],
|
||||
|
@@ -1,14 +0,0 @@
|
||||
champs_libres_async_uploader:
|
||||
openstack:
|
||||
os_username: '%env(resolve:OS_USERNAME)%' # Required
|
||||
os_password: '%env(resolve:OS_PASSWORD)%' # Required
|
||||
os_tenant_id: '%env(resolve:OS_TENANT_ID)%' # Required
|
||||
os_region_name: '%env(resolve:OS_REGION_NAME)%' # Required
|
||||
os_auth_url: '%env(resolve:OS_AUTH_URL)%' # Required
|
||||
temp_url:
|
||||
temp_url_key: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_KEY)%' # Required
|
||||
container: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_CONTAINER)%' # Required
|
||||
temp_url_base_path: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_BASE_PATH)%' # Required. Do not forget a trailing slash
|
||||
max_post_file_size: 15000000 # 15Mo (bytes)
|
||||
max_expires_delay: 180
|
||||
max_submit_delay: 3600
|
6
tests/app/config/packages/chill_doc_store.yaml
Normal file
6
tests/app/config/packages/chill_doc_store.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
chill_doc_store:
|
||||
openstack:
|
||||
temp_url:
|
||||
temp_url_key: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_KEY)%' # Required
|
||||
container: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_CONTAINER)%' # Required
|
||||
temp_url_base_path: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_BASE_PATH)%' # Required
|
Reference in New Issue
Block a user