mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge branch 'master' into 111_exports_suite
This commit is contained in:
commit
35a2d08267
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
.composer/*
|
||||
composer
|
||||
composer.phar
|
||||
composer.lock
|
||||
docs/build/
|
||||
|
13
CHANGELOG.md
13
CHANGELOG.md
@ -11,6 +11,17 @@ and this project adheres to
|
||||
## Unreleased
|
||||
|
||||
<!-- write down unreleased development here -->
|
||||
|
||||
## Test releases
|
||||
|
||||
### 2.0.0-beta2
|
||||
|
||||
* [workflow]: Fixed: the notification is sent when the user is added to the first step.
|
||||
* [budget] Feature: allow to desactivate some charges and resources, adding an `active` key in the configuration
|
||||
* [person] Feature: on Evaluation, allow to configure an URL from the admin
|
||||
|
||||
### 2022-06
|
||||
|
||||
* [workflow]: added pagination to workflow list page
|
||||
* [homepage_widget]: null error on tasks widget fixed
|
||||
* [person-thirdparty]: fix quick-add of names that consist of multiple parts (eg. De Vlieger) within onthefly modal person/thirdparty
|
||||
@ -19,8 +30,6 @@ and this project adheres to
|
||||
* [household]: Reposition and cut button for enfant hors menage have been deleted (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/620)
|
||||
* [admin]: Add crud for composition type in admin (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/611)
|
||||
|
||||
## Test releases
|
||||
|
||||
### 2022-05-30
|
||||
|
||||
* fix creating a new AccompanyingPeriodWorkEvaluationDocument when replacing the document (the workflow was lost)
|
||||
|
@ -104,6 +104,29 @@ The password is always ``password``.
|
||||
|
||||
Now, read `Operations` below.
|
||||
|
||||
Prepare for development
|
||||
***********************
|
||||
|
||||
Add a Gitlab token to ensure that you get always the source code:
|
||||
|
||||
1. generate a gitlab token there: https://gitlab.com/oauth/token
|
||||
2. run this command (in php container, at the app's root): :code:`composer config gitlab-token.gitlab.com <your token>`
|
||||
|
||||
The auth token should appears now in the directory :code:`.composer`:
|
||||
|
||||
.. code-block: bash
|
||||
|
||||
$ cat .composer/auth.json
|
||||
{
|
||||
"gitlab-token": {
|
||||
"gitlab.com": "<your token>"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
See also "how to switch branch and get new dependencies".
|
||||
|
||||
|
||||
Operations
|
||||
**********
|
||||
|
||||
@ -211,6 +234,25 @@ How to run webpack interactively
|
||||
|
||||
Executing :code:`bash docker-node.sh` will open a terminal in a node container, with volumes mounted.
|
||||
|
||||
How to switch the branch for chill-bundles, and get new dependencies
|
||||
====================================================================
|
||||
|
||||
During development, you will switch to new branches for chill-bundles. As long as the dependencies are equals, this does not cause any problem. But sometimes, a new branch introduces a new dependency, and you must download it.
|
||||
|
||||
In order to do that without pain, use those steps:
|
||||
|
||||
0. Ensuire you have a token, set
|
||||
1. at the app's root, update the `composer.json` to your current branch:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"require": {
|
||||
"chill-bundles": "dev-<my-branch>@dev"
|
||||
}
|
||||
|
||||
2. mount into the php container, and run `composer update`
|
||||
|
||||
Build the documentation API
|
||||
===========================
|
||||
|
||||
|
@ -29,44 +29,58 @@ class ConfigRepository
|
||||
$this->charges = $charges;
|
||||
}
|
||||
|
||||
public function getChargesKeys(): array
|
||||
public function getChargesKeys(bool $onlyActive = false): array
|
||||
{
|
||||
return array_map(static function ($element) { return $element['key']; }, $this->charges);
|
||||
return array_map(static function ($element) { return $element['key']; }, $this->getCharges($onlyActive));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array where keys are the resource'key and label the ressource label
|
||||
*/
|
||||
public function getChargesLabels()
|
||||
public function getChargesLabels(bool $onlyActive = false)
|
||||
{
|
||||
$charges = [];
|
||||
|
||||
foreach ($this->charges as $definition) {
|
||||
foreach ($this->getCharges($onlyActive) as $definition) {
|
||||
$charges[$definition['key']] = $this->normalizeLabel($definition['labels']);
|
||||
}
|
||||
|
||||
return $charges;
|
||||
}
|
||||
|
||||
public function getResourcesKeys(): array
|
||||
public function getResourcesKeys(bool $onlyActive = false): array
|
||||
{
|
||||
return array_map(static function ($element) { return $element['key']; }, $this->resources);
|
||||
return array_map(static function ($element) { return $element['key']; }, $this->getResources($onlyActive));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array where keys are the resource'key and label the ressource label
|
||||
*/
|
||||
public function getResourcesLabels()
|
||||
public function getResourcesLabels(bool $onlyActive = false)
|
||||
{
|
||||
$resources = [];
|
||||
|
||||
foreach ($this->resources as $definition) {
|
||||
foreach ($this->getResources($onlyActive) as $definition) {
|
||||
$resources[$definition['key']] = $this->normalizeLabel($definition['labels']);
|
||||
}
|
||||
|
||||
return $resources;
|
||||
}
|
||||
|
||||
private function getCharges(bool $onlyActive = false): array
|
||||
{
|
||||
return $onlyActive ?
|
||||
array_filter($this->charges, function ($el) { return $el['active']; })
|
||||
: $this->charges;
|
||||
}
|
||||
|
||||
private function getResources(bool $onlyActive = false): array
|
||||
{
|
||||
return $onlyActive ?
|
||||
array_filter($this->resources, function ($el) { return $el['active']; })
|
||||
: $this->resources;
|
||||
}
|
||||
|
||||
private function normalizeLabel($labels)
|
||||
{
|
||||
$normalizedLabels = [];
|
||||
|
@ -14,11 +14,6 @@ namespace Chill\BudgetBundle\DependencyInjection;
|
||||
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
|
||||
use Symfony\Component\Config\Definition\ConfigurationInterface;
|
||||
|
||||
/**
|
||||
* This is the class that validates and merges configuration from your app/config files.
|
||||
*
|
||||
* To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/configuration.html}
|
||||
*/
|
||||
class Configuration implements ConfigurationInterface
|
||||
{
|
||||
public function getConfigTreeBuilder()
|
||||
@ -37,6 +32,7 @@ class Configuration implements ConfigurationInterface
|
||||
->info('the key stored in database')
|
||||
->example('salary')
|
||||
->end()
|
||||
->booleanNode('active')->defaultTrue()->end()
|
||||
->arrayNode('labels')->isRequired()->requiresAtLeastOneElement()
|
||||
->arrayPrototype()
|
||||
->children()
|
||||
@ -59,6 +55,7 @@ class Configuration implements ConfigurationInterface
|
||||
->info('the key stored in database')
|
||||
->example('salary')
|
||||
->end()
|
||||
->booleanNode('active')->defaultTrue()->end()
|
||||
->arrayNode('labels')->isRequired()->requiresAtLeastOneElement()
|
||||
->arrayPrototype()
|
||||
->children()
|
||||
|
@ -103,7 +103,7 @@ class ChargeType extends AbstractType
|
||||
private function getTypes()
|
||||
{
|
||||
$charges = $this->configRepository
|
||||
->getChargesLabels();
|
||||
->getChargesLabels(true);
|
||||
|
||||
// rewrite labels to filter in language
|
||||
foreach ($charges as $key => $labels) {
|
||||
|
@ -87,7 +87,7 @@ class ResourceType extends AbstractType
|
||||
private function getTypes()
|
||||
{
|
||||
$resources = $this->configRepository
|
||||
->getResourcesLabels();
|
||||
->getResourcesLabels(true);
|
||||
|
||||
// rewrite labels to filter in language
|
||||
foreach ($resources as $key => $labels) {
|
||||
|
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Command;
|
||||
|
||||
use Chill\MainBundle\Service\Import\AddressReferenceBEFromBestAddress;
|
||||
use Chill\MainBundle\Service\Import\PostalCodeBEFromBestAddress;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class LoadAddressesBEFromBestAddressCommand extends Command
|
||||
{
|
||||
private AddressReferenceBEFromBestAddress $addressImporter;
|
||||
|
||||
private PostalCodeBEFromBestAddress $postalCodeBEFromBestAddressImporter;
|
||||
|
||||
public function __construct(
|
||||
AddressReferenceBEFromBestAddress $addressImporter,
|
||||
PostalCodeBEFromBestAddress $postalCodeBEFromBestAddressImporter
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->addressImporter = $addressImporter;
|
||||
$this->postalCodeBEFromBestAddressImporter = $postalCodeBEFromBestAddressImporter;
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('chill:main:address-ref-from-best-addresses')
|
||||
->addArgument('lang', InputArgument::REQUIRED)
|
||||
->addArgument('list', InputArgument::IS_ARRAY, 'The list to add');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->postalCodeBEFromBestAddressImporter->import();
|
||||
|
||||
$this->addressImporter->import($input->getArgument('lang'), $input->getArgument('list'));
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Command;
|
||||
|
||||
use Chill\MainBundle\Service\Import\AddressReferenceFromBano;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class LoadAddressesFRFromBANOCommand extends Command
|
||||
{
|
||||
private AddressReferenceFromBano $addressReferenceFromBano;
|
||||
|
||||
public function __construct(AddressReferenceFromBano $addressReferenceFromBano)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->addressReferenceFromBano = $addressReferenceFromBano;
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('chill:main:address-ref-from-bano')
|
||||
->addArgument('departementNo', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'a list of departement numbers')
|
||||
->setDescription('Import addresses from bano (see https://bano.openstreetmap.fr');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
foreach ($input->getArgument('departementNo') as $departementNo) {
|
||||
$output->writeln('Import addresses for ' . $departementNo);
|
||||
|
||||
$this->addressReferenceFromBano->import($departementNo);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
42
src/Bundle/ChillMainBundle/Command/LoadPostalCodeFR.php
Normal file
42
src/Bundle/ChillMainBundle/Command/LoadPostalCodeFR.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Command;
|
||||
|
||||
use Chill\MainBundle\Service\Import\PostalCodeFRFromOpenData;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class LoadPostalCodeFR extends Command
|
||||
{
|
||||
private PostalCodeFRFromOpenData $loader;
|
||||
|
||||
public function __construct(PostalCodeFRFromOpenData $loader)
|
||||
{
|
||||
$this->loader = $loader;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setName('chill:main:postal-code:load:FR')
|
||||
->setDescription('Load France\'s postal code from online open data');
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->loader->import();
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -318,19 +318,12 @@ class WorkflowController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
// TODO symfony 5: add those "future" on context ($workflow->apply($entityWorkflow, $transition, $context)
|
||||
$entityWorkflow->futureDestUsers = $transitionForm['future_dest_users']->getData();
|
||||
$entityWorkflow->futureDestEmails = $transitionForm['future_dest_emails']->getData();
|
||||
|
||||
$workflow->apply($entityWorkflow, $transition);
|
||||
|
||||
foreach ($transitionForm['future_dest_users']->getData() as $user) {
|
||||
$entityWorkflow->getCurrentStep()->addDestUser($user);
|
||||
}
|
||||
|
||||
foreach ($transitionForm['future_dest_emails']->getData() as $email) {
|
||||
$entityWorkflow->getCurrentStep()->addDestEmail($email);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]);
|
||||
|
@ -20,6 +20,9 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(name="chill_main_address_reference", indexes={
|
||||
* @ORM\Index(name="address_refid", columns={"refId"})
|
||||
* },
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(name="chill_main_address_reference_unicity", columns={"refId", "source"})
|
||||
* })
|
||||
* @ORM\HasLifecycleCallbacks
|
||||
*/
|
||||
|
@ -12,6 +12,11 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Entity;
|
||||
|
||||
use Chill\MainBundle\Doctrine\Model\Point;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
|
||||
@ -21,6 +26,10 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(
|
||||
* name="chill_main_postal_code",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(name="postal_code_import_unicity", columns={"code", "refpostalcodeid", "postalcodesource"},
|
||||
* options={"where": "refpostalcodeid is not null"})
|
||||
* },
|
||||
* indexes={
|
||||
* @ORM\Index(name="search_name_code", columns={"code", "label"}),
|
||||
* @ORM\Index(name="search_by_reference_code", columns={"code", "refpostalcodeid"})
|
||||
@ -28,8 +37,12 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
||||
*
|
||||
* @ORM\HasLifecycleCallbacks
|
||||
*/
|
||||
class PostalCode
|
||||
class PostalCode implements TrackUpdateInterface, TrackCreationInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
|
||||
use TrackUpdateTrait;
|
||||
|
||||
/**
|
||||
* This is an internal column which is populated by database.
|
||||
*
|
||||
@ -63,6 +76,11 @@ class PostalCode
|
||||
*/
|
||||
private $country;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime_immutable", nullable=true, options={"default": null})
|
||||
*/
|
||||
private ?DateTimeImmutable $deletedAt = null;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
|
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Service\Import;
|
||||
|
||||
use League\Csv\Reader;
|
||||
use League\Csv\Statement;
|
||||
use RuntimeException;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class AddressReferenceBEFromBestAddress
|
||||
{
|
||||
private const RELEASE = 'https://gitea.champs-libres.be/api/v1/repos/Chill-project/belgian-bestaddresses-transform/releases/tags/v1.0.0';
|
||||
|
||||
private AddressReferenceBaseImporter $baseImporter;
|
||||
|
||||
private HttpClientInterface $client;
|
||||
|
||||
public function __construct(HttpClientInterface $client, AddressReferenceBaseImporter $baseImporter)
|
||||
{
|
||||
$this->client = $client;
|
||||
$this->baseImporter = $baseImporter;
|
||||
}
|
||||
|
||||
public function import(string $lang, array $lists): void
|
||||
{
|
||||
foreach ($lists as $list) {
|
||||
$this->importList($lang, $list);
|
||||
}
|
||||
}
|
||||
|
||||
private function getDownloadUrl(string $lang, string $list): string
|
||||
{
|
||||
try {
|
||||
$release = $this->client->request('GET', self::RELEASE)
|
||||
->toArray();
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
throw new RuntimeException('could not get the release definition', 0, $e);
|
||||
}
|
||||
|
||||
$asset = array_filter($release['assets'], static function (array $item) use ($lang, $list) {
|
||||
return 'addresses-' . $list . '.' . $lang . '.csv.gz' === $item['name'];
|
||||
});
|
||||
|
||||
return array_values($asset)[0]['browser_download_url'];
|
||||
}
|
||||
|
||||
private function importList(string $lang, string $list): void
|
||||
{
|
||||
$downloadUrl = $this->getDownloadUrl($lang, $list);
|
||||
|
||||
$response = $this->client->request('GET', $downloadUrl);
|
||||
|
||||
if (200 !== $response->getStatusCode()) {
|
||||
throw new Exception('Could not download CSV: ' . $response->getStatusCode());
|
||||
}
|
||||
|
||||
$tmpname = tempnam(sys_get_temp_dir(), 'php-add-' . $list . $lang);
|
||||
$file = fopen($tmpname, 'r+b');
|
||||
|
||||
foreach ($this->client->stream($response) as $chunk) {
|
||||
fwrite($file, $chunk->getContent());
|
||||
}
|
||||
|
||||
fclose($file);
|
||||
|
||||
$uncompressedStream = gzopen($tmpname, 'r');
|
||||
|
||||
$csv = Reader::createFromStream($uncompressedStream);
|
||||
$csv->setDelimiter(',');
|
||||
$csv->setHeaderOffset(0);
|
||||
|
||||
$stmt = Statement::create()
|
||||
->process($csv);
|
||||
|
||||
foreach ($stmt as $record) {
|
||||
$this->baseImporter->importAddress(
|
||||
$record['best_id'],
|
||||
$record['municipality_objectid'],
|
||||
$record['postal_info_objectid'],
|
||||
$record['streetname'],
|
||||
$record['housenumber'] . $record['boxnumber'],
|
||||
'bestaddress.' . $list,
|
||||
(float) $record['X'],
|
||||
(float) $record['Y'],
|
||||
3812
|
||||
);
|
||||
}
|
||||
|
||||
$this->baseImporter->finalize();
|
||||
|
||||
gzclose($uncompressedStream);
|
||||
}
|
||||
}
|
@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Service\Import;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Statement;
|
||||
use Exception;
|
||||
use LogicException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use function array_key_exists;
|
||||
use function count;
|
||||
|
||||
final class AddressReferenceBaseImporter
|
||||
{
|
||||
private const INSERT = <<<'SQL'
|
||||
INSERT INTO reference_address_temp
|
||||
(postcode_id, refid, street, streetnumber, municipalitycode, source, point)
|
||||
SELECT
|
||||
cmpc.id, i.refid, i.street, i.streetnumber, i.refpostalcode, i.source,
|
||||
CASE WHEN (i.lon::float != 0.0 AND i.lat::float != 0.0) THEN ST_Transform(ST_setSrid(ST_point(i.lon::float, i.lat::float), i.srid::int), 4326) ELSE NULL END
|
||||
FROM
|
||||
(VALUES
|
||||
{{ values }}
|
||||
) AS i (refid, refpostalcode, postalcode, street, streetnumber, source, lat, lon, srid)
|
||||
JOIN chill_main_postal_code cmpc ON cmpc.refpostalcodeid = i.refpostalcode and cmpc.code = i.postalcode
|
||||
SQL;
|
||||
|
||||
private const LOG_PREFIX = '[AddressReferenceImporter] ';
|
||||
|
||||
private const VALUE = '(?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
|
||||
/**
|
||||
* @var array<int, Statement>
|
||||
*/
|
||||
private array $cachingStatements = [];
|
||||
|
||||
private ?string $currentSource = null;
|
||||
|
||||
private Connection $defaultConnection;
|
||||
|
||||
private bool $isInitialized = false;
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private array $waitingForInsert = [];
|
||||
|
||||
public function __construct(Connection $defaultConnection, LoggerInterface $logger)
|
||||
{
|
||||
$this->defaultConnection = $defaultConnection;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function finalize(): void
|
||||
{
|
||||
$this->doInsertPending();
|
||||
|
||||
$this->updateAddressReferenceTable();
|
||||
|
||||
$this->deleteTemporaryTable();
|
||||
|
||||
$this->currentSource = null;
|
||||
$this->isInitialized = false;
|
||||
}
|
||||
|
||||
public function importAddress(
|
||||
string $refAddress,
|
||||
?string $refPostalCode,
|
||||
string $postalCode,
|
||||
string $street,
|
||||
string $streetNumber,
|
||||
string $source,
|
||||
?float $lat = null,
|
||||
?float $lon = null,
|
||||
?int $srid = null
|
||||
): void {
|
||||
if (!$this->isInitialized) {
|
||||
$this->initialize($source);
|
||||
}
|
||||
|
||||
if ($this->currentSource !== $source) {
|
||||
throw new LogicException('Cannot store addresses from different sources during same import. Execute finalize to commit inserts before changing the source');
|
||||
}
|
||||
|
||||
$this->waitingForInsert[] = [
|
||||
$refAddress,
|
||||
$refPostalCode,
|
||||
$postalCode,
|
||||
$street,
|
||||
$streetNumber,
|
||||
$source,
|
||||
$lat,
|
||||
$lon,
|
||||
$srid,
|
||||
];
|
||||
|
||||
if (100 <= count($this->waitingForInsert)) {
|
||||
$this->doInsertPending();
|
||||
}
|
||||
}
|
||||
|
||||
private function createTemporaryTable(): void
|
||||
{
|
||||
$this->defaultConnection->executeStatement('CREATE TEMPORARY TABLE reference_address_temp (
|
||||
postcode_id INT,
|
||||
refid VARCHAR(255),
|
||||
street VARCHAR(255),
|
||||
streetnumber VARCHAR(255),
|
||||
municipalitycode VARCHAR(255),
|
||||
source VARCHAR(255),
|
||||
point GEOMETRY
|
||||
);
|
||||
');
|
||||
$this->defaultConnection->executeStatement('SET work_mem TO \'50MB\'');
|
||||
}
|
||||
|
||||
private function deleteTemporaryTable(): void
|
||||
{
|
||||
$this->defaultConnection->executeStatement('DROP TABLE IF EXISTS reference_address_temp');
|
||||
}
|
||||
|
||||
private function doInsertPending(): void
|
||||
{
|
||||
if (!array_key_exists($forNumber = count($this->waitingForInsert), $this->cachingStatements)) {
|
||||
$sql = strtr(self::INSERT, [
|
||||
'{{ values }}' => implode(
|
||||
', ',
|
||||
array_fill(0, $forNumber, self::VALUE)
|
||||
),
|
||||
]);
|
||||
|
||||
$this->logger->debug(self::LOG_PREFIX . ' generated sql for insert', [
|
||||
'sql' => $sql,
|
||||
'forNumber' => $forNumber,
|
||||
]);
|
||||
|
||||
$this->cachingStatements[$forNumber] = $this->defaultConnection->prepare($sql);
|
||||
}
|
||||
|
||||
if (0 === $forNumber) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->debug(self::LOG_PREFIX . ' inserting pending addresses', [
|
||||
'number' => $forNumber,
|
||||
'first' => $this->waitingForInsert[0] ?? null,
|
||||
]);
|
||||
|
||||
$statement = $this->cachingStatements[$forNumber];
|
||||
|
||||
try {
|
||||
$affected = $statement->executeStatement(array_merge(...$this->waitingForInsert));
|
||||
|
||||
if ($affected === 0) {
|
||||
throw new \RuntimeException('no row affected');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// in some case, we can add debug code here
|
||||
//dump($this->waitingForInsert);
|
||||
throw $e;
|
||||
} finally {
|
||||
$this->waitingForInsert = [];
|
||||
}
|
||||
}
|
||||
|
||||
private function initialize(string $source): void
|
||||
{
|
||||
$this->currentSource = $source;
|
||||
$this->deleteTemporaryTable();
|
||||
$this->createTemporaryTable();
|
||||
$this->isInitialized = true;
|
||||
}
|
||||
|
||||
private function updateAddressReferenceTable(): void
|
||||
{
|
||||
$this->defaultConnection->executeStatement(
|
||||
'CREATE INDEX idx_ref_add_temp ON reference_address_temp (refid)'
|
||||
);
|
||||
|
||||
//1) Add new addresses
|
||||
$this->logger->info(self::LOG_PREFIX . 'upsert new addresses');
|
||||
$affected = $this->defaultConnection->executeStatement("INSERT INTO chill_main_address_reference
|
||||
(id, postcode_id, refid, street, streetnumber, municipalitycode, source, point, createdat, deletedat, updatedat)
|
||||
SELECT
|
||||
nextval('chill_main_address_reference_id_seq'),
|
||||
postcode_id,
|
||||
refid,
|
||||
street,
|
||||
streetnumber,
|
||||
municipalitycode,
|
||||
source,
|
||||
point,
|
||||
NOW(),
|
||||
null,
|
||||
NOW()
|
||||
FROM reference_address_temp
|
||||
ON CONFLICT (refid, source) DO UPDATE
|
||||
SET postcode_id = excluded.postcode_id, refid = excluded.refid, street = excluded.street, streetnumber = excluded.streetnumber, municipalitycode = excluded.municipalitycode, source = excluded.source, point = excluded.point, updatedat = NOW(), deletedAt = NULL
|
||||
");
|
||||
$this->logger->info(self::LOG_PREFIX . 'addresses upserted', ['upserted' => $affected]);
|
||||
|
||||
//3) Delete addresses
|
||||
$this->logger->info(self::LOG_PREFIX . 'soft delete adresses');
|
||||
$affected = $this->defaultConnection->executeStatement('UPDATE chill_main_address_reference
|
||||
SET deletedat = NOW()
|
||||
WHERE
|
||||
chill_main_address_reference.refid NOT IN (SELECT refid FROM reference_address_temp WHERE source LIKE ?)
|
||||
AND chill_main_address_reference.source LIKE ?
|
||||
', [$this->currentSource, $this->currentSource]);
|
||||
$this->logger->info(self::LOG_PREFIX . 'addresses deleted', ['deleted' => $affected]);
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Service\Import;
|
||||
|
||||
use Exception;
|
||||
use League\Csv\Reader;
|
||||
use League\Csv\Statement;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use UnexpectedValueException;
|
||||
use function is_int;
|
||||
|
||||
class AddressReferenceFromBano
|
||||
{
|
||||
private AddressReferenceBaseImporter $baseImporter;
|
||||
|
||||
private HttpClientInterface $client;
|
||||
|
||||
public function __construct(HttpClientInterface $client, AddressReferenceBaseImporter $baseImporter)
|
||||
{
|
||||
$this->client = $client;
|
||||
$this->baseImporter = $baseImporter;
|
||||
}
|
||||
|
||||
public function import(string $departementNo): void
|
||||
{
|
||||
if (!is_numeric($departementNo) || !is_int((int) $departementNo)) {
|
||||
throw new UnexpectedValueException('Could not parse this department number');
|
||||
}
|
||||
|
||||
$url = "https://bano.openstreetmap.fr/data/bano-{$departementNo}.csv";
|
||||
|
||||
$response = $this->client->request('GET', $url);
|
||||
|
||||
if (200 !== $response->getStatusCode()) {
|
||||
throw new Exception('Could not download CSV: ' . $response->getStatusCode());
|
||||
}
|
||||
|
||||
$file = tmpfile();
|
||||
|
||||
foreach ($this->client->stream($response) as $chunk) {
|
||||
fwrite($file, $chunk->getContent());
|
||||
}
|
||||
|
||||
fseek($file, 0);
|
||||
|
||||
$csv = Reader::createFromStream($file);
|
||||
$csv->setDelimiter(',');
|
||||
$stmt = Statement::create()
|
||||
->process($csv, [
|
||||
'refId',
|
||||
'streetNumber',
|
||||
'street',
|
||||
'postcode',
|
||||
'city',
|
||||
'_o',
|
||||
'lat',
|
||||
'lon',
|
||||
]);
|
||||
|
||||
foreach ($stmt as $record) {
|
||||
$this->baseImporter->importAddress(
|
||||
$record['refId'],
|
||||
substr($record['refId'], 0, 5), // extract insee from reference
|
||||
$record['postcode'],
|
||||
$record['street'],
|
||||
$record['streetNumber'],
|
||||
'BANO.' . $departementNo,
|
||||
(float) $record['lat'],
|
||||
(float) $record['lon'],
|
||||
4326
|
||||
);
|
||||
}
|
||||
|
||||
$this->baseImporter->finalize();
|
||||
|
||||
fclose($file);
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Service\Import;
|
||||
|
||||
use League\Csv\Reader;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class PostalCodeBEFromBestAddress
|
||||
{
|
||||
private const RELEASE = 'https://gitea.champs-libres.be/api/v1/repos/Chill-project/belgian-bestaddresses-transform/releases/tags/v1.0.0';
|
||||
|
||||
private PostalCodeBaseImporter $baseImporter;
|
||||
|
||||
private HttpClientInterface $client;
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
public function __construct(PostalCodeBaseImporter $baseImporter, HttpClientInterface $client, LoggerInterface $logger)
|
||||
{
|
||||
$this->baseImporter = $baseImporter;
|
||||
$this->client = $client;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function import(string $lang = 'fr'): void
|
||||
{
|
||||
$fileDownloadUrl = $this->getFileDownloadUrl($lang);
|
||||
|
||||
$response = $this->client->request('GET', $fileDownloadUrl);
|
||||
|
||||
$tmpname = tempnam(sys_get_temp_dir(), 'postalcodes');
|
||||
$tmpfile = fopen($tmpname, 'r+b');
|
||||
|
||||
if (false === $tmpfile) {
|
||||
throw new RuntimeException('could not create temporary file');
|
||||
}
|
||||
|
||||
foreach ($this->client->stream($response) as $chunk) {
|
||||
fwrite($tmpfile, $chunk->getContent());
|
||||
}
|
||||
|
||||
fclose($tmpfile);
|
||||
|
||||
$uncompressedStream = gzopen($tmpname, 'r');
|
||||
|
||||
$csv = Reader::createFromStream($uncompressedStream);
|
||||
$csv->setDelimiter(',');
|
||||
$csv->setHeaderOffset(0);
|
||||
|
||||
foreach ($csv as $offset => $record) {
|
||||
$this->handleRecord($record);
|
||||
}
|
||||
|
||||
gzclose($uncompressedStream);
|
||||
unlink($tmpname);
|
||||
|
||||
$this->logger->info(__CLASS__ . ' list of postal code downloaded');
|
||||
|
||||
$this->baseImporter->finalize();
|
||||
|
||||
$this->logger->info(__CLASS__ . ' postal code fetched', ['offset' => $offset ?? 0]);
|
||||
}
|
||||
|
||||
private function getFileDownloadUrl(string $lang): string
|
||||
{
|
||||
try {
|
||||
$release = $this->client->request('GET', self::RELEASE)
|
||||
->toArray();
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
throw new RuntimeException('could not get the release definition', 0, $e);
|
||||
}
|
||||
|
||||
$postals = array_filter($release['assets'], static function (array $item) use ($lang) {
|
||||
return 'postals.' . $lang . '.csv.gz' === $item['name'];
|
||||
});
|
||||
|
||||
return array_values($postals)[0]['browser_download_url'];
|
||||
}
|
||||
|
||||
private function handleRecord(array $record): void
|
||||
{
|
||||
$this->baseImporter->importCode(
|
||||
'BE',
|
||||
trim($record['municipality_name']),
|
||||
trim($record['postal_info_objectid']),
|
||||
$record['municipality_objectid'],
|
||||
'bestaddress',
|
||||
$record['Y'],
|
||||
$record['X'],
|
||||
3812
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Service\Import;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Statement;
|
||||
use Exception;
|
||||
use function array_key_exists;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* Optimized way to load postal code into database.
|
||||
*/
|
||||
class PostalCodeBaseImporter
|
||||
{
|
||||
private const QUERY = <<<'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
|
||||
)
|
||||
INSERT INTO chill_main_postal_code (id, country_id, label, code, origin, refpostalcodeid, postalcodeSource, center, createdAt, updatedAt)
|
||||
SELECT
|
||||
nextval('chill_main_postal_code_id_seq'),
|
||||
g.country_id,
|
||||
g.label AS glabel,
|
||||
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,
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM g
|
||||
ON CONFLICT (code, refpostalcodeid, postalcodeSource) WHERE refpostalcodeid IS NOT NULL DO UPDATE SET label = excluded.label, center = excluded.center, updatedAt = NOW()
|
||||
SQL;
|
||||
|
||||
private const VALUE = '(?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
|
||||
/**
|
||||
* @var array<int, Statement>
|
||||
*/
|
||||
private array $cachingStatements = [];
|
||||
|
||||
private Connection $defaultConnection;
|
||||
|
||||
private array $waitingForInsert = [];
|
||||
|
||||
public function __construct(
|
||||
Connection $defaultConnection
|
||||
) {
|
||||
$this->defaultConnection = $defaultConnection;
|
||||
}
|
||||
|
||||
public function finalize(): void
|
||||
{
|
||||
$this->doInsertPending();
|
||||
}
|
||||
|
||||
public function importCode(
|
||||
string $countryCode,
|
||||
string $label,
|
||||
string $code,
|
||||
string $refPostalCodeId,
|
||||
string $refPostalCodeSource,
|
||||
float $centerLat,
|
||||
float $centerLon,
|
||||
int $centerSRID
|
||||
): void {
|
||||
$this->waitingForInsert[] = [
|
||||
$countryCode,
|
||||
$label,
|
||||
$code,
|
||||
$refPostalCodeId,
|
||||
$refPostalCodeSource,
|
||||
$centerLon,
|
||||
$centerLat,
|
||||
$centerSRID,
|
||||
];
|
||||
|
||||
if (100 <= count($this->waitingForInsert)) {
|
||||
$this->doInsertPending();
|
||||
}
|
||||
}
|
||||
|
||||
private function doInsertPending(): void
|
||||
{
|
||||
if (!array_key_exists($forNumber = count($this->waitingForInsert), $this->cachingStatements)) {
|
||||
$sql = strtr(self::QUERY, [
|
||||
'{{ values }}' => implode(
|
||||
', ',
|
||||
array_fill(0, $forNumber, self::VALUE)
|
||||
),
|
||||
]);
|
||||
|
||||
$this->cachingStatements[$forNumber] = $this->defaultConnection->prepare($sql);
|
||||
}
|
||||
|
||||
$statement = $this->cachingStatements[$forNumber];
|
||||
|
||||
try {
|
||||
$statement->executeStatement(array_merge(...$this->waitingForInsert));
|
||||
} catch (Exception $e) {
|
||||
// in some case, we can add debug code here
|
||||
//dump($this->waitingForInsert);
|
||||
throw $e;
|
||||
} finally {
|
||||
$this->waitingForInsert = [];
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Service\Import;
|
||||
|
||||
use League\Csv\Reader;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
* Load French's postal codes from opendata.
|
||||
*
|
||||
* Currently, the source is datanova / la poste:
|
||||
* https://datanova.legroupe.laposte.fr/explore/dataset/laposte_hexasmal/information/
|
||||
*/
|
||||
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 PostalCodeBaseImporter $baseImporter;
|
||||
|
||||
private HttpClientInterface $client;
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
public function __construct(
|
||||
PostalCodeBaseImporter $baseImporter,
|
||||
HttpClientInterface $client,
|
||||
LoggerInterface $logger
|
||||
) {
|
||||
$this->baseImporter = $baseImporter;
|
||||
$this->client = $client;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function import(): void
|
||||
{
|
||||
$response = $this->client->request('GET', self::CSV);
|
||||
|
||||
if (200 !== $response->getStatusCode()) {
|
||||
throw new RuntimeException('could not download CSV');
|
||||
}
|
||||
|
||||
$tmpfile = tmpfile();
|
||||
|
||||
if (false === $tmpfile) {
|
||||
throw new RuntimeException('could not create temporary file');
|
||||
}
|
||||
|
||||
foreach ($this->client->stream($response) as $chunk) {
|
||||
fwrite($tmpfile, $chunk->getContent());
|
||||
}
|
||||
|
||||
fseek($tmpfile, 0);
|
||||
|
||||
$csv = Reader::createFromStream($tmpfile);
|
||||
$csv->setDelimiter(';');
|
||||
$csv->setHeaderOffset(0);
|
||||
|
||||
foreach ($csv as $offset => $record) {
|
||||
$this->handleRecord($record);
|
||||
}
|
||||
|
||||
$this->baseImporter->finalize();
|
||||
fclose($tmpfile);
|
||||
|
||||
$this->logger->info(__CLASS__ . ' postal code fetched', ['offset' => $offset ?? 0]);
|
||||
}
|
||||
|
||||
private function handleRecord(array $record): void
|
||||
{
|
||||
if ('' !== trim($record['coordonnees_gps'])) {
|
||||
[$lat, $lon] = array_map(static fn ($el) => (float) trim($el), explode(',', $record['coordonnees_gps']));
|
||||
} else {
|
||||
$lat = $lon = 0.0;
|
||||
}
|
||||
|
||||
$ref = trim($record['Code_commune_INSEE']);
|
||||
|
||||
if ('987' === substr($ref, 0, 3)) {
|
||||
// some differences in French Polynesia
|
||||
$ref .= '.' . trim($record['Libellé_d_acheminement']);
|
||||
}
|
||||
|
||||
$this->baseImporter->importCode(
|
||||
'FR',
|
||||
trim($record['Libellé_d_acheminement']),
|
||||
trim($record['Code_postal']),
|
||||
$ref,
|
||||
'INSEE',
|
||||
$lat,
|
||||
$lon,
|
||||
4326
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Services\Import;
|
||||
|
||||
use Chill\MainBundle\Entity\PostalCode;
|
||||
use Chill\MainBundle\Repository\AddressReferenceRepository;
|
||||
use Chill\MainBundle\Repository\PostalCodeRepository;
|
||||
use Chill\MainBundle\Service\Import\AddressReferenceBaseImporter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
class AddressReferenceBaseImporterTest extends KernelTestCase
|
||||
{
|
||||
private AddressReferenceBaseImporter $importer;
|
||||
private AddressReferenceRepository $addressReferenceRepository;
|
||||
private EntityManagerInterface $entityManager;
|
||||
private PostalCodeRepository $postalCodeRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
self::bootKernel();
|
||||
|
||||
$this->importer = self::$container->get(AddressReferenceBaseImporter::class);
|
||||
$this->addressReferenceRepository = self::$container->get(AddressReferenceRepository::class);
|
||||
$this->entityManager = self::$container->get(EntityManagerInterface::class);
|
||||
$this->postalCodeRepository = self::$container->get(PostalCodeRepository::class);
|
||||
}
|
||||
|
||||
public function testImportAddress(): void
|
||||
{
|
||||
$postalCode = (new PostalCode())
|
||||
->setRefPostalCodeId($postalCodeId = '1234'.uniqid())
|
||||
->setPostalCodeSource('testing')
|
||||
->setCode('TEST456')
|
||||
->setName('testing');
|
||||
|
||||
$this->entityManager->persist($postalCode);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->importer->importAddress(
|
||||
'0000',
|
||||
$postalCodeId,
|
||||
'TEST456',
|
||||
'Rue test abccc-guessed',
|
||||
'-1',
|
||||
'unit-test',
|
||||
50.0,
|
||||
5.0,
|
||||
4326
|
||||
);
|
||||
|
||||
$this->importer->finalize();
|
||||
|
||||
$addresses = $this->addressReferenceRepository->findByPostalCodePattern(
|
||||
$postalCode,
|
||||
'Rue test abcc guessed');
|
||||
|
||||
$this->assertCount(1, $addresses);
|
||||
$this->assertEquals('Rue test abccc-guessed', $addresses[0]->getStreet());
|
||||
|
||||
$previousAddressId = $addresses[0]->getId();
|
||||
|
||||
$this->entityManager->clear();
|
||||
|
||||
$this->importer->importAddress(
|
||||
'0000',
|
||||
$postalCodeId,
|
||||
'TEST456',
|
||||
'Rue test abccc guessed fixed',
|
||||
'-1',
|
||||
'unit-test',
|
||||
50.0,
|
||||
5.0,
|
||||
4326
|
||||
);
|
||||
|
||||
$this->importer->finalize();
|
||||
|
||||
$addresses = $this->addressReferenceRepository->findByPostalCodePattern(
|
||||
$postalCode,
|
||||
'abcc guessed fixed');
|
||||
|
||||
$this->assertCount('1', $addresses);
|
||||
$this->assertEquals( 'Rue test abccc guessed fixed', $addresses[0]->getStreet());
|
||||
$this->assertEquals($previousAddressId, $addresses[0]->getId());
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Services\Import;
|
||||
|
||||
use Chill\MainBundle\Repository\CountryRepository;
|
||||
use Chill\MainBundle\Repository\PostalCodeRepository;
|
||||
use Chill\MainBundle\Service\Import\PostalCodeBaseImporter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
class PostalCodeBaseImporterTest extends KernelTestCase
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
private PostalCodeBaseImporter $importer;
|
||||
|
||||
private PostalCodeRepository $postalCodeRepository;
|
||||
|
||||
private CountryRepository $countryRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
self::bootKernel();
|
||||
|
||||
$this->entityManager = self::$container->get(EntityManagerInterface::class);
|
||||
$this->importer = self::$container->get(PostalCodeBaseImporter::class);
|
||||
$this->postalCodeRepository = self::$container->get(PostalCodeRepository::class);
|
||||
$this->countryRepository = self::$container->get(CountryRepository::class);
|
||||
}
|
||||
|
||||
public function testImportPostalCode(): void
|
||||
{
|
||||
$this->importer->importCode(
|
||||
'BE',
|
||||
'tested with pattern '. ($uniqid = uniqid()),
|
||||
'12345',
|
||||
$refPostalCodeId = 'test'.uniqid(),
|
||||
'test',
|
||||
50.0,
|
||||
5.0,
|
||||
4326
|
||||
);
|
||||
|
||||
$this->importer->finalize();
|
||||
|
||||
$postalCodes = $this->postalCodeRepository->findByPattern(
|
||||
'with pattern '.$uniqid,
|
||||
$this->countryRepository->findOneBy(['countryCode' => 'BE'])
|
||||
);
|
||||
|
||||
$this->assertCount(1, $postalCodes);
|
||||
$this->assertStringStartsWith('tested with pattern', $postalCodes[0]->getName());
|
||||
|
||||
$previousId = $postalCodes[0]->getId();
|
||||
|
||||
$this->entityManager->clear();
|
||||
|
||||
$this->importer->importCode(
|
||||
'BE',
|
||||
'tested with adapted pattern '. ($uniqid = uniqid()),
|
||||
'12345',
|
||||
$refPostalCodeId,
|
||||
'test',
|
||||
50.0,
|
||||
5.0,
|
||||
4326
|
||||
);
|
||||
|
||||
$this->importer->finalize();
|
||||
|
||||
$postalCodes = $this->postalCodeRepository->findByPattern(
|
||||
'with pattern '.$uniqid,
|
||||
$this->countryRepository->findOneBy(['countryCode' => 'BE'])
|
||||
);
|
||||
|
||||
$this->assertCount(1, $postalCodes);
|
||||
$this->assertStringStartsWith('tested with adapted pattern', $postalCodes[0]->getName());
|
||||
$this->assertEquals($previousId, $postalCodes[0]->getId());
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Tests\Workflow\EventSubscriber;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
||||
use Chill\MainBundle\Workflow\EventSubscriber\NotificationOnTransition;
|
||||
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Call\Call;
|
||||
use Prophecy\Exception\Prediction\FailedPredictionException;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Templating\EngineInterface;
|
||||
use Symfony\Component\Workflow\Event\Event;
|
||||
use Symfony\Component\Workflow\Marking;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
use Symfony\Component\Workflow\Transition;
|
||||
use Symfony\Component\Workflow\Workflow;
|
||||
use Symfony\Component\Workflow\WorkflowInterface;
|
||||
|
||||
class NotificationOnTransitionTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testOnCompleteSendNotification(): void
|
||||
{
|
||||
$dest = new User();
|
||||
$currentUser = new User();
|
||||
$workflowProphecy = $this->prophesize(WorkflowInterface::class);
|
||||
$workflow = $workflowProphecy->reveal();
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$entityWorkflow
|
||||
->setWorkflowName('workflow_name')
|
||||
->setRelatedEntityClass(\stdClass::class)
|
||||
->setRelatedEntityId(1)
|
||||
;
|
||||
// force an id to entityWorkflow:
|
||||
$reflection = new \ReflectionClass($entityWorkflow);
|
||||
$id = $reflection->getProperty('id');
|
||||
$id->setAccessible(true);
|
||||
$id->setValue($entityWorkflow, 1);
|
||||
|
||||
$step = new EntityWorkflowStep();
|
||||
$entityWorkflow->addStep($step);
|
||||
$step->addDestUser($dest)
|
||||
->setCurrentStep('to_state')
|
||||
;
|
||||
|
||||
$em = $this->prophesize(EntityManagerInterface::class);
|
||||
$em->persist(Argument::type(Notification::class))->should(
|
||||
function($args) use ($dest) {
|
||||
/** @var Call[] $args */
|
||||
if (1 !== count($args)) {
|
||||
throw new FailedPredictionException('no notification sent');
|
||||
}
|
||||
|
||||
$notification = $args[0]->getArguments()[0];
|
||||
|
||||
if (!$notification instanceof Notification) {
|
||||
throw new FailedPredictionException('persist is not a notification');
|
||||
}
|
||||
|
||||
if (!$notification->getAddressees()->contains($dest)) {
|
||||
throw new FailedPredictionException('the dest is not notified');
|
||||
}
|
||||
});
|
||||
|
||||
$engine = $this->prophesize(EngineInterface::class);
|
||||
$engine->render(Argument::type('string'), Argument::type('array'))
|
||||
->willReturn('dummy text');
|
||||
|
||||
$extractor = $this->prophesize(MetadataExtractor::class);
|
||||
$extractor->buildArrayPresentationForPlace(Argument::type(EntityWorkflow::class), Argument::any())
|
||||
->willReturn([]);
|
||||
$extractor->buildArrayPresentationForWorkflow(Argument::any())
|
||||
->willReturn([]);
|
||||
|
||||
$registry = $this->prophesize(Registry::class);
|
||||
$registry->get(Argument::type(EntityWorkflow::class), Argument::type('string'))
|
||||
->willReturn($workflow);
|
||||
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->getUser()->willReturn($currentUser);
|
||||
|
||||
$notificationOnTransition = new NotificationOnTransition(
|
||||
$em->reveal(),
|
||||
$engine->reveal(),
|
||||
$extractor->reveal(),
|
||||
$security->reveal(),
|
||||
$registry->reveal()
|
||||
);
|
||||
|
||||
$event = new Event($entityWorkflow, new Marking(), new Transition('dummy_transition', ['from_state'], ['to_state']), $workflow);
|
||||
|
||||
$notificationOnTransition->onCompletedSendNotification($event);
|
||||
}
|
||||
}
|
@ -45,13 +45,33 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac
|
||||
{
|
||||
return [
|
||||
'workflow.transition' => 'onTransition',
|
||||
'workflow.completed' => 'onCompleted',
|
||||
'workflow.completed' => [
|
||||
['markAsFinal', 2048],
|
||||
['addDests', 2048],
|
||||
],
|
||||
'workflow.guard' => [
|
||||
['guardEntityWorkflow', 0],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function addDests(Event $event): void
|
||||
{
|
||||
if (!$event->getSubject() instanceof EntityWorkflow) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var EntityWorkflow $entityWorkflow */
|
||||
$entityWorkflow = $event->getSubject();
|
||||
foreach ($entityWorkflow->futureDestUsers as $user) {
|
||||
$entityWorkflow->getCurrentStep()->addDestUser($user);
|
||||
}
|
||||
|
||||
foreach ($entityWorkflow->futureDestEmails as $email) {
|
||||
$entityWorkflow->getCurrentStep()->addDestEmail($email);
|
||||
}
|
||||
}
|
||||
|
||||
public function guardEntityWorkflow(GuardEvent $event)
|
||||
{
|
||||
if (!$event->getSubject() instanceof EntityWorkflow) {
|
||||
@ -90,7 +110,7 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac
|
||||
}
|
||||
}
|
||||
|
||||
public function onCompleted(Event $event): void
|
||||
public function markAsFinal(Event $event): void
|
||||
{
|
||||
if (!$event->getSubject() instanceof EntityWorkflow) {
|
||||
return;
|
||||
|
@ -52,10 +52,20 @@ class NotificationOnTransition implements EventSubscriberInterface
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
'workflow.completed' => 'onCompletedSendNotification',
|
||||
'workflow.completed' => ['onCompletedSendNotification', 2048],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification to:
|
||||
*
|
||||
* * the dests of the new step;
|
||||
* * the users which subscribed to workflow, on each step, or on final
|
||||
*
|
||||
* **Warning** take care that this method must be executed **after** the dest users are added to
|
||||
* the step (@link{EntityWorkflowStep::addDestUser}). Currently, this is done during
|
||||
* @link{EntityWorkflowTransitionEventSubscriber::addDests}.
|
||||
*/
|
||||
public function onCompletedSendNotification(Event $event): void
|
||||
{
|
||||
if (!$event->getSubject() instanceof EntityWorkflow) {
|
||||
@ -65,23 +75,27 @@ class NotificationOnTransition implements EventSubscriberInterface
|
||||
/** @var EntityWorkflow $entityWorkflow */
|
||||
$entityWorkflow = $event->getSubject();
|
||||
|
||||
$dests = array_merge(
|
||||
/** @var array<string, User> $dests array of unique values, where keys is the object's hash */
|
||||
$dests = [];
|
||||
foreach (array_merge(
|
||||
// the subscriber to each step
|
||||
$entityWorkflow->getSubscriberToStep()->toArray(),
|
||||
// the subscriber to final, only if final
|
||||
$entityWorkflow->isFinal() ? $entityWorkflow->getSubscriberToFinal()->toArray() : [],
|
||||
$entityWorkflow->getCurrentStepChained()->getPrevious()->getDestUser()->toArray()
|
||||
);
|
||||
// the dests for the current step
|
||||
$entityWorkflow->getCurrentStep()->getDestUser()->toArray()
|
||||
) as $dest) {
|
||||
$dests[spl_object_hash($dest)] = $dest;
|
||||
}
|
||||
|
||||
$place = $this->metadataExtractor->buildArrayPresentationForPlace($entityWorkflow);
|
||||
$workflow = $this->metadataExtractor->buildArrayPresentationForWorkflow(
|
||||
$this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName())
|
||||
);
|
||||
|
||||
$visited = [];
|
||||
|
||||
foreach ($dests as $subscriber) {
|
||||
if (
|
||||
$this->security->getUser() === $subscriber
|
||||
|| in_array($subscriber->getId(), $visited, true)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@ -102,8 +116,6 @@ class NotificationOnTransition implements EventSubscriberInterface
|
||||
->setMessage($this->engine->render('@ChillMain/Workflow/workflow_notification_on_transition_completed_content.fr.txt.twig', $context))
|
||||
->addAddressee($subscriber);
|
||||
$this->entityManager->persist($notification);
|
||||
|
||||
$visited[] = $subscriber->getId();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -93,3 +93,7 @@ services:
|
||||
|
||||
Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface: '@Chill\MainBundle\Security\Resolver\CenterResolverDispatcher'
|
||||
|
||||
Chill\MainBundle\Service\Import\:
|
||||
resource: '../Service/Import/'
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
@ -43,3 +43,21 @@ services:
|
||||
$entityManager: '@doctrine.orm.entity_manager'
|
||||
tags:
|
||||
- { name: console.command }
|
||||
|
||||
Chill\MainBundle\Command\LoadAddressesFRFromBANOCommand:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
tags:
|
||||
- { name: console.command }
|
||||
|
||||
Chill\MainBundle\Command\LoadAddressesBEFromBestAddressCommand:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
tags:
|
||||
- { name: console.command }
|
||||
|
||||
Chill\MainBundle\Command\LoadPostalCodeFR:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
tags:
|
||||
- { name: console.command }
|
||||
|
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Main;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20220729205416 extends AbstractMigration
|
||||
{
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX postal_code_import_unicity');
|
||||
$this->addSql('ALTER TABLE chill_main_postal_code DROP deletedAt');
|
||||
$this->addSql('ALTER TABLE chill_main_postal_code DROP updatedAt');
|
||||
$this->addSql('ALTER TABLE chill_main_postal_code DROP createdAt');
|
||||
$this->addSql('ALTER TABLE chill_main_postal_code DROP updatedBy_id');
|
||||
$this->addSql('ALTER TABLE chill_main_postal_code DROP createdBy_id');
|
||||
$this->addSql('ALTER TABLE chill_main_postal_code DROP CONSTRAINT chill_internal_postal_code_import_unicity');
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'postal code: add columns to track creation, update and deletion';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_main_postal_code ADD deletedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_main_postal_code ADD updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_main_postal_code ADD createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_main_postal_code ADD updatedBy_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_main_postal_code ADD createdBy_id INT DEFAULT NULL');
|
||||
$this->addSql('COMMENT ON COLUMN chill_main_postal_code.deletedAt IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN chill_main_postal_code.updatedAt IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN chill_main_postal_code.createdAt IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('ALTER TABLE chill_main_postal_code ADD CONSTRAINT FK_6CA145FA65FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_postal_code ADD CONSTRAINT FK_6CA145FA3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('CREATE INDEX IDX_6CA145FA65FF1AEC ON chill_main_postal_code (updatedBy_id)');
|
||||
$this->addSql('CREATE INDEX IDX_6CA145FA3174800F ON chill_main_postal_code (createdBy_id)');
|
||||
$this->addSql('CREATE UNIQUE INDEX postal_code_import_unicity ON chill_main_postal_code (code, refpostalcodeid, postalcodesource) WHERE refpostalcodeid is not null');
|
||||
//$this->addSql('ALTER TABLE chill_main_postal_code ADD CONSTRAINT chill_internal_postal_code_import_unicity '.
|
||||
// 'EXCLUDE (code WITH =, refpostalcodeid WITH =, postalcodesource WITH =) WHERE (refpostalcodeid IS NOT NULL)');
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Main;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20220730204216 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add an unique constraint on addresses references';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE UNIQUE INDEX chill_main_address_reference_unicity ON chill_main_address_reference (refId, source)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX chill_main_address_reference_unicity');
|
||||
}
|
||||
}
|
@ -49,9 +49,8 @@ class Evaluation
|
||||
/**
|
||||
* @ORM\ManyToMany(
|
||||
* targetEntity=SocialAction::class,
|
||||
* inversedBy="evaluations"
|
||||
* mappedBy="evaluations"
|
||||
* )
|
||||
* @ORM\JoinTable(name="chill_person_social_work_evaluation_action")
|
||||
*/
|
||||
private Collection $socialActions;
|
||||
|
||||
|
@ -16,6 +16,7 @@ use Chill\MainBundle\Form\Type\TranslatableStringFormType;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Chill\PersonBundle\Entity\SocialWork\Evaluation;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\UrlType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
@ -40,6 +41,10 @@ class EvaluationType extends AbstractType
|
||||
->add('title', TranslatableStringFormType::class, [
|
||||
'label' => 'Nom',
|
||||
])
|
||||
->add('url', UrlType::class, [
|
||||
'label' => 'evaluation.url',
|
||||
'required' => false,
|
||||
])
|
||||
->add('delay', DateIntervalType::class, [
|
||||
'label' => 'evaluation.delay',
|
||||
'required' => false,
|
||||
|
@ -180,7 +180,7 @@
|
||||
{% if members|length > 0 %}
|
||||
<div class="flex-table list-household-members">
|
||||
{% for m in members %}
|
||||
{% if m.position.shareHousehold %}
|
||||
{% if m.position is null or m.position.shareHousehold %}
|
||||
{% include '@ChillPerson/Household/_render_member.html.twig' with {
|
||||
'member': m,
|
||||
'customButtons': { 'before': _self.customButtons(m, household) }
|
||||
|
@ -9,12 +9,12 @@ services:
|
||||
|
||||
## FILTERS
|
||||
|
||||
chill.person.export.filter_social_work_type:
|
||||
class: Chill\PersonBundle\Export\Filter\SocialWorkFilters\SocialWorkTypeFilter
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
tags:
|
||||
- { name: chill.export_filter, alias: social_work_type_filter }
|
||||
#chill.person.export.filter_social_work_type:
|
||||
# class: Chill\PersonBundle\Export\Filter\SocialWorkFilters\SocialWorkTypeFilter
|
||||
# autowire: true
|
||||
# autoconfigure: true
|
||||
# tags:
|
||||
# - { name: chill.export_filter, alias: social_work_type_filter }
|
||||
|
||||
chill.person.export.filter_scope:
|
||||
class: Chill\PersonBundle\Export\Filter\SocialWorkFilters\ScopeFilter
|
||||
|
@ -696,6 +696,7 @@ origin:
|
||||
evaluation:
|
||||
delay: Délai
|
||||
notificationDelay: Délai de notification
|
||||
url: Lien internet
|
||||
|
||||
goal:
|
||||
desactivationDate: Date de désactivation
|
||||
|
Loading…
x
Reference in New Issue
Block a user