mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2026-03-09 15:37:46 +00:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
e2dec28577
|
|||
| 30385da409 | |||
| 562fecb4aa | |||
| 8e8f459f90 | |||
| 5de3862ec2 | |||
| 26838648c8 | |||
| 030553a4de | |||
|
966f9f7e33
|
|||
| 7a5300b713 | |||
| dc3a585e5b | |||
| 7712d76889 | |||
|
|
69bb7026c9 | ||
| acd7240903 | |||
| 22049558da | |||
| c0f2f3f3e0 | |||
| bf56b3cc65 | |||
| f85973f7ae | |||
| f1446d7abe | |||
| 76d675ac02 | |||
| cf0a2b7393 | |||
|
|
80b05a8133 | ||
| 69aba8d9c9 | |||
| a87d936828 | |||
| 290fa7a77c | |||
| 0e1d233d79 | |||
|
3402e4863f
|
|||
| 1f0974ea68 | |||
| 9997fb287a | |||
| f9a9de1148 | |||
|
|
c34f720f94 | ||
| e1b1f592fa | |||
| 8546f4dadc |
4
.changes/v4.12.1.md
Normal file
4
.changes/v4.12.1.md
Normal file
@@ -0,0 +1,4 @@
|
||||
## v4.12.1 - 2026-02-01
|
||||
### Fixed
|
||||
* ([#496](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/496)) Add the option to deal with duplicate address in BAN adress importer
|
||||
|
||||
15
.changes/v4.13.0.md
Normal file
15
.changes/v4.13.0.md
Normal file
@@ -0,0 +1,15 @@
|
||||
## v4.13.0 - 2026-02-23
|
||||
### Feature
|
||||
* ([#500](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/500)) ([!964](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/964)) Limit the number of public download of stored object to 30 downloads
|
||||
* ([#495](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/495)) ([!967](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/967)) Send email related to notification in both html and txt format, and render quote correctly
|
||||
|
||||
### Fixed
|
||||
* ([#438](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/438)) Change wrong color of submit button "Désigner comme adresse du parcours"
|
||||
* ([#498](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/498)) For giving edit permissions on documents, take into account the workflow creator
|
||||
* Fixed mispelling of address in translations: addresse -> adresse
|
||||
* ([#499](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/499)) ([!963](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/963)) Fix: some postal code appears in the UI, although they are marked as deleted
|
||||
* ([#501](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/501)) ([!966](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/966)) Fix deprecation in the markdown rendering
|
||||
* ([#494](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/494)) ([!965](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/965)) Remove unused all-day slot display
|
||||
|
||||
### DX
|
||||
* ([!960](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/960)) Configure changie to ask for merge request number for a better tracking of changes
|
||||
6
.changes/v4.14.0.md
Normal file
6
.changes/v4.14.0.md
Normal file
@@ -0,0 +1,6 @@
|
||||
## v4.14.0 - 2026-03-09
|
||||
### Feature
|
||||
* ([#486](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/486)) ([!<no value>](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/<no value>)) Add filter and aggregator based on referrer's main center for exports of accompanying period
|
||||
### Fixed
|
||||
* ([#502](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/502)) ([!968](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/968)) Fix import of postal code: mark postal code as deleted if they are not present in the import any more
|
||||
* ([#503](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/503)) ([!969](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/969)) Add a flash message when reassigning accompanying course (reassign list)
|
||||
@@ -7,7 +7,7 @@ versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}'
|
||||
kindFormat: '### {{.Kind}}'
|
||||
# Note: it is possible to add a `.custom.Long` text manually into the yaml file produced by `changie new`. This will add a long description.
|
||||
changeFormat: >-
|
||||
* {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{ end }}{{ .Body }} {{ if and .Custom.SchemaChange (ne .Custom.SchemaChange "No schema change") }}
|
||||
* {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{ end }}{{ if not (eq .Custom.MR "") }}([!{{ .Custom.MR }}](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/{{ .Custom.MR }})) {{ end }}{{ .Body }} {{ if and .Custom.SchemaChange (ne .Custom.SchemaChange "No schema change") }}
|
||||
|
||||
**Schema Change**: {{ .Custom.SchemaChange }}
|
||||
{{- end -}}
|
||||
@@ -30,6 +30,12 @@ custom:
|
||||
type: int
|
||||
minInt: 1
|
||||
|
||||
- key: MR
|
||||
label: Merge request number (on chill-bundles repository) (optional)
|
||||
optional: true
|
||||
type: int
|
||||
minInt: 1
|
||||
|
||||
body:
|
||||
# allow multiline messages
|
||||
block: true
|
||||
|
||||
@@ -238,13 +238,13 @@ The tests are run from the project's root (not from the bundle's root).
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
vendor/bin/phpunit
|
||||
symfony composer exec phpunit
|
||||
|
||||
# Run a specific test file
|
||||
vendor/bin/phpunit path/to/TestFile.php
|
||||
symfony composer exec phpunit -- path/to/TestFile.php
|
||||
|
||||
# Run a specific test method
|
||||
vendor/bin/phpunit --filter methodName path/to/TestFile.php
|
||||
symfony composer exec phpunit --filter methodName path/to/TestFile.php
|
||||
```
|
||||
|
||||
When writing tests, only test specific files. Do not run all tests or the full
|
||||
|
||||
30
CHANGELOG.md
30
CHANGELOG.md
@@ -6,6 +6,34 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
|
||||
|
||||
## v4.14.0 - 2026-03-09
|
||||
### Feature
|
||||
* ([#486](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/486)) ([!<no value>](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/<no value>)) Add filter and aggregator based on referrer's main center for exports of accompanying period
|
||||
### Fixed
|
||||
* ([#502](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/502)) ([!968](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/968)) Fix import of postal code: mark postal code as deleted if they are not present in the import any more
|
||||
* ([#503](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/503)) ([!969](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/969)) Add a flash message when reassigning accompanying course (reassign list)
|
||||
|
||||
## v4.13.0 - 2026-02-23
|
||||
### Feature
|
||||
* ([#500](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/500)) ([!964](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/964)) Limit the number of public download of stored object to 30 downloads
|
||||
* ([#495](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/495)) ([!967](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/967)) Send email related to notification in both html and txt format, and render quote correctly
|
||||
|
||||
### Fixed
|
||||
* ([#438](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/438)) Change wrong color of submit button "Désigner comme adresse du parcours"
|
||||
* ([#498](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/498)) For giving edit permissions on documents, take into account the workflow creator
|
||||
* Fixed mispelling of address in translations: addresse -> adresse
|
||||
* ([#499](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/499)) ([!963](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/963)) Fix: some postal code appears in the UI, although they are marked as deleted
|
||||
* ([#501](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/501)) ([!966](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/966)) Fix deprecation in the markdown rendering
|
||||
* ([#494](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/494)) ([!965](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/965)) Remove unused all-day slot display
|
||||
|
||||
### DX
|
||||
* ([!960](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/960)) Configure changie to ask for merge request number for a better tracking of changes
|
||||
|
||||
## v4.12.1 - 2026-02-01
|
||||
### Fixed
|
||||
* ([#496](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/496)) Add the option to deal with duplicate address in BAN adress importer
|
||||
|
||||
|
||||
## v4.12.0 - 2026-01-15
|
||||
### Feature
|
||||
* ([#473](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/473)) Display version of chill bundles in application footer
|
||||
@@ -21,6 +49,8 @@ and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
* ([#492](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/492)) fix CommentInput: replace deprecated value binding with model-value
|
||||
* ([#493](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/493)) fix issue with stored object permissions associated with workflows (as attachment, or through a related entity)
|
||||
|
||||
BC: the constructor's signature of `\Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter` has changed.
|
||||
|
||||
## v4.11.0 - 2025-12-17
|
||||
### Feature
|
||||
* ([#478](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/478)) Add filtering to admin lists: social actions, social issues, goals, results, and evaluations
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
"require-dev": {
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.3",
|
||||
"fakerphp/faker": "^1.13",
|
||||
"friendsofphp/php-cs-fixer": "3.92.5",
|
||||
"friendsofphp/php-cs-fixer": "3.93.0",
|
||||
"jangregor/phpstan-prophecy": "^1.0",
|
||||
"nelmio/alice": "^3.8",
|
||||
"nikic/php-parser": "^4.15",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
kind: Added
|
||||
body: Use admin delegated account for handling authentication
|
||||
time: 2026-01-22T15:32:23.932994899+01:00
|
||||
@@ -80,12 +80,19 @@ final readonly class CreateZimbraComponent
|
||||
$location = $calendar->getCalendar()->getLocation();
|
||||
$hasLocation = $calendar->getCalendar()->hasLocation();
|
||||
$isPrivate = $calendar->getCalendar()->getAccompanyingPeriod()?->isConfidential() ?? false;
|
||||
} else {
|
||||
} elseif ($calendar instanceof Calendar) {
|
||||
$startDate = $calendar->getStartDate();
|
||||
$endDate = $calendar->getEndDate();
|
||||
$location = $calendar->getLocation();
|
||||
$hasLocation = $calendar->hasLocation();
|
||||
$isPrivate = $calendar->getAccompanyingPeriod()?->isConfidential() ?? false;
|
||||
} else {
|
||||
// Calendar range case
|
||||
$startDate = $calendar->getStartDate();
|
||||
$endDate = $calendar->getEndDate();
|
||||
$location = $calendar->getLocation();
|
||||
$hasLocation = $calendar->hasLocation();
|
||||
$isPrivate = false;
|
||||
}
|
||||
|
||||
$comp = new InviteComponent();
|
||||
|
||||
@@ -11,48 +11,84 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector;
|
||||
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\HttpClient\Psr18Client;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Zimbra\Admin\AdminApi;
|
||||
use Zimbra\Common\Enum\AccountBy;
|
||||
use Zimbra\Common\Soap\ClientFactory;
|
||||
use Zimbra\Common\Struct\AccountSelector;
|
||||
use Zimbra\Common\Struct\Header\AccountInfo;
|
||||
use Zimbra\Mail\MailApi;
|
||||
|
||||
final readonly class SoapClientBuilder
|
||||
final class SoapClientBuilder
|
||||
{
|
||||
private string $username;
|
||||
private readonly string $username;
|
||||
|
||||
private string $password;
|
||||
private readonly string $password;
|
||||
|
||||
private string $url;
|
||||
private readonly string $url;
|
||||
|
||||
public function __construct(private ParameterBagInterface $parameterBag, private HttpClientInterface $client)
|
||||
{
|
||||
private readonly string $adminUrl;
|
||||
|
||||
private readonly bool $verifyHost;
|
||||
|
||||
private readonly bool $verifyPeer;
|
||||
|
||||
private readonly bool $adminVerifyHost;
|
||||
|
||||
private readonly bool $adminVerifyPeer;
|
||||
|
||||
/**
|
||||
* Keep the cache of the tokens.
|
||||
*
|
||||
* @var array<string, array{token: string, expirationTime: \DateTimeImmutable}>
|
||||
*/
|
||||
private array $tokenCache = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly ParameterBagInterface $parameterBag,
|
||||
private readonly HttpClientInterface $client,
|
||||
private readonly ClockInterface $clock,
|
||||
) {
|
||||
$dsn = $this->parameterBag->get('chill_calendar.remote_calendar_dsn');
|
||||
$url = parse_url($dsn);
|
||||
|
||||
$this->username = urldecode($url['user']);
|
||||
$this->password = urldecode($url['pass']);
|
||||
if ('zimbra+http' === $url['scheme']) {
|
||||
$scheme = 'http://';
|
||||
$scheme = 'http';
|
||||
$port = $url['port'] ?? 80;
|
||||
} elseif ('zimbra+https' === $url['scheme']) {
|
||||
$scheme = 'https://';
|
||||
$scheme = 'https';
|
||||
$port = $url['port'] ?? 443;
|
||||
} else {
|
||||
throw new \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException('Unsupported remote calendar scheme: '.$url['scheme']);
|
||||
}
|
||||
|
||||
$this->url = $scheme.$url['host'].':'.$port;
|
||||
// get attributes for adminUrl
|
||||
$query = [];
|
||||
parse_str($url['query'] ?? '', $query);
|
||||
$adminPort = $query['adminPort'] ?? '7071';
|
||||
$adminHost = $query['adminHost'] ?? $url['host'];
|
||||
$adminScheme = $query['adminScheme'] ?? $scheme;
|
||||
|
||||
$this->verifyPeer = (bool) ($query['verifyPeer'] ?? true);
|
||||
$this->verifyHost = (bool) ($query['verifyHost'] ?? true);
|
||||
$this->adminVerifyHost = (bool) ($query['adminVerifyHost'] ?? $this->verifyPeer);
|
||||
$this->adminVerifyPeer = (bool) ($query['adminVerifyPeer'] ?? $this->verifyHost);
|
||||
|
||||
$this->url = $scheme.'://'.$url['host'].':'.$port;
|
||||
$this->adminUrl = $adminScheme.'://'.$adminHost.':'.$adminPort;
|
||||
}
|
||||
|
||||
private function buildApi(): MailApi
|
||||
{
|
||||
$baseClient = $this->client->withOptions([
|
||||
'base_uri' => $location = $this->url.'/service/soap',
|
||||
'verify_host' => false,
|
||||
'verify_peer' => false,
|
||||
'verify_host' => $this->verifyHost,
|
||||
'verify_peer' => $this->verifyPeer,
|
||||
]);
|
||||
$psr18Client = new Psr18Client($baseClient);
|
||||
$api = new MailApi();
|
||||
@@ -62,12 +98,36 @@ final readonly class SoapClientBuilder
|
||||
return $api;
|
||||
}
|
||||
|
||||
private function buildAdminApi(): AdminApi
|
||||
{
|
||||
$baseClient = $this->client->withOptions([
|
||||
'base_uri' => $location = $this->adminUrl.'/service/admin/soap',
|
||||
'verify_host' => $this->adminVerifyHost,
|
||||
'verify_peer' => $this->adminVerifyPeer,
|
||||
]);
|
||||
$psr18Client = new Psr18Client($baseClient);
|
||||
$api = new AdminApi();
|
||||
$client = ClientFactory::create($location, $psr18Client);
|
||||
$api->setClient($client);
|
||||
|
||||
return $api;
|
||||
}
|
||||
|
||||
public function getApiForAccount(string $accountName): MailApi
|
||||
{
|
||||
$api = $this->buildApi();
|
||||
$response = $api->authByAccountName($this->username, $this->password);
|
||||
['token' => $token, 'expirationTime' => $expirationTime] = $this->tokenCache[$accountName]
|
||||
?? ['token' => null, 'expirationTime' => null];
|
||||
|
||||
$token = $response->getAuthToken();
|
||||
if (null === $token || null === $expirationTime || $expirationTime <= $this->clock->now()) {
|
||||
$adminApi = $this->buildAdminApi();
|
||||
$adminApi->auth($this->username, $this->password);
|
||||
|
||||
$delegateResponse = $adminApi->delegateAuth(new AccountSelector(AccountBy::NAME, $accountName));
|
||||
$token = $delegateResponse->getAuthToken();
|
||||
$expiration = $delegateResponse->getLifetime();
|
||||
$expirationTime = $this->clock->now()->add(new \DateInterval('PT'.$expiration.'S'));
|
||||
$this->tokenCache[$accountName] = ['token' => $token, 'expirationTime' => $expirationTime];
|
||||
}
|
||||
|
||||
$apiBy = $this->buildApi();
|
||||
$apiBy->setAuthToken($token);
|
||||
|
||||
@@ -69,7 +69,7 @@ class ChillActivityExtension extends Extension implements PrependExtensionInterf
|
||||
}
|
||||
|
||||
/** (non-PHPdoc).
|
||||
* @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend()
|
||||
* @see PrependExtensionInterface::prepend()
|
||||
*/
|
||||
public function prependRoutes(ContainerBuilder $container)
|
||||
{
|
||||
|
||||
@@ -56,7 +56,7 @@ class ChillBudgetExtension extends Extension implements PrependExtensionInterfac
|
||||
}
|
||||
|
||||
/** (non-PHPdoc).
|
||||
* @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend()
|
||||
* @see PrependExtensionInterface::prepend()
|
||||
*/
|
||||
public function prependRoutes(ContainerBuilder $container)
|
||||
{
|
||||
|
||||
@@ -346,6 +346,7 @@ const baseOptions = ref<CalendarOptions>({
|
||||
center: "title",
|
||||
right: "timeGridWeek,timeGridDay",
|
||||
},
|
||||
allDaySlot: false,
|
||||
});
|
||||
|
||||
const ranges = computed<EventInput[]>(() => {
|
||||
|
||||
@@ -52,7 +52,7 @@ class ChillCustomFieldsExtension extends Extension implements PrependExtensionIn
|
||||
}
|
||||
|
||||
/** (non-PHPdoc).
|
||||
* @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend()
|
||||
* @see PrependExtensionInterface::prepend()
|
||||
*/
|
||||
public function prepend(ContainerBuilder $container)
|
||||
{
|
||||
|
||||
@@ -25,7 +25,7 @@ class ChoiceWithOtherType extends AbstractType
|
||||
private string $otherValueLabel = 'Other value';
|
||||
|
||||
/** (non-PHPdoc).
|
||||
* @see \Symfony\Component\Form\AbstractType::buildForm()
|
||||
* @see AbstractType::buildForm()
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
@@ -42,7 +42,7 @@ class ChoiceWithOtherType extends AbstractType
|
||||
}
|
||||
|
||||
/** (non-PHPdoc).
|
||||
* @see \Symfony\Component\Form\AbstractType::configureOptions()
|
||||
* @see AbstractType::configureOptions()
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@ use Symfony\Component\Form\FormEvents;
|
||||
class ChoicesListType extends AbstractType
|
||||
{
|
||||
/** (non-PHPdoc).
|
||||
* @see \Symfony\Component\Form\AbstractType::buildForm()
|
||||
* @see AbstractType::buildForm()
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
|
||||
@@ -82,7 +82,7 @@ class CustomFieldProvider implements ContainerAwareInterface
|
||||
/**
|
||||
* (non-PHPdoc).
|
||||
*
|
||||
* @see \Symfony\Component\DependencyInjection\ContainerAwareInterface::setContainer()
|
||||
* @see ContainerAwareInterface::setContainer()
|
||||
*/
|
||||
public function setContainer(?ContainerInterface $container = null)
|
||||
{
|
||||
|
||||
@@ -52,7 +52,7 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
|
||||
}
|
||||
|
||||
/** (non-PHPdoc).
|
||||
* @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend()
|
||||
* @see PrependExtensionInterface::prepend()
|
||||
*/
|
||||
public function prepend(ContainerBuilder $container): void
|
||||
{
|
||||
|
||||
@@ -31,7 +31,8 @@ class LoadAddressesFRFromBANCommand extends Command
|
||||
{
|
||||
$this->setName('chill:main:address-ref-from-ban')
|
||||
->addArgument('departementNo', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'a list of departement numbers')
|
||||
->addOption('send-report-email', 's', InputOption::VALUE_REQUIRED, 'Email address where a list of unimported addresses can be send');
|
||||
->addOption('send-report-email', 's', InputOption::VALUE_REQUIRED, 'Email address where a list of unimported addresses can be send')
|
||||
->addOption('allow-remove-double-refid', 'd', InputOption::VALUE_NONE, 'Should the address importer be allowed to remove same refid in the source data, if any');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
@@ -40,7 +41,7 @@ class LoadAddressesFRFromBANCommand extends Command
|
||||
foreach ($input->getArgument('departementNo') as $departementNo) {
|
||||
$output->writeln('Import addresses for '.$departementNo);
|
||||
|
||||
$this->addressReferenceFromBAN->import($departementNo, $input->hasOption('send-report-email') ? $input->getOption('send-report-email') : null);
|
||||
$this->addressReferenceFromBAN->import($departementNo, $input->hasOption('send-report-email') ? $input->getOption('send-report-email') : null, allowRemoveDoubleRefId: $input->hasOption('allow-remove-double-refid') ? $input->getOption('allow-remove-double-refid') : false);
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
|
||||
@@ -48,7 +48,7 @@ class LoadAndUpdateLanguagesCommand extends Command
|
||||
/**
|
||||
* (non-PHPdoc).
|
||||
*
|
||||
* @see \Symfony\Component\Console\Command\Command::configure()
|
||||
* @see Command::configure()
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
@@ -73,7 +73,7 @@ class LoadAndUpdateLanguagesCommand extends Command
|
||||
/**
|
||||
* (non-PHPdoc).
|
||||
*
|
||||
* @see \Symfony\Component\Console\Command\Command::execute()
|
||||
* @see Command::execute()
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
|
||||
@@ -51,7 +51,7 @@ class LoadCountriesCommand extends Command
|
||||
/**
|
||||
* (non-PHPdoc).
|
||||
*
|
||||
* @see \Symfony\Component\Console\Command\Command::configure()
|
||||
* @see Command::configure()
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
@@ -61,7 +61,7 @@ class LoadCountriesCommand extends Command
|
||||
/**
|
||||
* (non-PHPdoc).
|
||||
*
|
||||
* @see \Symfony\Component\Console\Command\Command::execute()
|
||||
* @see Command::execute()
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
|
||||
@@ -79,5 +79,7 @@ final class PostalCodeAPIController extends ApiController
|
||||
|
||||
$qb->andWhere('e.origin = :zero')
|
||||
->setParameter('zero', 0);
|
||||
|
||||
$qb->andWhere('e.deletedAt IS NULL');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,15 +62,15 @@ final readonly class WorkflowViewSendPublicController
|
||||
);
|
||||
}
|
||||
|
||||
if (100 < $workflowSend->getViews()->count()) {
|
||||
$this->chillLogger->info(self::LOG_PREFIX.'100 view reached, not allowed to see it again');
|
||||
throw new AccessDeniedHttpException('100 views reached, not allowed to see it again');
|
||||
if (30 < $workflowSend->getViews()->count()) {
|
||||
$this->chillLogger->info(self::LOG_PREFIX.'30 view reached, not allowed to see it again');
|
||||
throw new AccessDeniedHttpException('30 views reached, not allowed to see it again');
|
||||
}
|
||||
|
||||
try {
|
||||
$metadata = new EntityWorkflowViewMetadataDTO(
|
||||
$workflowSend->getViews()->count(),
|
||||
100 - $workflowSend->getViews()->count(),
|
||||
30 - $workflowSend->getViews()->count(),
|
||||
);
|
||||
$response = new Response(
|
||||
$this->entityWorkflowManager->renderPublicView($workflowSend, $metadata),
|
||||
|
||||
@@ -20,7 +20,7 @@ class SearchableServicesCompilerPass implements CompilerPassInterface
|
||||
/**
|
||||
* (non-PHPdoc).
|
||||
*
|
||||
* @see \Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface::process()
|
||||
* @see CompilerPassInterface::process()
|
||||
*/
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
|
||||
@@ -32,7 +32,7 @@ abstract class AbstractWidgetFactory implements WidgetFactoryInterface
|
||||
* Will create the definition by returning the definition from the `services.yml`
|
||||
* file (or `services.xml` or `what-you-want.yml`).
|
||||
*
|
||||
* @see \Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface::createDefinition()
|
||||
* @see WidgetFactoryInterface::createDefinition()
|
||||
*/
|
||||
public function createDefinition(ContainerBuilder $containerBuilder, $place, $order, array $config)
|
||||
{
|
||||
|
||||
@@ -215,4 +215,14 @@ class PostalCode implements TrackUpdateInterface, TrackCreationInterface
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isDeleted(): bool
|
||||
{
|
||||
return null !== $this->deletedAt;
|
||||
}
|
||||
|
||||
public function getDeletedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->deletedAt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,6 +394,10 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
||||
|
||||
public function isUserInvolved(User $user): bool
|
||||
{
|
||||
if ($this->getCreatedBy() === $user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($this->getSteps() as $step) {
|
||||
if ($step->getAllDestUser()->contains($user)) {
|
||||
return true;
|
||||
|
||||
@@ -59,7 +59,8 @@ readonly class NotificationMailer
|
||||
$email
|
||||
->to($dest->getEmail())
|
||||
->subject('Re: '.$comment->getNotification()->getTitle())
|
||||
->textTemplate('@ChillMain/Notification/email_notification_comment_persist.md.twig')
|
||||
->textTemplate('@ChillMain/Notification/email_notification_comment_persist.txt.twig')
|
||||
->htmlTemplate('@ChillMain/Notification/email_notification_comment_persist.md.twig')
|
||||
->context([
|
||||
'comment' => $comment,
|
||||
'dest' => $dest,
|
||||
@@ -83,7 +84,6 @@ readonly class NotificationMailer
|
||||
public function postPersistNotification(Notification $notification, PostPersistEventArgs $eventArgs): void
|
||||
{
|
||||
$this->sendNotificationEmailsToAddressees($notification);
|
||||
$this->sendNotificationEmailsToAddressesEmails($notification);
|
||||
}
|
||||
|
||||
private function sendNotificationEmailsToAddressees(Notification $notification): void
|
||||
@@ -149,7 +149,8 @@ readonly class NotificationMailer
|
||||
} else {
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
->textTemplate('@ChillMain/Notification/email_non_system_notification_content.md.twig')
|
||||
->textTemplate('@ChillMain/Notification/email_non_system_notification_content.txt.twig')
|
||||
->htmlTemplate('@ChillMain/Notification/email_non_system_notification_content.md.twig')
|
||||
->context([
|
||||
'notification' => $notification,
|
||||
'dest' => $addressee,
|
||||
@@ -186,7 +187,8 @@ readonly class NotificationMailer
|
||||
} else {
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
->textTemplate('@ChillMain/Notification/email_non_system_notification_content.md.twig')
|
||||
->textTemplate('@ChillMain/Notification/email_non_system_notification_content.txt.twig')
|
||||
->htmlTemplate('@ChillMain/Notification/email_non_system_notification_content.md.twig')
|
||||
->context([
|
||||
'notification' => $notification,
|
||||
'dest' => $addressee,
|
||||
@@ -286,38 +288,4 @@ readonly class NotificationMailer
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function sendNotificationEmailsToAddressesEmails(Notification $notification): void
|
||||
{
|
||||
foreach ($notification->getAddresseeUserGroups() as $userGroup) {
|
||||
|
||||
if (!$userGroup->hasEmail()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$emailAddress = $userGroup->getEmail();
|
||||
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
->textTemplate('@ChillMain/Notification/email_non_system_notification_content_to_email.md.twig')
|
||||
->context([
|
||||
'notification' => $notification,
|
||||
'dest' => $emailAddress,
|
||||
]);
|
||||
|
||||
$email
|
||||
->subject($notification->getTitle())
|
||||
->to($emailAddress);
|
||||
|
||||
try {
|
||||
$this->mailer->send($email);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
$this->logger->warning('[NotificationMailer] could not send an email notification', [
|
||||
'to' => $emailAddress,
|
||||
'error_message' => $e->getMessage(),
|
||||
'error_trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,9 @@ final readonly class PostalCodeRepository implements PostalCodeRepositoryInterfa
|
||||
|
||||
$query
|
||||
->setFromClause('chill_main_postal_code cmpc')
|
||||
->andWhereClause('cmpc.origin = 0');
|
||||
->andWhereClause('cmpc.origin = 0')
|
||||
->andWhereClause('cmpc.deletedAt IS NULL')
|
||||
;
|
||||
|
||||
if (null !== $country) {
|
||||
$query->andWhereClause('cmpc.country_id = ?', [$country->getId()]);
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
{% apply markdown_to_html %}
|
||||
{{ dest.label }},
|
||||
|
||||
{{ notification.sender.label }} a créé une notification pour vous:
|
||||
|
||||
> {{ notification.title }}
|
||||
>
|
||||
>
|
||||
{%- for line in notification.message|split("\n") %}
|
||||
**Titre de la notification**: {{ notification.title }}
|
||||
|
||||
{% for line in notification.message|split("\n") %}
|
||||
> {{ line }}
|
||||
{%- if not loop.last %}
|
||||
>
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{% endfor %}
|
||||
|
||||
Vous pouvez visualiser la notification et y répondre ici:
|
||||
[Vous pouvez visualiser la notification et y répondre ici.]({{ absolute_url(path('chill_main_notification_show', {'_locale': dest.locale, 'id': notification.id }, false)) }})
|
||||
|
||||
{{ absolute_url(path('chill_main_notification_show', {'_locale': dest.locale, 'id': notification.id }, false)) }}
|
||||
-----
|
||||
|
||||
--
|
||||
Le logiciel Chill
|
||||
{% endapply %}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{{ dest.label }},
|
||||
|
||||
{{ notification.sender.label }} a créé une notification pour vous:
|
||||
|
||||
Titre de la notification: {{ notification.title }}
|
||||
|
||||
{% for line in notification.message|split("\n") %}
|
||||
> {{ line|raw }}
|
||||
{% endfor %}
|
||||
|
||||
Vous pouvez visualiser la notification et y répondre ici: {{ absolute_url(path('chill_main_notification_show', {'_locale': dest.locale, 'id': notification.id }, false)) }}.
|
||||
|
||||
--
|
||||
Le logiciel Chill
|
||||
@@ -1,20 +0,0 @@
|
||||
{{ dest }},
|
||||
|
||||
{{ notification.sender.label }} a créé une notification pour vous:
|
||||
|
||||
> {{ notification.title }}
|
||||
>
|
||||
>
|
||||
{%- for line in notification.message|split("\n") %}
|
||||
> {{ line }}
|
||||
{%- if not loop.last %}
|
||||
>
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
|
||||
Vous pouvez cliquer sur ce lien pour obtenir un accès permanent à la notification:
|
||||
|
||||
{{ absolute_url(path('chill_main_notification_grant_access_by_access_key', {'_locale': 'fr', 'id': notification.id, 'accessKey': notification.accessKey, 'email': dest})) }}
|
||||
|
||||
--
|
||||
Le logiciel Chill
|
||||
@@ -1,3 +1,4 @@
|
||||
{% apply markdown_to_html %}
|
||||
{{ dest.label }},
|
||||
|
||||
{{ comment.createdBy.label }} a créé un commentaire sur la notification "{{ comment.notification.title }}".
|
||||
@@ -6,14 +7,11 @@ Commentaire:
|
||||
|
||||
{% for line in comment.content|split("\n") %}
|
||||
> {{ line }}
|
||||
{%- if not loop.last %}
|
||||
>
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{% endfor %}
|
||||
|
||||
Vous pouvez visualiser la notification et y répondre ici:
|
||||
[Vous pouvez visualiser la notification et y répondre ici.]({{ absolute_url(path('chill_main_notification_show', {'_locale': dest.locale, 'id': comment.notification.id }, false)) }})
|
||||
|
||||
{{ absolute_url(path('chill_main_notification_show', {'_locale': dest.locale, 'id': comment.notification.id }, false)) }}
|
||||
----
|
||||
|
||||
--
|
||||
Le logiciel Chill
|
||||
{% endapply %}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{{ dest.label }},
|
||||
|
||||
{{ comment.createdBy.label }} a créé un commentaire sur la notification "{{ comment.notification.title }}".
|
||||
|
||||
Commentaire:
|
||||
|
||||
{% for line in comment.content|split("\n") %}
|
||||
> {{ line }}
|
||||
{%- endfor %}
|
||||
|
||||
Vous pouvez visualiser la notification et y répondre ici: {{ absolute_url(path('chill_main_notification_show', {'_locale': dest.locale, 'id': comment.notification.id }, false)) }}
|
||||
|
||||
--
|
||||
Le logiciel Chill
|
||||
@@ -33,7 +33,7 @@ final readonly class ScopeResolverDispatcher
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Scope|iterable<Scope>|Scope|null
|
||||
* @return Scope|iterable<Scope>|null
|
||||
*/
|
||||
public function resolveScope(mixed $entity, ?array $options = []): iterable|Scope|null
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ class AddressReferenceFromBAN
|
||||
private readonly AddressToReferenceMatcher $addressToReferenceMatcher,
|
||||
) {}
|
||||
|
||||
public function import(string $departementNo, ?string $sendAddressReportToEmail = null): void
|
||||
public function import(string $departementNo, ?string $sendAddressReportToEmail = null, ?bool $allowRemoveDoubleRefId = false): void
|
||||
{
|
||||
if (!is_numeric($departementNo)) {
|
||||
throw new \UnexpectedValueException('Could not parse this department number');
|
||||
@@ -96,7 +96,7 @@ class AddressReferenceFromBAN
|
||||
);
|
||||
}
|
||||
|
||||
$this->baseImporter->finalize(sendAddressReportToEmail: $sendAddressReportToEmail);
|
||||
$this->baseImporter->finalize(allowRemoveDoubleRefId: $allowRemoveDoubleRefId, sendAddressReportToEmail: $sendAddressReportToEmail);
|
||||
|
||||
$this->addressToReferenceMatcher->checkAddressesMatchingReferences();
|
||||
|
||||
|
||||
@@ -19,31 +19,66 @@ use Doctrine\DBAL\Statement;
|
||||
*/
|
||||
class PostalCodeBaseImporter
|
||||
{
|
||||
private const QUERY = <<<'SQL'
|
||||
private const CREATE_TEMP_TABLE = <<<'SQL'
|
||||
CREATE TEMPORARY TABLE chill_main_postal_code_temp (
|
||||
countrycode VARCHAR(10),
|
||||
label VARCHAR(255),
|
||||
code VARCHAR(100),
|
||||
refpostalcodeid VARCHAR(255),
|
||||
postalcodeSource VARCHAR(255),
|
||||
lon FLOAT,
|
||||
lat FLOAT,
|
||||
srid INT
|
||||
)
|
||||
SQL;
|
||||
|
||||
private const INSERT_TEMP = <<<'SQL'
|
||||
INSERT INTO chill_main_postal_code_temp
|
||||
(countrycode, label, code, refpostalcodeid, postalcodeSource, lon, lat, srid)
|
||||
VALUES
|
||||
{{ values }}
|
||||
SQL;
|
||||
|
||||
private const UPSERT = <<<'SQL'
|
||||
WITH g AS (
|
||||
SELECT DISTINCT
|
||||
country.id AS country_id,
|
||||
g.*
|
||||
FROM (VALUES
|
||||
{{ values }}
|
||||
) AS g (countrycode, label, code, refpostalcodeid, postalcodeSource, lon, lat, srid)
|
||||
JOIN country ON country.countrycode = g.countrycode
|
||||
temp.*
|
||||
FROM chill_main_postal_code_temp temp
|
||||
JOIN country ON country.countrycode = temp.countrycode
|
||||
)
|
||||
INSERT INTO chill_main_postal_code (id, country_id, label, code, origin, refpostalcodeid, postalcodeSource, center, createdAt, updatedAt)
|
||||
INSERT INTO chill_main_postal_code (id, country_id, label, code, origin, refpostalcodeid, postalcodeSource, center, createdAt, updatedAt, deletedAt)
|
||||
SELECT
|
||||
nextval('chill_main_postal_code_id_seq'),
|
||||
g.country_id,
|
||||
g.label AS glabel,
|
||||
g.label,
|
||||
g.code,
|
||||
0,
|
||||
g.refpostalcodeid,
|
||||
g.postalcodeSource,
|
||||
CASE WHEN (g.lon::float != 0.0 AND g.lat::float != 0.0) THEN ST_Transform(ST_setSrid(ST_point(g.lon::float, g.lat::float), g.srid::int), 4326) ELSE NULL END,
|
||||
CASE WHEN (g.lon != 0.0 AND g.lat != 0.0) THEN ST_Transform(ST_setSrid(ST_point(g.lon, g.lat), g.srid), 4326) ELSE NULL END,
|
||||
NOW(),
|
||||
NOW()
|
||||
NOW(),
|
||||
NULL
|
||||
FROM g
|
||||
ON CONFLICT (code, refpostalcodeid, postalcodeSource) WHERE refpostalcodeid IS NOT NULL DO UPDATE
|
||||
SET label = excluded.label, center = excluded.center, updatedAt = CASE WHEN NOT st_equals(excluded.center, chill_main_postal_code.center) OR excluded.label != chill_main_postal_code.label THEN NOW() ELSE chill_main_postal_code.updatedAt END
|
||||
SET label = excluded.label,
|
||||
center = excluded.center,
|
||||
deletedAt = NULL,
|
||||
updatedAt = CASE WHEN NOT st_equals(excluded.center, chill_main_postal_code.center) OR excluded.label != chill_main_postal_code.label OR chill_main_postal_code.deletedAt IS NOT NULL THEN NOW() ELSE chill_main_postal_code.updatedAt END
|
||||
SQL;
|
||||
|
||||
private const DELETE_MISSING = <<<'SQL'
|
||||
UPDATE chill_main_postal_code
|
||||
SET deletedAt = NOW(), updatedAt = NOW()
|
||||
WHERE postalcodeSource = ?
|
||||
AND deletedAt IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM chill_main_postal_code_temp temp
|
||||
WHERE temp.code = chill_main_postal_code.code
|
||||
AND temp.refpostalcodeid = chill_main_postal_code.refpostalcodeid
|
||||
AND temp.postalcodeSource = chill_main_postal_code.postalcodeSource
|
||||
)
|
||||
SQL;
|
||||
|
||||
private const VALUE = '(?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
@@ -55,11 +90,26 @@ class PostalCodeBaseImporter
|
||||
|
||||
private array $waitingForInsert = [];
|
||||
|
||||
private bool $isInitialized = false;
|
||||
|
||||
private ?string $currentSource = null;
|
||||
|
||||
public function __construct(private readonly Connection $defaultConnection) {}
|
||||
|
||||
public function finalize(): void
|
||||
{
|
||||
$this->doInsertPending();
|
||||
|
||||
if ($this->isInitialized && null !== $this->currentSource) {
|
||||
$this->defaultConnection->transactional(function (Connection $connection): void {
|
||||
$connection->executeStatement(self::UPSERT);
|
||||
$connection->executeStatement(self::DELETE_MISSING, [$this->currentSource]);
|
||||
});
|
||||
$this->deleteTemporaryTable();
|
||||
}
|
||||
|
||||
$this->isInitialized = false;
|
||||
$this->currentSource = null;
|
||||
}
|
||||
|
||||
public function importCode(
|
||||
@@ -72,6 +122,14 @@ class PostalCodeBaseImporter
|
||||
float $centerLon,
|
||||
int $centerSRID,
|
||||
): void {
|
||||
if (!$this->isInitialized) {
|
||||
$this->initialize($refPostalCodeSource);
|
||||
}
|
||||
|
||||
if ($this->currentSource !== $refPostalCodeSource) {
|
||||
throw new \LogicException('Cannot store postal codes from different sources during same import. Execute finalize to commit inserts before changing the source');
|
||||
}
|
||||
|
||||
$this->waitingForInsert[] = [
|
||||
$countryCode,
|
||||
$label,
|
||||
@@ -88,10 +146,32 @@ class PostalCodeBaseImporter
|
||||
}
|
||||
}
|
||||
|
||||
private function initialize(string $source): void
|
||||
{
|
||||
$this->currentSource = $source;
|
||||
$this->deleteTemporaryTable();
|
||||
$this->createTemporaryTable();
|
||||
$this->isInitialized = true;
|
||||
}
|
||||
|
||||
private function createTemporaryTable(): void
|
||||
{
|
||||
$this->defaultConnection->executeStatement(self::CREATE_TEMP_TABLE);
|
||||
}
|
||||
|
||||
private function deleteTemporaryTable(): void
|
||||
{
|
||||
$this->defaultConnection->executeStatement('DROP TABLE IF EXISTS chill_main_postal_code_temp');
|
||||
}
|
||||
|
||||
private function doInsertPending(): void
|
||||
{
|
||||
if ([] == $this->waitingForInsert) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!\array_key_exists($forNumber = \count($this->waitingForInsert), $this->cachingStatements)) {
|
||||
$sql = strtr(self::QUERY, [
|
||||
$sql = strtr(self::INSERT_TEMP, [
|
||||
'{{ values }}' => implode(
|
||||
', ',
|
||||
array_fill(0, $forNumber, self::VALUE)
|
||||
|
||||
@@ -41,6 +41,6 @@ final class ChillMarkdownRenderExtension extends AbstractExtension
|
||||
|
||||
public function renderMarkdownToHtml(?string $var): string
|
||||
{
|
||||
return $this->parsedown->parse((string) $var);
|
||||
return $this->parsedown->text((string) $var);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
/**
|
||||
* Helper to test filters.
|
||||
@@ -255,8 +256,8 @@ abstract class AbstractFilterTest extends KernelTestCase
|
||||
$description = $this->getFilter()->describeAction($data, $context);
|
||||
|
||||
$this->assertTrue(
|
||||
\is_string($description) || \is_array($description),
|
||||
'test that the description is a string or an array'
|
||||
\is_string($description) || \is_array($description) || $description instanceof TranslatableInterface,
|
||||
'test that the description is a string or an array, or a TranslatableInterface'
|
||||
);
|
||||
|
||||
if (\is_string($description)) {
|
||||
|
||||
@@ -93,4 +93,80 @@ final class PostalCodeBaseImporterTest extends KernelTestCase
|
||||
$this->assertStringStartsWith('tested with adapted pattern', $postalCodes[0]->getName());
|
||||
$this->assertEquals($previousId, $postalCodes[0]->getId());
|
||||
}
|
||||
|
||||
public function testPostalCodeRemoval(): void
|
||||
{
|
||||
$source = 'removal_test_'.uniqid();
|
||||
$refId1 = 'ref1_'.uniqid();
|
||||
$refId2 = 'ref2_'.uniqid();
|
||||
|
||||
// 1. Import two postal codes
|
||||
$this->importer->importCode('BE', 'Label 1', '1000', $refId1, $source, 50.0, 5.0, 4326);
|
||||
$this->importer->importCode('BE', 'Label 2', '2000', $refId2, $source, 50.0, 5.0, 4326);
|
||||
$this->importer->finalize();
|
||||
|
||||
$pc1 = $this->postalCodeRepository->findOneBy(['refPostalCodeId' => $refId1, 'postalCodeSource' => $source]);
|
||||
$pc2 = $this->postalCodeRepository->findOneBy(['refPostalCodeId' => $refId2, 'postalCodeSource' => $source]);
|
||||
|
||||
$this->assertNotNull($pc1);
|
||||
$this->assertNotNull($pc2);
|
||||
|
||||
// 2. Import only the first one
|
||||
$this->importer->importCode('BE', 'Label 1 updated', '1000', $refId1, $source, 50.0, 5.0, 4326);
|
||||
$this->importer->finalize();
|
||||
|
||||
$this->entityManager->clear();
|
||||
|
||||
$pc1 = $this->postalCodeRepository->findOneBy(['refPostalCodeId' => $refId1, 'postalCodeSource' => $source]);
|
||||
$pc2 = $this->postalCodeRepository->findOneBy(['refPostalCodeId' => $refId2, 'postalCodeSource' => $source]);
|
||||
|
||||
$this->assertNotNull($pc1);
|
||||
$this->assertEquals('Label 1 updated', $pc1->getName());
|
||||
|
||||
$this->assertFalse($pc1->isDeleted(), 'pc1 should NOT be marked as deleted');
|
||||
|
||||
// pc2 should be marked as deleted. Note: findOneBy might still find it if it doesn't filter by deletedAt
|
||||
$this->assertNotNull($pc2);
|
||||
|
||||
$this->assertTrue($pc2->isDeleted(), 'Postal code should be marked as deleted (deletedAt is not null)');
|
||||
|
||||
// 3. Reactivate pc2 by re-importing it
|
||||
$this->importer->importCode('BE', 'Label 2 restored', '2000', $refId2, $source, 50.0, 5.0, 4326);
|
||||
$this->importer->finalize();
|
||||
|
||||
$this->entityManager->clear();
|
||||
$pc2 = $this->postalCodeRepository->findOneBy(['refPostalCodeId' => $refId2, 'postalCodeSource' => $source]);
|
||||
$this->assertFalse($pc2->isDeleted(), 'Postal code should NOT be marked as deleted after restoration');
|
||||
$this->assertEquals('Label 2 restored', $pc2->getName());
|
||||
}
|
||||
|
||||
public function testNoInterferenceBetweenSources(): void
|
||||
{
|
||||
$source1 = 'source1_'.uniqid();
|
||||
$source2 = 'source2_'.uniqid();
|
||||
$refId1 = 'ref1_'.uniqid();
|
||||
$refId2 = 'ref2_'.uniqid();
|
||||
|
||||
// 1. Import from source1
|
||||
$this->importer->importCode('BE', 'Label 1', '1000', $refId1, $source1, 50.0, 5.0, 4326);
|
||||
$this->importer->finalize();
|
||||
|
||||
$pc1 = $this->postalCodeRepository->findOneBy(['refPostalCodeId' => $refId1, 'postalCodeSource' => $source1]);
|
||||
$this->assertNotNull($pc1);
|
||||
$this->assertFalse($pc1->isDeleted());
|
||||
|
||||
// 2. Import from source2
|
||||
$this->importer->importCode('BE', 'Label 2', '2000', $refId2, $source2, 50.0, 5.0, 4326);
|
||||
$this->importer->finalize();
|
||||
|
||||
$this->entityManager->clear();
|
||||
|
||||
$pc1 = $this->postalCodeRepository->findOneBy(['refPostalCodeId' => $refId1, 'postalCodeSource' => $source1]);
|
||||
$pc2 = $this->postalCodeRepository->findOneBy(['refPostalCodeId' => $refId2, 'postalCodeSource' => $source2]);
|
||||
|
||||
$this->assertNotNull($pc1);
|
||||
$this->assertNotNull($pc2);
|
||||
$this->assertFalse($pc1->isDeleted(), 'pc1 from source1 should NOT be deleted after import from source2');
|
||||
$this->assertFalse($pc2->isDeleted(), 'pc2 from source2 should NOT be deleted');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ final class ChillMarkdownRenderExtensionTest extends TestCase
|
||||
MD;
|
||||
|
||||
private const UNAUTHORIZED_HTML = <<<'HTML'
|
||||
<p><script>alert("ok");</script></p>
|
||||
<p><script>alert("ok");</script></p>
|
||||
HTML;
|
||||
|
||||
private const UNAUTHORIZED_MARKDOWN = <<<'MD'
|
||||
|
||||
@@ -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\Migrations\Main;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260223134919 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create a partial index for postal_code search_name_code, to avoid deleted records';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX public.search_name_code');
|
||||
$this->addSql('CREATE INDEX search_name_code ON public.chill_main_postal_code USING GIN (LOWER(code) gin_trgm_ops, LOWER(label) gin_trgm_ops) WHERE deletedAt IS NULL');
|
||||
$this->addSql('DROP INDEX public.chill_internal_postal_code_canonicalized');
|
||||
$this->addSql('CREATE INDEX chill_internal_postal_code_canonicalized ON chill_main_postal_code USING GIST (canonical gist_trgm_ops) WHERE origin = 0 AND deletedAt IS NULL');
|
||||
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX public.search_name_code');
|
||||
$this->addSql('CREATE INDEX search_name_code ON chill_main_postal_code USING GIN (LOWER(code) gin_trgm_ops, LOWER(label) gin_trgm_ops)');
|
||||
|
||||
$this->addSql('DROP INDEX public.chill_internal_postal_code_canonicalized');
|
||||
$this->addSql('CREATE INDEX chill_internal_postal_code_canonicalized ON chill_main_postal_code USING GIST (canonical gist_trgm_ops) WHERE origin = 0');
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
common:
|
||||
after: Après
|
||||
until: Jusqu'à
|
||||
centers: Territoires
|
||||
"This program is free software: you can redistribute it and/or modify it under the terms of the <strong>GNU Affero General Public License</strong>": "Ce programme est un logiciel libre: vous pouvez le redistribuer et/ou le modifier selon les termes de la licence <strong>GNU Affero GPL</strong>"
|
||||
User manual: Manuel d'utilisation
|
||||
Search: Rechercher
|
||||
|
||||
@@ -17,7 +17,6 @@ use Chill\MainBundle\Form\Type\PickPostalCodeType;
|
||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Repository\UserRepository;
|
||||
use Chill\MainBundle\Templating\Entity\UserRender;
|
||||
use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepositoryInterface;
|
||||
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
|
||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
|
||||
@@ -31,18 +30,29 @@ use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
use Symfony\Component\Validator\Constraints\NotIdenticalTo;
|
||||
use Symfony\Component\Validator\Constraints\NotNull;
|
||||
|
||||
class ReassignAccompanyingPeriodController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly AccompanyingPeriodACLAwareRepositoryInterface $accompanyingPeriodACLAwareRepository, private readonly UserRepository $userRepository, private readonly AccompanyingPeriodRepository $courseRepository, private readonly \Twig\Environment $engine, private readonly FormFactoryInterface $formFactory, private readonly PaginatorFactory $paginatorFactory, private readonly Security $security, private readonly UserRender $userRender, private readonly EntityManagerInterface $em) {}
|
||||
public function __construct(
|
||||
private readonly AccompanyingPeriodACLAwareRepositoryInterface $accompanyingPeriodACLAwareRepository,
|
||||
private readonly UserRepository $userRepository,
|
||||
private readonly AccompanyingPeriodRepository $courseRepository,
|
||||
private readonly \Twig\Environment $engine,
|
||||
private readonly FormFactoryInterface $formFactory,
|
||||
private readonly PaginatorFactory $paginatorFactory,
|
||||
private readonly Security $security,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
#[Route(path: '/{_locale}/person/accompanying-periods/reassign', name: 'chill_course_list_reassign')]
|
||||
public function listAction(Request $request): Response
|
||||
public function listAction(Request $request, Session $session): Response
|
||||
{
|
||||
if (!$this->security->isGranted(AccompanyingPeriodVoter::REASSIGN_BULK)) {
|
||||
throw new AccessDeniedHttpException('no right to reassign bulk');
|
||||
@@ -96,7 +106,8 @@ class ReassignAccompanyingPeriodController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
$this->entityManager->flush();
|
||||
$this->addFlash('success', new TranslatableMessage('period_by_user_list.successfully_re_assigned', ['count' => count($assignPeriodIds)]));
|
||||
|
||||
// redirect to the first page
|
||||
return $this->redirectToRoute('chill_course_list_reassign', $request->query->all());
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
|
||||
|
||||
use Chill\MainBundle\Export\AggregatorInterface;
|
||||
use Chill\MainBundle\Export\DataTransformerInterface;
|
||||
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||
use Chill\MainBundle\Repository\CenterRepositoryInterface;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Chill\PersonBundle\Export\Declarations;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
final readonly class ReferrerMainCenterAggregator implements AggregatorInterface, DataTransformerInterface
|
||||
{
|
||||
private const P = 'acp_agg_referrer_main_center';
|
||||
|
||||
public function __construct(
|
||||
private CenterRepositoryInterface $centerRepository,
|
||||
private RollingDateConverterInterface $rollingDateConverter,
|
||||
) {}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void
|
||||
{
|
||||
$p = self::P;
|
||||
|
||||
$qb
|
||||
->leftJoin('acp.userHistories', "{$p}_uh", Join::WITH, $qb->expr()->andX(
|
||||
$qb->expr()->eq("{$p}_uh.accompanyingPeriod", 'acp.id'),
|
||||
"OVERLAPSI (acp.openingDate, acp.closingDate), ({$p}_uh.startDate, {$p}_uh.endDate) = TRUE",
|
||||
"OVERLAPSI (:{$p}_startDate, :{$p}_endDate), ({$p}_uh.startDate, {$p}_uh.endDate) = TRUE"
|
||||
))
|
||||
->leftJoin("{$p}_uh.user", "{$p}_user")
|
||||
->addSelect("IDENTITY({$p}_user.mainCenter) AS {$p}_select")
|
||||
->addGroupBy("{$p}_select")
|
||||
->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date']))
|
||||
->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date']));
|
||||
}
|
||||
|
||||
public function applyOn(): string
|
||||
{
|
||||
return Declarations::ACP_TYPE;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder): void
|
||||
{
|
||||
$builder
|
||||
->add('start_date', PickRollingDateType::class, [
|
||||
'label' => 'common.after',
|
||||
'required' => true,
|
||||
])
|
||||
->add('end_date', PickRollingDateType::class, [
|
||||
'label' => 'common.until',
|
||||
'required' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getNormalizationVersion(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function normalizeFormData(array $formData): array
|
||||
{
|
||||
return [
|
||||
'start_date' => $formData['start_date']->normalize(),
|
||||
'end_date' => $formData['end_date']->normalize(),
|
||||
];
|
||||
}
|
||||
|
||||
public function denormalizeFormData(array $formData, int $fromVersion): array
|
||||
{
|
||||
$default = $this->getFormDefaultData();
|
||||
|
||||
return [
|
||||
'start_date' => array_key_exists('start_date', $formData) ? RollingDate::fromNormalized($formData['start_date']) : $default['start_date'],
|
||||
'end_date' => array_key_exists('end_date', $formData) ? RollingDate::fromNormalized($formData['end_date']) : $default['end_date'],
|
||||
];
|
||||
}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [
|
||||
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
|
||||
'end_date' => new RollingDate(RollingDate::T_TODAY),
|
||||
];
|
||||
}
|
||||
|
||||
public function transformData(?array $before): array
|
||||
{
|
||||
$default = $this->getFormDefaultData();
|
||||
|
||||
if (null === $before) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return [
|
||||
'start_date' => $before['start_date'] ?? $before['date_calc'] ?? $default['start_date'],
|
||||
'end_date' => $before['end_date'] ?? $before['date_calc'] ?? $default['end_date'],
|
||||
];
|
||||
}
|
||||
|
||||
public function getLabels($key, array $values, $data): callable
|
||||
{
|
||||
return function ($value): string {
|
||||
if ('_header' === $value) {
|
||||
return 'person.export.period.aggregator.by_referrer_main_center.column_header';
|
||||
}
|
||||
|
||||
if (null === $value || '' === $value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return (string) $this->centerRepository->find((int) $value)?->getName();
|
||||
};
|
||||
}
|
||||
|
||||
public function getQueryKeys($data): array
|
||||
{
|
||||
return [self::P.'_select'];
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'person.export.period.aggregator.by_referrer_main_center.title';
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ final readonly class CenterAggregator implements AggregatorInterface
|
||||
{
|
||||
return function (int|string|null $value) {
|
||||
if (null === $value || '' === $value) {
|
||||
return $this->translator->trans('person.export.aggregator.by_center.no_center');
|
||||
return $this->translator->trans('person.export.period.aggregator.by_center.no_center');
|
||||
}
|
||||
|
||||
if ('_header' === $value) {
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Export\ExportGenerationContext;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||
use Chill\MainBundle\Repository\CenterRepositoryInterface;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory;
|
||||
use Chill\PersonBundle\Export\Declarations;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
/**
|
||||
* Filter accompanying periods by the main center of their referrer (at a given date).
|
||||
*/
|
||||
final readonly class ReferrerMainCenterFilter implements FilterInterface
|
||||
{
|
||||
private const UH = 'acp_referrer_main_center_filter_uh';
|
||||
private const DATE_PARAM_SINCE = 'acp_referrer_main_center_filter_date_since';
|
||||
private const DATE_PARAM_UNTIL = 'acp_referrer_main_center_filter_date_until';
|
||||
|
||||
private const CENTER_PARAM = 'acp_referrer_main_center_filter_center';
|
||||
|
||||
public function __construct(
|
||||
private RollingDateConverterInterface $rollingDateConverter,
|
||||
private CenterRepositoryInterface $centerRepository,
|
||||
) {}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void
|
||||
{
|
||||
$dql = 'SELECT 1 FROM '.UserHistory::class.' '.self::UH.'
|
||||
JOIN '.self::UH.'.user '.self::UH.'_user
|
||||
WHERE acp = '.self::UH.'.accompanyingPeriod
|
||||
AND (
|
||||
'.self::UH.'.startDate < :'.self::DATE_PARAM_UNTIL.'
|
||||
AND (
|
||||
'.self::UH.'.endDate IS NULL OR '.self::UH.'.endDate >= :'.self::DATE_PARAM_SINCE.'
|
||||
)
|
||||
)
|
||||
AND '.self::UH.'_user.mainCenter IN (:'.self::CENTER_PARAM.')';
|
||||
|
||||
$qb->andWhere(
|
||||
$qb->expr()->exists($dql)
|
||||
);
|
||||
$qb
|
||||
->setParameter(self::DATE_PARAM_SINCE, $this->rollingDateConverter->convert($data['date_calc_since']))
|
||||
->setParameter(self::DATE_PARAM_UNTIL, $this->rollingDateConverter->convert($data['date_calc_until']))
|
||||
->setParameter(self::CENTER_PARAM, $data['centers']);
|
||||
}
|
||||
|
||||
public function applyOn(): string
|
||||
{
|
||||
return Declarations::ACP_TYPE;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder): void
|
||||
{
|
||||
$builder
|
||||
->add('centers', EntityType::class, [
|
||||
'class' => Center::class,
|
||||
'choices' => $this->centerRepository->findActive(),
|
||||
'multiple' => true,
|
||||
'expanded' => false,
|
||||
'choice_label' => static fn (Center $c) => $c->getName(),
|
||||
'required' => true,
|
||||
'label' => 'common.centers',
|
||||
'attr' => [
|
||||
'class' => 'select2',
|
||||
],
|
||||
])
|
||||
->add('date_calc_since', PickRollingDateType::class, [
|
||||
'label' => 'person.export.period.filter.by_referrer_main_center.referrer_since',
|
||||
'required' => true,
|
||||
])
|
||||
->add('date_calc_until', PickRollingDateType::class, [
|
||||
'label' => 'person.export.period.filter.by_referrer_main_center.referrer_until',
|
||||
'required' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getNormalizationVersion(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function normalizeFormData(array $formData): array
|
||||
{
|
||||
return [
|
||||
'centers' => array_values(array_map(static fn (Center $c) => $c->getId(), $formData['centers'])),
|
||||
'date_calc_since' => $formData['date_calc_since']->normalize(),
|
||||
'date_calc_until' => $formData['date_calc_until']->normalize(),
|
||||
];
|
||||
}
|
||||
|
||||
public function denormalizeFormData(array $formData, int $fromVersion): array
|
||||
{
|
||||
return [
|
||||
'centers' => array_values(array_filter(array_map(
|
||||
fn (int $id) => $this->centerRepository->find($id),
|
||||
$formData['centers'] ?? []
|
||||
))),
|
||||
'date_calc_since' => RollingDate::fromNormalized($formData['date_calc_since']),
|
||||
'date_calc_until' => RollingDate::fromNormalized($formData['date_calc_until']),
|
||||
];
|
||||
}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [
|
||||
'centers' => [],
|
||||
'date_calc_since' => new RollingDate(RollingDate::T_TODAY),
|
||||
'date_calc_until' => new RollingDate(RollingDate::T_TODAY),
|
||||
];
|
||||
}
|
||||
|
||||
public function describeAction($data, ExportGenerationContext $context): TranslatableInterface
|
||||
{
|
||||
$names = array_map(static fn (Center $c) => $c->getName(), $data['centers']);
|
||||
|
||||
return new TranslatableMessage(
|
||||
'person.export.period.filter.by_referrer_main_center.description',
|
||||
[
|
||||
'centers' => implode(', ', $names),
|
||||
'date_since' => $this->rollingDateConverter->convert($data['date_calc_since']),
|
||||
'date_until' => $this->rollingDateConverter->convert($data['date_calc_until']),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getTitle(): TranslatableInterface
|
||||
{
|
||||
return new TranslatableMessage('person.export.period.filter.by_referrer_main_center.title');
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@
|
||||
<p>{{ $t("courselocation.sure_description") }}</p>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button class="btn btn-danger" @click="assignAddress">
|
||||
<button class="btn btn-submit" @click="assignAddress">
|
||||
{{ $t("courselocation.ok") }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -167,7 +167,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
/**
|
||||
* (non-PHPdoc).
|
||||
*
|
||||
* @see \Chill\MainBundle\Search\SearchInterface::getOrder()
|
||||
* @see SearchInterface::getOrder()
|
||||
*/
|
||||
public function getOrder(): int
|
||||
{
|
||||
@@ -177,7 +177,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
/**
|
||||
* (non-PHPdoc).
|
||||
*
|
||||
* @see \Chill\MainBundle\Search\SearchInterface::isActiveByDefault()
|
||||
* @see SearchInterface::isActiveByDefault()
|
||||
*/
|
||||
public function isActiveByDefault()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<?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\Aggregator\AccompanyingCourseAggregators;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ReferrerMainCenterAggregator;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @covers \Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ReferrerMainCenterAggregator
|
||||
*/
|
||||
final class ReferrerMainCenterAggregatorTest extends AbstractAggregatorTest
|
||||
{
|
||||
private ReferrerMainCenterAggregator $aggregator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$this->aggregator = self::getContainer()->get('Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ReferrerMainCenterAggregator');
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideBeforeData
|
||||
*/
|
||||
public function testDataTransformer(?array $before, array $expected): void
|
||||
{
|
||||
$actual = $this->getAggregator()->transformData($before);
|
||||
|
||||
self::assertEqualsCanonicalizing(array_keys($expected), array_keys($actual));
|
||||
foreach (['start_date', 'end_date'] as $key) {
|
||||
self::assertInstanceOf(RollingDate::class, $actual[$key]);
|
||||
self::assertEquals($expected[$key]->getRoll(), $actual[$key]->getRoll(), "Check that the roll is the same for {$key}");
|
||||
}
|
||||
}
|
||||
|
||||
public static function provideBeforeData(): iterable
|
||||
{
|
||||
yield [
|
||||
['date_calc' => new RollingDate(RollingDate::T_TODAY)],
|
||||
['start_date' => new RollingDate(RollingDate::T_TODAY), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
|
||||
];
|
||||
|
||||
yield [
|
||||
['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
|
||||
['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
|
||||
];
|
||||
|
||||
yield [
|
||||
null,
|
||||
// this is the default configuration
|
||||
['start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
|
||||
];
|
||||
}
|
||||
|
||||
public function getAggregator(): ReferrerMainCenterAggregator
|
||||
{
|
||||
return $this->aggregator;
|
||||
}
|
||||
|
||||
public static function getFormData(): array
|
||||
{
|
||||
self::bootKernel();
|
||||
$em = self::getContainer()->get(EntityManagerInterface::class);
|
||||
$centers = $em->getRepository(Center::class)->findBy([], null, 1);
|
||||
|
||||
return [
|
||||
[
|
||||
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
|
||||
'end_date' => new RollingDate(RollingDate::T_TODAY),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function provideGetResultsAndLabels(): iterable
|
||||
{
|
||||
self::bootKernel();
|
||||
$em = self::getContainer()->get(EntityManagerInterface::class);
|
||||
$centers = $em->getRepository(Center::class)->findAll();
|
||||
|
||||
$qb = $em->createQueryBuilder()
|
||||
->select('count(acp.id)')
|
||||
->from(AccompanyingPeriod::class, 'acp');
|
||||
|
||||
$data = [
|
||||
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
|
||||
'end_date' => new RollingDate(RollingDate::T_TODAY),
|
||||
];
|
||||
|
||||
// Yield result with each center ID and null
|
||||
foreach ($centers as $center) {
|
||||
yield [$qb, $data, [(string) $center->getId() => 0]];
|
||||
}
|
||||
|
||||
yield [$qb, $data, ['' => 0]];
|
||||
}
|
||||
|
||||
public static function getQueryBuilders(): iterable
|
||||
{
|
||||
self::bootKernel();
|
||||
$em = self::getContainer()->get(EntityManagerInterface::class);
|
||||
|
||||
return [
|
||||
$em->createQueryBuilder()
|
||||
->select('count(acp.id)')
|
||||
->from(AccompanyingPeriod::class, 'acp'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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\PersonBundle\Tests\Export\Filter\AccompanyingCourseFilters;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||
use Chill\MainBundle\Test\Export\AbstractFilterTest;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\ReferrerMainCenterFilter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
final class ReferrerMainCenterFilterTest extends AbstractFilterTest
|
||||
{
|
||||
private ReferrerMainCenterFilter $filter;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$this->filter = self::getContainer()->get(ReferrerMainCenterFilter::class);
|
||||
}
|
||||
|
||||
public function getFilter(): ReferrerMainCenterFilter
|
||||
{
|
||||
return $this->filter;
|
||||
}
|
||||
|
||||
public static function getFormData(): array
|
||||
{
|
||||
self::bootKernel();
|
||||
$em = self::getContainer()->get(EntityManagerInterface::class);
|
||||
$centers = $em->getRepository(Center::class)->findAll();
|
||||
|
||||
if ([] === $centers) {
|
||||
throw new \RuntimeException('No centers found in database');
|
||||
}
|
||||
|
||||
return [
|
||||
[
|
||||
'centers' => [$centers[0]],
|
||||
'date_calc_since' => new RollingDate(RollingDate::T_TODAY),
|
||||
'date_calc_until' => new RollingDate(RollingDate::T_TODAY),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function getQueryBuilders(): iterable
|
||||
{
|
||||
self::bootKernel();
|
||||
$em = self::getContainer()->get(EntityManagerInterface::class);
|
||||
yield $em->createQueryBuilder()
|
||||
->from(AccompanyingPeriod::class, 'acp')
|
||||
->select('acp.id');
|
||||
}
|
||||
}
|
||||
@@ -104,6 +104,10 @@ services:
|
||||
tags:
|
||||
- { name: chill.export_filter, alias: accompanyingcourse_referrer_filter_between_dates }
|
||||
|
||||
Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\ReferrerMainCenterFilter:
|
||||
tags:
|
||||
- { name: chill.export_filter, alias: accompanyingcourse_referrer_main_center_filter }
|
||||
|
||||
chill.person.export.filter_openbetweendates:
|
||||
class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\OpenBetweenDatesFilter
|
||||
tags:
|
||||
@@ -270,3 +274,7 @@ services:
|
||||
Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\PersonParticipatingAggregator:
|
||||
tags:
|
||||
- { name: chill.export_aggregator, alias: accompanyingcourse_person_part_aggregator }
|
||||
|
||||
Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ReferrerMainCenterAggregator:
|
||||
tags:
|
||||
- { name: chill.export_aggregator, alias: accompanyingcourse_referrer_main_center_aggregator }
|
||||
|
||||
@@ -24,6 +24,14 @@ accompanying_period:
|
||||
number: >-
|
||||
n° {id}
|
||||
|
||||
period_by_user_list:
|
||||
successfully_re_assigned: >-
|
||||
{count, plural,
|
||||
=0 {Aucune assignation de référent effectuée}
|
||||
=1 {Assignation d'un nouveau référent pour un parcours}
|
||||
other {Assignation d'un nouveau référent pour # parcours}
|
||||
}
|
||||
|
||||
person:
|
||||
from_the: depuis le
|
||||
And himself: >-
|
||||
@@ -33,6 +41,13 @@ person:
|
||||
neutral {et lui·elle-même}
|
||||
other {et lui·elle-même}
|
||||
}
|
||||
export:
|
||||
period:
|
||||
filter:
|
||||
by_referrer_main_center:
|
||||
description: >-
|
||||
Filtre les parcours par territoire du référent, entre le {date_since, date, medium} et le {date_until, date, medium}, uniquement {centers}
|
||||
|
||||
|
||||
household:
|
||||
Household: Ménage
|
||||
|
||||
@@ -105,9 +105,18 @@ Administrative status: Situation administrative
|
||||
person:
|
||||
# trans key according to new conventions
|
||||
export:
|
||||
aggregator:
|
||||
by_center:
|
||||
no_center: Sans territoire
|
||||
period:
|
||||
aggregator:
|
||||
by_center:
|
||||
no_center: Sans territoire
|
||||
by_referrer_main_center:
|
||||
title: Grouper les parcours par territoire du référent
|
||||
column_header: Territoire du référent
|
||||
filter:
|
||||
by_referrer_main_center:
|
||||
title: Filtrer les parcours par territoire du référent
|
||||
referrer_since: Référent depuis le
|
||||
referrer_until: Référent avant le
|
||||
Identifiers: Identifiants
|
||||
|
||||
|
||||
@@ -453,8 +462,8 @@ Filtered by entrusted child status: Uniquement les usagers qui sont "enfant conf
|
||||
Filter by nomadic status: Filtrer les usagers "gens du voyage"
|
||||
Filtered by nomadic status: Uniquement les usagers qui sont "gens du voyage"
|
||||
|
||||
"Filter by person's who have a residential address located at another user": Filtrer les usagers qui ont une addresse de résidence chez une autre usager
|
||||
"Filtered by person's who have a residential address located at another user": Uniquement les usagers qui ont une addresse de résidence chez une autre usager
|
||||
"Filter by person's who have a residential address located at another user": Filtrer les usagers qui ont une adresse de résidence chez une autre usager
|
||||
"Filtered by person's who have a residential address located at another user": Uniquement les usagers qui ont une adresse de résidence chez une autre usager
|
||||
|
||||
Filter by person's that are alive or have deceased at a certain date: Filtrer les usagers qui sont décédés ou vivantes à une certaine date
|
||||
Filtered by person's that are alive or have deceased at a certain date: Uniquement les usagers qui sont décédés ou vivantes à une certaine date
|
||||
|
||||
@@ -129,11 +129,11 @@ export:
|
||||
thirdParties: Tiers intervenant
|
||||
|
||||
# exports filters/aggregators
|
||||
Filtered by person\'s who have a residential address located at a thirdparty of type %thirparty_type%: Uniquement les usagers qui ont une addresse de résidence chez un tiers de catégorie %thirdparty_type%
|
||||
Filtered by person\'s who have a residential address located at a thirdparty of type %thirparty_type%: Uniquement les usagers qui ont une adresse de résidence chez un tiers de catégorie %thirdparty_type%
|
||||
is thirdparty: Le demandeur est un tiers
|
||||
|
||||
Filter by person's who have a residential address located at a thirdparty of type: Filtrer les usagers qui ont une addresse de résidence chez un tiers
|
||||
"Filtered by person's who have a residential address located at a thirdparty of type %thirdparty_type% and valid on %date_calc%": "Uniquement les usagers qui ont une addresse de résidence chez un tiers de catégorie %thirdparty_type% et valide sur la date %date_calc%"
|
||||
Filter by person's who have a residential address located at a thirdparty of type: Filtrer les usagers qui ont une adresse de résidence chez un tiers
|
||||
"Filtered by person's who have a residential address located at a thirdparty of type %thirdparty_type% and valid on %date_calc%": "Uniquement les usagers qui ont une adresse de résidence chez un tiers de catégorie %thirdparty_type% et valide sur la date %date_calc%"
|
||||
|
||||
# admin
|
||||
admin:
|
||||
|
||||
Reference in New Issue
Block a user