Merge branch '111_exports_suite' into calendar/finalization

This commit is contained in:
2022-10-05 15:28:37 +02:00
294 changed files with 10155 additions and 1612 deletions

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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;
}
}

View File

@@ -23,6 +23,7 @@ use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -142,10 +143,8 @@ class ExportController extends AbstractController
/**
* Render the list of available exports.
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function indexAction(Request $request)
public function indexAction(): Response
{
$exportManager = $this->exportManager;
@@ -443,6 +442,12 @@ class ExportController extends AbstractController
}
$rawData = unserialize($serialized);
$this->logger->notice('[export] choices for an export unserialized', [
'key' => $key,
'rawData' => json_encode($rawData),
]);
$alias = $rawData['alias'];
$formCenters = $this->createCreateFormExport($alias, 'generate_centers');

View File

@@ -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()]);

View File

@@ -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
*/

View File

@@ -14,15 +14,17 @@ namespace Chill\MainBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="chill_main_geographical_unit")
* @ORM\Entity
* @ORM\Table(name="chill_main_geographical_unit", uniqueConstraints={
* @ORM\UniqueConstraint(name="geographical_unit_refid", columns={"layer_id", "unitRefId"})
* })
* @ORM\Entity(readOnly=true)
*/
class GeographicalUnit
{
/**
* @ORM\Column(type="text", nullable=true)
*/
private $geom;
private string $geom;
/**
* @ORM\Id
@@ -32,23 +34,28 @@ class GeographicalUnit
private ?int $id = null;
/**
* @ORM\Column(type="string", length=255, nullable=true)
* @ORM\ManyToOne(targetEntity=GeographicalUnitLayer::class, inversedBy="units")
*/
private $layerName;
private ?GeographicalUnitLayer $layer;
/**
* @ORM\Column(type="string", length=255, nullable=true)
* @ORM\Column(type="text", nullable=false, options={"default": ""})
*/
private $unitName;
private string $unitName;
/**
* @ORM\Column(type="text", nullable=false, options={"default": ""})
*/
private string $unitRefId;
public function getId(): ?int
{
return $this->id;
}
public function getLayerName(): ?string
public function getLayer(): ?GeographicalUnitLayer
{
return $this->layerName;
return $this->layer;
}
public function getUnitName(): ?string
@@ -56,9 +63,9 @@ class GeographicalUnit
return $this->unitName;
}
public function setLayerName(?string $layerName): self
public function setLayer(?GeographicalUnitLayer $layer): GeographicalUnit
{
$this->layerName = $layerName;
$this->layer = $layer;
return $this;
}
@@ -69,4 +76,18 @@ class GeographicalUnit
return $this;
}
public function setUnitRefId(string $unitRefId): GeographicalUnit
{
$this->unitRefId = $unitRefId;
return $this;
}
protected function setId(int $id): self
{
$this->id = $id;
return $this;
}
}

View File

@@ -0,0 +1,79 @@
<?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\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="chill_main_geographical_unit_layer", uniqueConstraints={
* @ORM\UniqueConstraint(name="geographical_unit_layer_refid", columns={"refId"})
* })
* @ORM\Entity
*/
class GeographicalUnitLayer
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private ?int $id = null;
/**
* @ORM\Column(type="json", nullable=false, options={"default": "[]"})
*/
private array $name = [];
/**
* @ORM\Column(type="text", nullable=false, options={"default": ""})
*/
private string $refId = '';
/**
* @ORM\OneToMany(targetEntity=GeographicalUnit::class, mappedBy="layer")
*/
private Collection $units;
public function __construct()
{
$this->units = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): array
{
return $this->name;
}
public function getRefId(): string
{
return $this->refId;
}
public function getUnits(): Collection
{
return $this->units;
}
public function setName(array $name): GeographicalUnitLayer
{
$this->name = $name;
return $this;
}
}

View File

@@ -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
*

View File

@@ -13,7 +13,7 @@ namespace Chill\MainBundle\Export;
use Chill\MainBundle\Form\Type\Export\ExportType;
use Chill\MainBundle\Form\Type\Export\PickCenterType;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Generator;
@@ -42,52 +42,38 @@ class ExportManager
/**
* The collected aggregators, injected by DI.
*
* @var AggregatorInterface[]
* @var array|AggregatorInterface[]
*/
private $aggregators = [];
private array $aggregators = [];
/**
* @var AuthorizationChecker
*/
private $authorizationChecker;
private AuthorizationCheckerInterface $authorizationChecker;
/**
* @var AuthorizationHelper
*/
private $authorizationHelper;
private AuthorizationHelperInterface $authorizationHelper;
/**
* @var EntityManagerInterface
*/
private $em;
private EntityManagerInterface $em;
/**
* Collected Exports, injected by DI.
*
* @var ExportInterface[]
* @var array|ExportInterface[]
*/
private $exports = [];
private array $exports = [];
/**
* The collected filters, injected by DI.
*
* @var FilterInterface[]
* @var array|FilterInterface[]
*/
private $filters = [];
private array $filters = [];
/**
* Collected Formatters, injected by DI.
*
* @var FormatterInterface[]
* @var array|FormatterInterface[]
*/
private $formatters = [];
private array $formatters = [];
/**
* a logger.
*
* @var LoggerInterface
*/
private $logger;
private LoggerInterface $logger;
/**
* @var \Symfony\Component\Security\Core\User\UserInterface
@@ -98,7 +84,7 @@ class ExportManager
LoggerInterface $logger,
EntityManagerInterface $em,
AuthorizationCheckerInterface $authorizationChecker,
AuthorizationHelper $authorizationHelper,
AuthorizationHelperInterface $authorizationHelper,
TokenStorageInterface $tokenStorage
) {
$this->logger = $logger;
@@ -277,8 +263,8 @@ class ExportManager
//handle aggregators
$this->handleAggregators($export, $query, $data[ExportType::AGGREGATOR_KEY], $centers);
$this->logger->debug('current query is ' . $query->getDQL(), [
'class' => self::class, 'function' => __FUNCTION__,
$this->logger->notice('[export] will execute this qb in export', [
'dql' => $query->getDQL(),
]);
} else {
throw new UnexpectedValueException('The method `intiateQuery` should return '
@@ -547,19 +533,16 @@ class ExportManager
. 'an ExportInterface.');
}
if (null === $centers) {
$centers = $this->authorizationHelper->getReachableCenters(
if (null === $centers || [] !== $centers) {
// we want to try if at least one center is reachabler
return [] !== $this->authorizationHelper->getReachableCenters(
$this->user,
$role
);
}
if (count($centers) === 0) {
return false;
}
foreach ($centers as $center) {
if ($this->authorizationChecker->isGranted($role, $center) === false) {
if (false === $this->authorizationChecker->isGranted($role, $center)) {
//debugging
$this->logger->debug('user has no access to element', [
'method' => __METHOD__,
@@ -568,10 +551,6 @@ class ExportManager
'role' => $role,
]);
///// Bypasse les autorisations qui empêche d'afficher les nouveaux exports
return true;
///// TODO supprimer le return true
return false;
}
}

View File

@@ -230,7 +230,8 @@ class SpreadSheetFormatter implements FormatterInterface
$worksheet->fromArray(
$sortedResults,
null,
'A' . $line
'A' . $line,
true
);
return $line + count($sortedResults) + 1;
@@ -495,8 +496,13 @@ class SpreadSheetFormatter implements FormatterInterface
// 3. iterate on `keysExportElementAssociation` to store the callable
// in cache
foreach ($keysExportElementAssociation as $key => [$element, $data]) {
$this->cacheDisplayableResult[$key] =
$element->getLabels($key, array_unique($allValues[$key]), $data);
// handle the case when there is not results lines (query is empty)
if ([] === $allValues) {
$this->cacheDisplayableResult[$key] = $element->getLabels($key, ['_header'], $data);
} else {
$this->cacheDisplayableResult[$key] =
$element->getLabels($key, array_unique($allValues[$key]), $data);
}
}
// the cache is initialized !

View File

@@ -14,8 +14,7 @@ namespace Chill\MainBundle\Form\Type\Export;
use Chill\MainBundle\Center\GroupingCenterInterface;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Export\ExportManager;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Doctrine\ORM\EntityRepository;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
@@ -24,6 +23,7 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use function array_intersect;
use function array_key_exists;
use function array_merge;
@@ -38,30 +38,21 @@ class PickCenterType extends AbstractType
{
public const CENTERS_IDENTIFIERS = 'c';
/**
* @var AuthorizationHelper
*/
protected $authorizationHelper;
protected AuthorizationHelperInterface $authorizationHelper;
protected ExportManager $exportManager;
/**
* @var ExportManager
* @var array|GroupingCenterInterface[]
*/
protected $exportManager;
protected array $groupingCenters = [];
/**
* @var GroupingCenterInterface[]
*/
protected $groupingCenters = [];
/**
* @var \Symfony\Component\Security\Core\User\UserInterface
*/
protected $user;
protected UserInterface $user;
public function __construct(
TokenStorageInterface $tokenStorage,
ExportManager $exportManager,
AuthorizationHelper $authorizationHelper
AuthorizationHelperInterface $authorizationHelper
) {
$this->exportManager = $exportManager;
$this->user = $tokenStorage->getToken()->getUser();
@@ -78,22 +69,12 @@ class PickCenterType extends AbstractType
$export = $this->exportManager->getExport($options['export_alias']);
$centers = $this->authorizationHelper->getReachableCenters(
$this->user,
(string) $export->requiredRole()
$export->requiredRole()
);
$builder->add(self::CENTERS_IDENTIFIERS, EntityType::class, [
'class' => Center::class,
'query_builder' => static function (EntityRepository $er) use ($centers) {
$qb = $er->createQueryBuilder('c');
$ids = array_map(
static function (Center $el) {
return $el->getId();
},
$centers
);
return $qb->where($qb->expr()->in('c.id', $ids));
},
'choices' => $centers,
'multiple' => true,
'expanded' => true,
'choice_label' => static function (Center $c) {

View File

@@ -14,9 +14,8 @@ namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Center;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
final class CenterRepository implements ObjectRepository
final class CenterRepository implements CenterRepositoryInterface
{
private EntityRepository $repository;
@@ -30,6 +29,11 @@ final class CenterRepository implements ObjectRepository
return $this->repository->find($id, $lockMode, $lockVersion);
}
public function findActive(): array
{
return $this->findAll();
}
/**
* @return Center[]
*/

View File

@@ -0,0 +1,27 @@
<?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\Repository;
use Chill\MainBundle\Entity\Center;
use Doctrine\Persistence\ObjectRepository;
interface CenterRepositoryInterface extends ObjectRepository
{
/**
* Return all active centers.
*
* Note: this is a teaser: active will comes later on center entity
*
* @return Center[]
*/
public function findActive(): array;
}

View File

@@ -0,0 +1,66 @@
<?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\Repository;
use Chill\MainBundle\Entity\GeographicalUnitLayer;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
final class GeographicalUnitLayerLayerRepository implements GeographicalUnitLayerRepositoryInterface
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $em)
{
$this->repository = $em->getRepository($this->getClassName());
}
public function find($id): ?GeographicalUnitLayer
{
return $this->repository->find($id);
}
/**
* @return array|GeographicalUnitLayer[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
public function findAllHavingUnits(): array
{
$qb = $this->repository->createQueryBuilder('l');
return $qb->where($qb->expr()->gt('SIZE(l.units)', 0))
->getQuery()
->getResult();
}
/**
* @return array|GeographicalUnitLayer[]
*/
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria): ?GeographicalUnitLayer
{
return $this->repository->findOneBy($criteria);
}
public function getClassName(): string
{
return GeographicalUnitLayer::class;
}
}

View File

@@ -0,0 +1,23 @@
<?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\Repository;
use Chill\MainBundle\Entity\GeographicalUnitLayer;
use Doctrine\Persistence\ObjectRepository;
interface GeographicalUnitLayerRepositoryInterface extends ObjectRepository
{
/**
* @return array|GeographicalUnitLayer[]
*/
public function findAllHavingUnits(): array;
}

View File

@@ -0,0 +1,65 @@
<?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\Repository;
use Chill\MainBundle\Entity\GeographicalUnit;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
class GeographicalUnitRepository implements GeographicalUnitRepositoryInterface
{
private EntityManagerInterface $em;
private EntityRepository $repository;
public function __construct(EntityManagerInterface $em)
{
$this->repository = $em->getRepository($this->getClassName());
$this->em = $em;
}
public function find($id): ?GeographicalUnit
{
return $this->repository->find($id);
}
/**
* Will return only partial object, where the @see{GeographicalUnit::geom} property is not loaded.
*
* @return array|GeographicalUnit[]
*/
public function findAll(): array
{
return $this->repository
->createQueryBuilder('gu')
->select('PARTIAL gu.{id,unitName,unitRefId,layer}')
->addOrderBy('IDENTITY(gu.layer)')
->addOrderBy(('gu.unitName'))
->getQuery()
->getResult();
}
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): ?GeographicalUnit
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria): ?GeographicalUnit
{
return $this->repository->findOneBy($criteria);
}
public function getClassName(): string
{
return GeographicalUnit::class;
}
}

View File

@@ -0,0 +1,18 @@
<?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\Repository;
use Doctrine\Persistence\ObjectRepository;
interface GeographicalUnitRepositoryInterface extends ObjectRepository
{
}

View File

@@ -14,9 +14,9 @@ namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Scope;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
use Doctrine\ORM\QueryBuilder;
final class ScopeRepository implements ObjectRepository
final class ScopeRepository implements ScopeRepositoryInterface
{
private EntityRepository $repository;
@@ -25,7 +25,7 @@ final class ScopeRepository implements ObjectRepository
$this->repository = $entityManager->getRepository(Scope::class);
}
public function createQueryBuilder($alias, $indexBy = null)
public function createQueryBuilder($alias, $indexBy = null): QueryBuilder
{
return $this->repository->createQueryBuilder($alias, $indexBy);
}
@@ -59,7 +59,7 @@ final class ScopeRepository implements ObjectRepository
return $this->repository->findOneBy($criteria, $orderBy);
}
public function getClassName()
public function getClassName(): string
{
return Scope::class;
}

View File

@@ -0,0 +1,40 @@
<?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\Repository;
use Chill\MainBundle\Entity\Scope;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
interface ScopeRepositoryInterface extends ObjectRepository
{
public function createQueryBuilder($alias, $indexBy = null): QueryBuilder;
public function find($id, $lockMode = null, $lockVersion = null): ?Scope;
/**
* @return Scope[]
*/
public function findAll(): array;
/**
* @param null|mixed $limit
* @param null|mixed $offset
*
* @return Scope[]
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array;
public function findOneBy(array $criteria, ?array $orderBy = null): ?Scope;
public function getClassName(): string;
}

View File

@@ -517,3 +517,9 @@ div.popover {
div.v-toast {
z-index: 10000!important;
}
div.grouped {
padding: 1em;
border: 1px solid black;
margin-bottom: 2em;
}

View File

@@ -0,0 +1,6 @@
<h6>
<a href="{{ path('chill_main_export_index') }}" title="{{ 'Back to the list'|trans }}">
<i class="fa fa-folder-open-o fa-fw"></i>
</a>
{{ export_group|trans }}
</h6>

View File

@@ -36,10 +36,7 @@ window.addEventListener("DOMContentLoaded", function(e) {
{% block content %}
<div class="col-md-10">
<h6>
<i class="fa fa-folder-open-o fa-fw"></i>
{{ export_group|trans }}
</h6>
{{ include('@ChillMain/Export/_breadcrumb.html.twig') }}
<h1>{{ export.title|trans }}</h1>
<h2>{{ "Download export"|trans }}</h2>

View File

@@ -22,15 +22,15 @@
{% block js %}
{{ encore_entry_script_tags('page_export') }}
{% if export_alias == 'count_social_work_actions' %}
{{ encore_entry_script_tags('vue_export_action_goal_result') }}
{% endif %}
{% endblock js %}
{% block content %}
<div class="col-md-10">
<h6>
<i class="fa fa-folder-open-o fa-fw"></i>
{{ export_group|trans }}
</h6>
{{ include('@ChillMain/Export/_breadcrumb.html.twig') }}
<h1>{{ export.title|trans }}</h1>

View File

@@ -22,11 +22,8 @@
{% block content %}
<div class="col-md-10">
<h6>
<i class="fa fa-folder-open-o fa-fw"></i>
{{ export_group|trans }}
</h6>
{{ include('@ChillMain/Export/_breadcrumb.html.twig') }}
<h1>{{ export.title|trans }}</h1>

View File

@@ -23,10 +23,7 @@
{% block content %}
<div class="col-md-10">
<h6>
<i class="fa fa-folder-open-o fa-fw"></i>
{{ export_group|trans }}
</h6>
{{ include('@ChillMain/Export/_breadcrumb.html.twig') }}
<h1>{{ export.title|trans }}</h1>
@@ -36,19 +33,21 @@
<section class="formatter mb-4">
<h2>{{ 'Formatter'| trans }}</h2>
<div>
{% if form.children.formatter.children|length == 0 %}
<p>
<span class="chill-no-data-statement">{{ "No options availables. Your report is fully configured."|trans }}</span>
</p>
{{ form_widget(form.children.formatter) }}
{% else %}
{# we always have to render children, to mark as rendered #}
{% for input in form.children.formatter.children %}
{{ form_row(input) }}
{% endfor %}
<div class="container py-4">
{# we always have to render children, to mark as rendered #}
{% for input in form.children.formatter.children %}
<div class="row">
{{ form_row(input) }}
</div>
{% endfor %}
</div>
{% endif %}
</div>
</section>
<div class="mb-4">

View File

@@ -11,7 +11,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Security\Authorization;
use Chill\MainBundle\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
@@ -19,24 +18,23 @@ class ChillExportVoter extends Voter
{
public const EXPORT = 'chill_export';
protected AuthorizationHelperInterface $authorizationHelper;
private VoterHelperInterface $helper;
public function __construct(AuthorizationHelperInterface $authorizationHelper)
public function __construct(VoterHelperFactoryInterface $voterHelperFactory)
{
$this->authorizationHelper = $authorizationHelper;
$this->helper = $voterHelperFactory
->generate(self::class)
->addCheckFor(null, [self::EXPORT])
->build();
}
protected function supports($attribute, $subject): bool
{
return self::EXPORT === $attribute;
return $this->helper->supports($attribute, $subject);
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{
if (!$token->getUser() instanceof User) {
return false;
}
return [] !== $this->authorizationHelper->getReachableCenters($token->getUser(), $attribute);
return $this->helper->voteOnAttribute($attribute, $subject, $token);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,221 @@
<?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 RuntimeException;
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 (0 === $affected) {
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]);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,236 @@
<?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 Doctrine\DBAL\Types\Types;
use Exception;
use Psr\Log\LoggerInterface;
use RuntimeException;
use function array_key_exists;
use function count;
final class GeographicalUnitBaseImporter
{
private const INSERT = <<<'SQL'
INSERT INTO geographical_unit_temp
(layerKey, layerName, unitName, unitKey, geom)
SELECT
i.layerKey, i.layerName, i.unitName, i.unitKey,
ST_Transform(ST_setSrid(ST_GeomFromText(i.wkt), i.srid), 4326)
FROM
(VALUES
{{ values }}
) AS i (layerKey, layerName, unitName, unitKey, wkt, srid)
SQL;
private const LOG_PREFIX = '[GeographicalUnitBAseImporter] ';
private const VALUE = '(?, ?::jsonb, ?, ?, ?, ?::int)';
/**
* @var array<int, Statement>
*/
private array $cachingStatements = [];
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->prepareForFinalize();
$this->updateGeographicalUnitTable();
$this->deleteTemporaryTable();
$this->isInitialized = false;
}
public function importUnit(
string $layerKey,
array $layerName,
string $unitName,
string $unitKey,
string $geomAsWKT,
?int $srid = null
): void {
$this->initialize();
$this->waitingForInsert[] = [
'layerKey' => $layerKey,
'layerName' => $layerName,
'unitName' => $unitName,
'unitKey' => $unitKey,
'geomAsWKT' => $geomAsWKT,
'srid' => $srid,
];
if (100 <= count($this->waitingForInsert)) {
$this->doInsertPending();
}
}
private function createTemporaryTable(): void
{
$this->defaultConnection->executeStatement("CREATE TEMPORARY TABLE geographical_unit_temp (
layerKey TEXT DEFAULT '' NOT NULL,
layerName JSONB DEFAULT '[]'::jsonb NOT NULL,
unitName TEXT default '' NOT NULL,
unitKey TEXT default '' NOT NULL,
geom GEOMETRY(MULTIPOLYGON, 4326)
)");
$this->defaultConnection->executeStatement('SET work_mem TO \'50MB\'');
}
private function deleteTemporaryTable(): void
{
$this->defaultConnection->executeStatement('DROP TABLE IF EXISTS geographical_unit_temp');
}
private function doInsertPending(): void
{
$forNumber = count($this->waitingForInsert);
if (0 === $forNumber) {
return;
}
if (!array_key_exists($forNumber, $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);
}
$statement = $this->cachingStatements[$forNumber];
try {
$i = 0;
foreach ($this->waitingForInsert as $insert) {
$statement->bindValue(++$i, $insert['layerKey'], Types::STRING);
$statement->bindValue(++$i, $insert['layerName'], Types::JSON);
$statement->bindValue(++$i, $insert['unitName'], Types::STRING);
$statement->bindValue(++$i, $insert['unitKey'], Types::STRING);
$statement->bindValue(++$i, $insert['geomAsWKT'], Types::STRING);
$statement->bindValue(++$i, $insert['srid'], Types::INTEGER);
}
$affected = $statement->executeStatement();
if (0 === $affected) {
throw new RuntimeException('no row affected');
}
} catch (Exception $e) {
throw $e;
} finally {
$this->waitingForInsert = [];
}
}
private function initialize(): void
{
if ($this->isInitialized) {
return;
}
$this->deleteTemporaryTable();
$this->createTemporaryTable();
$this->isInitialized = true;
}
private function prepareForFinalize(): void
{
$this->defaultConnection->executeStatement(
'CREATE INDEX idx_ref_add_temp ON geographical_unit_temp (unitKey)'
);
}
private function updateGeographicalUnitTable(): void
{
$this->defaultConnection->transactional(
function () {
// 0) create new layers
$this->defaultConnection->executeStatement(
"
WITH unique_layers AS (
SELECT DISTINCT layerKey, layerName FROM geographical_unit_temp
)
INSERT INTO chill_main_geographical_unit_layer (id, name, refid)
SELECT
nextval('chill_main_geographical_unit_layer_id_seq'),
layerName,
layerKey
FROM unique_layers
ON CONFLICT (refid)
DO UPDATE SET name=EXCLUDED.name
"
);
//1) Add new units
$this->logger->info(self::LOG_PREFIX . 'upsert new units');
$affected = $this->defaultConnection->executeStatement("INSERT INTO chill_main_geographical_unit
(id, geom, unitname, layer_id, unitrefid)
SELECT
nextval('chill_main_geographical_unit_id_seq'),
geom,
unitName,
layer.id,
unitKey
FROM geographical_unit_temp JOIN chill_main_geographical_unit_layer AS layer ON layer.refid = layerKey
ON CONFLICT (layer_id, unitrefid)
DO UPDATE
SET geom = EXCLUDED.geom, unitname = EXCLUDED.unitname
");
$this->logger->info(self::LOG_PREFIX . 'units upserted', ['upserted' => $affected]);
//3) Delete units
$this->logger->info(self::LOG_PREFIX . 'soft delete adresses');
$affected = $this->defaultConnection->executeStatement('WITH to_delete AS (
SELECT cmgu.id
FROM chill_main_geographical_unit AS cmgu
JOIN chill_main_geographical_unit_layer AS cmgul ON cmgul.id = cmgu.layer_id
JOIN geographical_unit_temp AS gut ON cmgul.refid = gut.layerKey AND cmgu.unitrefid = gut.unitKey
)
DELETE FROM chill_main_geographical_unit
WHERE id NOT IN (SELECT id FROM to_delete)
');
$this->logger->info(self::LOG_PREFIX . 'addresses deleted', ['deleted' => $affected]);
}
);
}
}

View 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
);
}
}

View File

@@ -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 = [];
}
}
}

View 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\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
);
}
}

View File

@@ -18,6 +18,8 @@ namespace Chill\MainBundle\Test;
* and use tearDownTrait after usage.
*
* @codeCoverageIgnore
*
* @deprecated use @class{Prophecy\PhpUnit\ProphecyTrait} instead
*/
trait ProphecyTrait
{

View File

@@ -0,0 +1,106 @@
<?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 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;
/**
* @internal
* @coversNothing
*/
final class AddressReferenceBaseImporterTest extends KernelTestCase
{
private AddressReferenceRepository $addressReferenceRepository;
private EntityManagerInterface $entityManager;
private AddressReferenceBaseImporter $importer;
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());
}
}

View File

@@ -0,0 +1,100 @@
<?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 Services\Import;
use Chill\MainBundle\Service\Import\GeographicalUnitBaseImporter;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\NullLogger;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @internal
* @coversNothing
*/
final class GeographicalUnitBaseImporterTest extends KernelTestCase
{
private Connection $connection;
private EntityManagerInterface $entityManager;
protected function setUp(): void
{
parent::setUp();
self::bootKernel();
$this->connection = self::$container->get(Connection::class);
$this->entityManager = self::$container->get(EntityManagerInterface::class);
}
public function testImportUnit(): void
{
$importer = new GeographicalUnitBaseImporter(
$this->connection,
new NullLogger()
);
$importer->importUnit(
'test',
['fr' => 'Test Layer'],
'Layer one',
'layer_one',
'MULTIPOLYGON (((30 20, 45 40, 10 40, 30 20)),((15 5, 40 10, 10 20, 5 10, 15 5)))',
3812
);
$importer->finalize();
$unit = $this->connection->executeQuery('
SELECT unitname, unitrefid, cmgul.refid AS layerrefid, cmgul.name AS layername, ST_AsText(ST_snapToGrid(ST_Transform(u.geom, 3812), 1)) AS geom
FROM chill_main_geographical_unit u JOIN chill_main_geographical_unit_layer cmgul on u.layer_id = cmgul.id
WHERE u.unitrefid = ?', ['layer_one']);
$results = $unit->fetchAssociative();
$this->assertEquals($results['unitrefid'], 'layer_one');
$this->assertEquals($results['unitname'], 'Layer one');
$this->assertEquals(json_decode($results['layername'], true), ['fr' => 'Test Layer']);
$this->assertEquals($results['layerrefid'], 'test');
$this->assertEquals($results['geom'], 'MULTIPOLYGON(((30 20,45 40,10 40,30 20)),((15 5,40 10,10 20,5 10,15 5)))');
$importer = new GeographicalUnitBaseImporter(
$this->connection,
new NullLogger()
);
$importer->importUnit(
'test',
['fr' => 'Test Layer fixed'],
'Layer one fixed',
'layer_one',
'MULTIPOLYGON (((130 120, 45 40, 10 40, 130 120)),((0 0, 15 5, 40 10, 10 20, 0 0)))',
3812
);
$importer->finalize();
$unit = $this->connection->executeQuery('
SELECT unitname, unitrefid, cmgul.refid AS layerrefid, cmgul.name AS layername, ST_AsText(ST_snapToGrid(ST_Transform(u.geom, 3812), 1)) AS geom
FROM chill_main_geographical_unit u JOIN chill_main_geographical_unit_layer cmgul on u.layer_id = cmgul.id
WHERE u.unitrefid = ?', ['layer_one']);
$results = $unit->fetchAssociative();
$this->assertEquals($results['unitrefid'], 'layer_one');
$this->assertEquals($results['unitname'], 'Layer one fixed');
$this->assertEquals(json_decode($results['layername'], true), ['fr' => 'Test Layer fixed']);
$this->assertEquals($results['layerrefid'], 'test');
$this->assertEquals($results['geom'], 'MULTIPOLYGON(((130 120,45 40,10 40,130 120)),((0 0,15 5,40 10,10 20,0 0)))');
}
}

View File

@@ -0,0 +1,95 @@
<?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 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;
/**
* @internal
* @coversNothing
*/
final class PostalCodeBaseImporterTest extends KernelTestCase
{
private CountryRepository $countryRepository;
private EntityManagerInterface $entityManager;
private PostalCodeBaseImporter $importer;
private PostalCodeRepository $postalCodeRepository;
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());
}
}

View File

@@ -0,0 +1,116 @@
<?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\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 ReflectionClass;
use stdClass;
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\WorkflowInterface;
use function count;
/**
* @internal
* @coversNothing
*/
final 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(
static 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);
}
}

View File

@@ -41,11 +41,32 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac
$this->userRender = $userRender;
}
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 static function getSubscribedEvents(): array
{
return [
'workflow.transition' => 'onTransition',
'workflow.completed' => 'onCompleted',
'workflow.completed' => [
['markAsFinal', 2048],
['addDests', 2048],
],
'workflow.guard' => [
['guardEntityWorkflow', 0],
],
@@ -90,7 +111,7 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac
}
}
public function onCompleted(Event $event): void
public function markAsFinal(Event $event): void
{
if (!$event->getSubject() instanceof EntityWorkflow) {
return;

View File

@@ -52,10 +52,21 @@ 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 (@see{EntityWorkflowStep::addDestUser}). Currently, this is done during
*
* @see{EntityWorkflowTransitionEventSubscriber::addDests}.
*/
public function onCompletedSendNotification(Event $event): void
{
if (!$event->getSubject() instanceof EntityWorkflow) {
@@ -65,23 +76,28 @@ 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 +118,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();
}
}
}

View File

@@ -89,12 +89,12 @@ services:
- { name: validator.constraint_validator, alias: 'role_scope_scope_presence' }
Chill\MainBundle\Export\ExportManager:
arguments:
- "@logger"
- "@doctrine.orm.entity_manager"
- "@security.authorization_checker"
- "@chill.main.security.authorization.helper"
- "@security.token_storage"
autoconfigure: true
autowire: true
Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface: '@Chill\MainBundle\Security\Resolver\CenterResolverDispatcher'
Chill\MainBundle\Service\Import\:
resource: '../Service/Import/'
autowire: true
autoconfigure: true

View File

@@ -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 }

View File

@@ -81,12 +81,8 @@ services:
chill.main.form.pick_centers_type:
class: Chill\MainBundle\Form\Type\Export\PickCenterType
arguments:
- "@security.token_storage"
- '@Chill\MainBundle\Export\ExportManager'
- "@chill.main.security.authorization.helper"
tags:
- { name: form.type }
autowire: true
autoconfigure: true
chill.main.form.formatter_type:
class: Chill\MainBundle\Form\Type\Export\FormatterType

View File

@@ -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)');
}
}

View File

@@ -0,0 +1,33 @@
<?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 Version20220730204216 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('DROP INDEX chill_main_address_reference_unicity');
}
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)');
}
}

View File

@@ -0,0 +1,36 @@
<?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;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20220913174922 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_geographical_unit ALTER COLUMN geom SET DATA TYPE TEXT');
}
public function getDescription(): string
{
return 'Geographical Unit correction';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_geographical_unit ALTER COLUMN geom SET DATA TYPE GEOMETRY(MULTIPOLYGON, 4326)');
}
}

View File

@@ -0,0 +1,64 @@
<?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 Version20221003112151 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->throwIrreversibleMigrationException();
/* for memory
$this->addSql('ALTER TABLE chill_main_geographical_unit DROP CONSTRAINT FK_360A2B2FEA6EFDCD');
$this->addSql('DROP SEQUENCE chill_main_geographical_unit_layer_id_seq CASCADE');
$this->addSql('DROP TABLE chill_main_geographical_unit_layer');
$this->addSql('ALTER TABLE chill_main_geographical_unit ADD layername VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE chill_main_geographical_unit DROP layer_id');
$this->addSql('ALTER TABLE chill_main_geographical_unit DROP unitRefId');
$this->addSql('ALTER TABLE chill_main_geographical_unit ALTER geom TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE chill_main_geographical_unit ALTER unitName TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE chill_main_geographical_unit ALTER unitName DROP DEFAULT');
$this->addSql('ALTER TABLE chill_main_geographical_unit ALTER unitName DROP NOT NULL');
*/
}
public function getDescription(): string
{
return 'Add a proper entity for GeographicalUnitLayer';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE SEQUENCE chill_main_geographical_unit_layer_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE chill_main_geographical_unit_layer (id INT NOT NULL, name JSONB DEFAULT \'[]\'::jsonb NOT NULL, refid TEXT DEFAULT \'\' NOT NULL, PRIMARY KEY(id))');
$this->addSql("COMMENT ON COLUMN chill_main_geographical_unit_layer.name IS '(DC2Type:json)';");
$this->addSql('INSERT INTO chill_main_geographical_unit_layer (id, name, refid)
SELECT DISTINCT nextval(\'chill_main_geographical_unit_layer_id_seq\'), jsonb_build_object(\'fr\', layername), layername FROM chill_main_geographical_unit');
$this->addSql('ALTER TABLE chill_main_geographical_unit ADD layer_id INT DEFAULT NULL');
$this->addSql('UPDATE chill_main_geographical_unit SET layer_id = layer.id FROM chill_main_geographical_unit_layer AS layer WHERE layer.refid = chill_main_geographical_unit.layername');
$this->addSql('ALTER TABLE chill_main_geographical_unit ADD unitRefId TEXT DEFAULT \'\' NOT NULL');
$this->addSql('ALTER TABLE chill_main_geographical_unit DROP layername');
$this->addSql("COMMENT ON COLUMN chill_main_geographical_unit.geom IS '(DC2Type:text)';");
$this->addSql('ALTER TABLE chill_main_geographical_unit ALTER unitname TYPE TEXT');
$this->addSql('ALTER TABLE chill_main_geographical_unit ALTER unitname SET DEFAULT \'\'');
$this->addSql('ALTER TABLE chill_main_geographical_unit ALTER unitname SET NOT NULL');
$this->addSql('ALTER TABLE chill_main_geographical_unit ADD CONSTRAINT FK_360A2B2FEA6EFDCD FOREIGN KEY (layer_id) REFERENCES chill_main_geographical_unit_layer (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX IDX_360A2B2FEA6EFDCD ON chill_main_geographical_unit (layer_id)');
}
}

View File

@@ -0,0 +1,37 @@
<?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 Version20221003132620 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('DROP INDEX geographical_unit_layer_refid');
$this->addSql('DROP INDEX geographical_unit_refid');
$this->addSql('DROP INDEX chill_internal_geographical_unit_layer_geom_idx');
}
public function getDescription(): string
{
return 'Create indexes and unique constraints on geographical unit entities';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE UNIQUE INDEX geographical_unit_layer_refid ON chill_main_geographical_unit_layer (refId)');
$this->addSql('CREATE UNIQUE INDEX geographical_unit_refid ON chill_main_geographical_unit (layer_id, unitRefId)');
$this->addSql('CREATE INDEX chill_internal_geographical_unit_layer_geom_idx ON chill_main_geographical_unit USING GIST (layer_id, geom)');
}
}