mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-26 16:45:01 +00:00
Merge branch 'calendar/finalization' into chill_amli
This commit is contained in:
@@ -18,6 +18,7 @@ use Chill\MainBundle\DependencyInjection\CompilerPass\GroupingCenterCompilerPass
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\MenuCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\NotificationCounterCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\SearchableServicesCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\ShortMessageCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\TimelineCompilerClass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\WidgetsCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\ConfigConsistencyCompilerPass;
|
||||
@@ -70,5 +71,6 @@ class ChillMainBundle extends Bundle
|
||||
$container->addCompilerPass(new ACLFlagsCompilerPass());
|
||||
$container->addCompilerPass(new GroupingCenterCompilerPass());
|
||||
$container->addCompilerPass(new CRUDControllerCompilerPass());
|
||||
$container->addCompilerPass(new ShortMessageCompilerPass());
|
||||
}
|
||||
}
|
||||
|
@@ -405,7 +405,7 @@ class ExportController extends AbstractController
|
||||
'alias' => $alias,
|
||||
];
|
||||
unset($parameters['_token']);
|
||||
$key = md5(uniqid(mt_rand(), false));
|
||||
$key = md5(uniqid((string) mt_rand(), false));
|
||||
|
||||
$this->redis->setEx($key, 3600, serialize($parameters));
|
||||
|
||||
|
@@ -196,8 +196,11 @@ class ChillMainExtension extends Extension implements
|
||||
$loader->load('services/search.yaml');
|
||||
$loader->load('services/serializer.yaml');
|
||||
$loader->load('services/mailer.yaml');
|
||||
$loader->load('services/short_message.yaml');
|
||||
|
||||
$this->configureCruds($container, $config['cruds'], $config['apis'], $loader);
|
||||
$container->setParameter('chill_main.short_messages', $config['short_messages']);
|
||||
//$this->configureSms($config['short_messages'], $container, $loader);
|
||||
}
|
||||
|
||||
public function prepend(ContainerBuilder $container)
|
||||
|
@@ -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\DependencyInjection\CompilerPass;
|
||||
|
||||
use Chill\MainBundle\Service\ShortMessage\NullShortMessageSender;
|
||||
use Chill\MainBundle\Service\ShortMessage\ShortMessageTransporter;
|
||||
use Chill\MainBundle\Service\ShortMessageOvh\OvhShortMessageSender;
|
||||
use libphonenumber\PhoneNumberUtil;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use function array_key_exists;
|
||||
|
||||
class ShortMessageCompilerPass implements CompilerPassInterface
|
||||
{
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
$config = $container->resolveEnvPlaceholders($container->getParameter('chill_main.short_messages', null), true);
|
||||
// weird fix for special characters
|
||||
$config['dsn'] = str_replace(['%%'], ['%'], $config['dsn']);
|
||||
$dsn = parse_url($config['dsn']);
|
||||
parse_str($dsn['query'] ?? '', $dsn['queries']);
|
||||
|
||||
if ('null' === $dsn['scheme'] || false === $config['enabled']) {
|
||||
$defaultTransporter = new Reference(NullShortMessageSender::class);
|
||||
} elseif ('ovh' === $dsn['scheme']) {
|
||||
if (!class_exists('\Ovh\Api')) {
|
||||
throw new RuntimeException('Class \\Ovh\\Api not found');
|
||||
}
|
||||
|
||||
foreach (['user', 'host', 'pass'] as $component) {
|
||||
if (!array_key_exists($component, $dsn)) {
|
||||
throw new RuntimeException(sprintf('The component %s does not exist in dsn. Please provide a dsn ' .
|
||||
'like ovh://applicationKey:applicationSecret@endpoint?consumerKey=xxxx&sender=yyyy&service_name=zzzz', $component));
|
||||
}
|
||||
|
||||
$container->setParameter('chill_main.short_messages.ovh_config_' . $component, $dsn[$component]);
|
||||
}
|
||||
|
||||
foreach (['consumer_key', 'sender', 'service_name'] as $param) {
|
||||
if (!array_key_exists($param, $dsn['queries'])) {
|
||||
throw new RuntimeException(sprintf('The parameter %s does not exist in dsn. Please provide a dsn ' .
|
||||
'like ovh://applicationKey:applicationSecret@endpoint?consumerKey=xxxx&sender=yyyy&service_name=zzzz', $param));
|
||||
}
|
||||
$container->setParameter('chill_main.short_messages.ovh_config_' . $param, $dsn['queries'][$param]);
|
||||
}
|
||||
|
||||
$ovh = new Definition();
|
||||
$ovh
|
||||
->setClass('\Ovh\Api')
|
||||
->setArgument(0, $dsn['user'])
|
||||
->setArgument(1, $dsn['pass'])
|
||||
->setArgument(2, $dsn['host'])
|
||||
->setArgument(3, $dsn['queries']['consumer_key']);
|
||||
$container->setDefinition('Ovh\Api', $ovh);
|
||||
|
||||
$ovhSender = new Definition();
|
||||
$ovhSender
|
||||
->setClass(OvhShortMessageSender::class)
|
||||
->setArgument(0, new Reference('Ovh\Api'))
|
||||
->setArgument(1, $dsn['queries']['service_name'])
|
||||
->setArgument(2, $dsn['queries']['sender'])
|
||||
->setArgument(3, new Reference(LoggerInterface::class))
|
||||
->setArgument(4, new Reference(PhoneNumberUtil::class));
|
||||
$container->setDefinition(OvhShortMessageSender::class, $ovhSender);
|
||||
|
||||
$defaultTransporter = new Reference(OvhShortMessageSender::class);
|
||||
} else {
|
||||
throw new RuntimeException(sprintf('Cannot find a sender for this dsn: %s', $config['dsn']));
|
||||
}
|
||||
|
||||
$container->getDefinition(ShortMessageTransporter::class)
|
||||
->setArgument(0, $defaultTransporter);
|
||||
}
|
||||
}
|
@@ -102,6 +102,14 @@ class Configuration implements ConfigurationInterface
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('short_messages')
|
||||
->canBeEnabled()
|
||||
->children()
|
||||
->scalarNode('dsn')->cannotBeEmpty()->defaultValue('null://null')
|
||||
->info('the dsn for sending short message. Example: ovh://applicationKey:secret@endpoint')
|
||||
->end()
|
||||
->end()
|
||||
->end() // end for 'short_messages'
|
||||
->arrayNode('acl')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
|
@@ -359,6 +359,11 @@ class Address
|
||||
return $this->validTo;
|
||||
}
|
||||
|
||||
public function hasAddressReference(): bool
|
||||
{
|
||||
return null !== $this->getAddressReference();
|
||||
}
|
||||
|
||||
public function isNoAddress(): bool
|
||||
{
|
||||
return $this->getIsNoAddress();
|
||||
|
@@ -168,6 +168,11 @@ class AddressReference
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
public function hasPoint(): bool
|
||||
{
|
||||
return null !== $this->getPoint();
|
||||
}
|
||||
|
||||
public function setCreatedAt(?DateTimeImmutable $createdAt): self
|
||||
{
|
||||
$this->createdAt = $createdAt;
|
||||
|
@@ -180,6 +180,11 @@ class Location implements TrackCreationInterface, TrackUpdateInterface
|
||||
return $this->updatedBy;
|
||||
}
|
||||
|
||||
public function hasAddress(): bool
|
||||
{
|
||||
return null !== $this->getAddress();
|
||||
}
|
||||
|
||||
public function setActive(bool $active): self
|
||||
{
|
||||
$this->active = $active;
|
||||
|
@@ -43,7 +43,7 @@ class User implements AdvancedUserInterface
|
||||
/**
|
||||
* Array where SAML attributes's data are stored.
|
||||
*
|
||||
* @ORM\Column(type="json", nullable=true)
|
||||
* @ORM\Column(type="json", nullable=false)
|
||||
*/
|
||||
private array $attributes = [];
|
||||
|
||||
@@ -359,16 +359,21 @@ class User implements AdvancedUserInterface
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set attributes.
|
||||
*
|
||||
* @param array $attributes
|
||||
*
|
||||
* @return Report
|
||||
*/
|
||||
public function setAttributes($attributes)
|
||||
public function setAttributeByDomain(string $domain, string $key, $value): self
|
||||
{
|
||||
$this->attributes = $attributes;
|
||||
$this->attributes[$domain][$key] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the attributes with existing attributes.
|
||||
*
|
||||
* Only the key provided will be created or updated. For a two-level array, use @see{User::setAttributeByDomain}
|
||||
*/
|
||||
public function setAttributes(array $attributes): self
|
||||
{
|
||||
$this->attributes = array_merge($this->attributes, $attributes);
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -506,4 +511,11 @@ class User implements AdvancedUserInterface
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function unsetAttribute($key): self
|
||||
{
|
||||
unset($this->attributes[$key]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@@ -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\Form\DataTransformer;
|
||||
|
||||
use Closure;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use Symfony\Component\Form\DataTransformerInterface;
|
||||
use Symfony\Component\Form\Exception\TransformationFailedException;
|
||||
use function call_user_func;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
class IdToEntityDataTransformer implements DataTransformerInterface
|
||||
{
|
||||
private Closure $getId;
|
||||
|
||||
private bool $multiple = false;
|
||||
|
||||
private ObjectRepository $repository;
|
||||
|
||||
/**
|
||||
* @param Closure $getId
|
||||
*/
|
||||
public function __construct(ObjectRepository $repository, bool $multiple = false, ?callable $getId = null)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->multiple = $multiple;
|
||||
$this->getId = $getId ?? static function (object $o) { return $o->getId(); };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
*
|
||||
* @return array|object[]|T[]|T|object
|
||||
*/
|
||||
public function reverseTransform($value)
|
||||
{
|
||||
if ($this->multiple) {
|
||||
if (null === $value | '' === $value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_map(
|
||||
fn (string $id): ?object => $this->repository->findOneBy(['id' => (int) $id]),
|
||||
explode(',', $value)
|
||||
);
|
||||
}
|
||||
|
||||
if (null === $value | '' === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$object = $this->repository->findOneBy(['id' => (int) $value]);
|
||||
|
||||
if (null === $object) {
|
||||
throw new TransformationFailedException('could not find any object by object id');
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object|T|object[]|T[] $value
|
||||
*/
|
||||
public function transform($value): string
|
||||
{
|
||||
if ($this->multiple) {
|
||||
$ids = [];
|
||||
|
||||
foreach ($value as $v) {
|
||||
$ids[] = $id = call_user_func($this->getId, $v);
|
||||
|
||||
if (null === $id) {
|
||||
throw new TransformationFailedException('id is null');
|
||||
}
|
||||
}
|
||||
|
||||
return implode(',', $ids);
|
||||
}
|
||||
|
||||
if (null === $value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$id = call_user_func($this->getId, $value);
|
||||
|
||||
if (null === $id) {
|
||||
throw new TransformationFailedException('id is null');
|
||||
}
|
||||
|
||||
return (string) $id;
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
<?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\Form\DataTransformer;
|
||||
|
||||
use Chill\MainBundle\Repository\LocationRepository;
|
||||
|
||||
class IdToLocationDataTransformer extends IdToEntityDataTransformer
|
||||
{
|
||||
public function __construct(LocationRepository $repository)
|
||||
{
|
||||
parent::__construct($repository, false);
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
<?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\Form\DataTransformer;
|
||||
|
||||
use Chill\MainBundle\Repository\UserRepository;
|
||||
|
||||
class IdToUserDataTransformer extends IdToEntityDataTransformer
|
||||
{
|
||||
public function __construct(UserRepository $repository)
|
||||
{
|
||||
parent::__construct($repository, false);
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
<?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\Form\DataTransformer;
|
||||
|
||||
use Chill\MainBundle\Repository\UserRepository;
|
||||
|
||||
class IdToUsersDataTransformer extends IdToEntityDataTransformer
|
||||
{
|
||||
public function __construct(UserRepository $repository)
|
||||
{
|
||||
parent::__construct($repository, true);
|
||||
}
|
||||
}
|
@@ -75,14 +75,16 @@ class PickCenterType extends AbstractType
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
|
||||
|
||||
$export = $this->exportManager->getExport($options['export_alias']);
|
||||
$centers = $this->authorizationHelper->getReachableCenters(
|
||||
$this->user,
|
||||
$export->requiredRole()
|
||||
(string) $export->requiredRole()
|
||||
);
|
||||
|
||||
$builder->add(self::CENTERS_IDENTIFIERS, EntityType::class, [
|
||||
'class' => 'ChillMainBundle:Center',
|
||||
'class' => Center::class,
|
||||
'query_builder' => static function (EntityRepository $er) use ($centers) {
|
||||
$qb = $er->createQueryBuilder('c');
|
||||
$ids = array_map(
|
||||
|
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Form\Type\Listing;
|
||||
|
||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
@@ -70,10 +71,38 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType
|
||||
$builder->add($checkboxesBuilder);
|
||||
}
|
||||
|
||||
if (0 < count($helper->getDateRanges())) {
|
||||
$dateRangesBuilder = $builder->create('dateRanges', null, ['compound' => true]);
|
||||
|
||||
foreach ($helper->getDateRanges() as $name => $opts) {
|
||||
$rangeBuilder = $dateRangesBuilder->create($name, null, [
|
||||
'compound' => true,
|
||||
'label' => null === $opts['label'] ? false : $opts['label'] ?? $name,
|
||||
]);
|
||||
|
||||
$rangeBuilder->add(
|
||||
'from',
|
||||
ChillDateType::class,
|
||||
['input' => 'datetime_immutable', 'required' => false]
|
||||
);
|
||||
$rangeBuilder->add(
|
||||
'to',
|
||||
ChillDateType::class,
|
||||
['input' => 'datetime_immutable', 'required' => false]
|
||||
);
|
||||
|
||||
$dateRangesBuilder->add($rangeBuilder);
|
||||
}
|
||||
|
||||
$builder->add($dateRangesBuilder);
|
||||
}
|
||||
|
||||
foreach ($this->requestStack->getCurrentRequest()->query->getIterator() as $key => $value) {
|
||||
switch ($key) {
|
||||
case 'q':
|
||||
case 'checkboxes' . $key:
|
||||
case $key . '_from':
|
||||
case $key . '_to':
|
||||
break;
|
||||
|
||||
case 'page':
|
||||
|
@@ -119,8 +119,8 @@ final class PostalCodeRepository implements ObjectRepository
|
||||
|
||||
$pertinenceClause = ['STRICT_WORD_SIMILARITY(canonical, UNACCENT(?))'];
|
||||
$pertinenceArgs = [$pattern];
|
||||
$orWhere = ['canonical %>> UNACCENT(?)'];
|
||||
$orWhereArgs = [$pattern];
|
||||
$andWhere = ['canonical %>> UNACCENT(?)'];
|
||||
$andWhereArgs = [$pattern];
|
||||
|
||||
foreach (explode(' ', $pattern) as $part) {
|
||||
$part = trim($part);
|
||||
@@ -129,8 +129,8 @@ final class PostalCodeRepository implements ObjectRepository
|
||||
continue;
|
||||
}
|
||||
|
||||
$orWhere[] = "canonical LIKE '%' || UNACCENT(LOWER(?)) || '%'";
|
||||
$orWhereArgs[] = $part;
|
||||
$andWhere[] = "canonical LIKE '%' || UNACCENT(LOWER(?)) || '%'";
|
||||
$andWhereArgs[] = $part;
|
||||
$pertinenceClause[] =
|
||||
"(EXISTS (SELECT 1 FROM unnest(string_to_array(canonical, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(?)))))::int";
|
||||
$pertinenceClause[] =
|
||||
@@ -139,7 +139,7 @@ final class PostalCodeRepository implements ObjectRepository
|
||||
}
|
||||
$query
|
||||
->setSelectPertinence(implode(' + ', $pertinenceClause), $pertinenceArgs)
|
||||
->andWhereClause(implode(' OR ', $orWhere), $orWhereArgs);
|
||||
->andWhereClause(implode(' AND ', $andWhere), $andWhereArgs);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
@@ -15,12 +15,14 @@ use Chill\MainBundle\Entity\GroupCenter;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\NoResultException;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\ORM\Query\ResultSetMappingBuilder;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
use function count;
|
||||
|
||||
final class UserRepository implements ObjectRepository
|
||||
final class UserRepository implements UserRepositoryInterface
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
@@ -42,6 +44,16 @@ final class UserRepository implements ObjectRepository
|
||||
return $this->countBy(['enabled' => true]);
|
||||
}
|
||||
|
||||
public function countByNotHavingAttribute(string $key): int
|
||||
{
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('count', 'count');
|
||||
|
||||
$sql = 'SELECT count(*) FROM users u WHERE NOT attributes ?? :key OR attributes IS NULL AND enabled IS TRUE';
|
||||
|
||||
return $this->entityManager->createNativeQuery($sql, $rsm)->setParameter(':key', $key)->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function countByUsernameOrEmail(string $pattern): int
|
||||
{
|
||||
$qb = $this->queryByUsernameOrEmail($pattern);
|
||||
@@ -83,6 +95,29 @@ final class UserRepository implements ObjectRepository
|
||||
return $this->findBy(['enabled' => true], $orderBy, $limit, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find users which does not have a key on attribute column.
|
||||
*
|
||||
* @return array|User[]
|
||||
*/
|
||||
public function findByNotHavingAttribute(string $key, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
$rsm = new ResultSetMappingBuilder($this->entityManager);
|
||||
$rsm->addRootEntityFromClassMetadata(User::class, 'u');
|
||||
|
||||
$sql = 'SELECT ' . $rsm->generateSelectClause() . ' FROM users u WHERE NOT attributes ?? :key OR attributes IS NULL AND enabled IS TRUE';
|
||||
|
||||
if (null !== $limit) {
|
||||
$sql .= " LIMIT {$limit}";
|
||||
}
|
||||
|
||||
if (null !== $offset) {
|
||||
$sql .= " OFFSET {$offset}";
|
||||
}
|
||||
|
||||
return $this->entityManager->createNativeQuery($sql, $rsm)->setParameter(':key', $key)->getResult();
|
||||
}
|
||||
|
||||
public function findByUsernameOrEmail(string $pattern, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
$qb = $this->queryByUsernameOrEmail($pattern);
|
||||
@@ -109,11 +144,15 @@ final class UserRepository implements ObjectRepository
|
||||
return $this->repository->findOneBy($criteria, $orderBy);
|
||||
}
|
||||
|
||||
public function findOneByUsernameOrEmail(string $pattern)
|
||||
public function findOneByUsernameOrEmail(string $pattern): ?User
|
||||
{
|
||||
$qb = $this->queryByUsernameOrEmail($pattern);
|
||||
$qb = $this->queryByUsernameOrEmail($pattern)->select('u');
|
||||
|
||||
return $qb->getQuery()->getSingleResult();
|
||||
try {
|
||||
return $qb->getQuery()->getSingleResult();
|
||||
} catch (NoResultException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,7 +210,7 @@ final class UserRepository implements ObjectRepository
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function getClassName()
|
||||
public function getClassName(): string
|
||||
{
|
||||
return User::class;
|
||||
}
|
||||
|
@@ -0,0 +1,73 @@
|
||||
<?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\User;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
interface UserRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
public function countBy(array $criteria): int;
|
||||
|
||||
public function countByActive(): int;
|
||||
|
||||
public function countByNotHavingAttribute(string $key): int;
|
||||
|
||||
public function countByUsernameOrEmail(string $pattern): int;
|
||||
|
||||
public function find($id, $lockMode = null, $lockVersion = null): ?User;
|
||||
|
||||
/**
|
||||
* @return User[]
|
||||
*/
|
||||
public function findAll(): array;
|
||||
|
||||
/**
|
||||
* @param mixed|null $limit
|
||||
* @param mixed|null $offset
|
||||
*
|
||||
* @return User[]
|
||||
*/
|
||||
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array;
|
||||
|
||||
/**
|
||||
* @return array|User[]
|
||||
*/
|
||||
public function findByActive(?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
|
||||
|
||||
/**
|
||||
* Find users which does not have a key on attribute column.
|
||||
*
|
||||
* @return array|User[]
|
||||
*/
|
||||
public function findByNotHavingAttribute(string $key, ?int $limit = null, ?int $offset = null): array;
|
||||
|
||||
public function findByUsernameOrEmail(string $pattern, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array;
|
||||
|
||||
public function findOneBy(array $criteria, ?array $orderBy = null): ?User;
|
||||
|
||||
public function findOneByUsernameOrEmail(string $pattern): ?User;
|
||||
|
||||
/**
|
||||
* Get the users having a specific flags.
|
||||
*
|
||||
* If provided, only the users amongst "filtered users" are searched. This
|
||||
* allows to make a first search amongst users based on role and center
|
||||
* and, then filter those users having some flags.
|
||||
*
|
||||
* @param \Chill\MainBundle\Entity\User[] $amongstUsers
|
||||
* @param mixed $flag
|
||||
*/
|
||||
public function findUsersHavingFlags($flag, array $amongstUsers = []): array;
|
||||
|
||||
public function getClassName(): string;
|
||||
}
|
@@ -24,7 +24,7 @@ require('./chillmain.scss');
|
||||
import { chill } from './js/chill.js';
|
||||
global.chill = chill;
|
||||
|
||||
require('./js/date.js');
|
||||
require('./js/date');
|
||||
require('./js/counter.js');
|
||||
|
||||
/// Load fonts
|
||||
|
@@ -12,7 +12,7 @@
|
||||
* Do not take time into account
|
||||
*
|
||||
*/
|
||||
const dateToISO = (date) => {
|
||||
export const dateToISO = (date: Date|null): string|null => {
|
||||
if (null === date) {
|
||||
return null;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ const dateToISO = (date) => {
|
||||
*
|
||||
* **Experimental**
|
||||
*/
|
||||
const ISOToDate = (str) => {
|
||||
export const ISOToDate = (str: string|null): Date|null => {
|
||||
if (null === str) {
|
||||
return null;
|
||||
}
|
||||
@@ -38,25 +38,25 @@ const ISOToDate = (str) => {
|
||||
}
|
||||
|
||||
let
|
||||
[year, month, day] = str.split('-');
|
||||
[year, month, day] = str.split('-').map(p => parseInt(p));
|
||||
|
||||
return new Date(year, month-1, day);
|
||||
return new Date(year, month-1, day, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a date object from iso string formatted as YYYY-mm-dd:HH:MM:ss+01:00
|
||||
*
|
||||
*/
|
||||
const ISOToDatetime = (str) => {
|
||||
export const ISOToDatetime = (str: string|null): Date|null => {
|
||||
if (null === str) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let
|
||||
[cal, times] = str.split('T'),
|
||||
[year, month, date] = cal.split('-'),
|
||||
[year, month, date] = cal.split('-').map(s => parseInt(s)),
|
||||
[time, timezone] = times.split(times.charAt(8)),
|
||||
[hours, minutes, seconds] = time.split(':')
|
||||
[hours, minutes, seconds] = time.split(':').map(s => parseInt(s));
|
||||
;
|
||||
|
||||
return new Date(year, month-1, date, hours, minutes, seconds);
|
||||
@@ -66,7 +66,7 @@ const ISOToDatetime = (str) => {
|
||||
* Convert a date to ISO8601, valid for usage in api
|
||||
*
|
||||
*/
|
||||
const datetimeToISO = (date) => {
|
||||
export const datetimeToISO = (date: Date): string => {
|
||||
let cal, time, offset;
|
||||
cal = [
|
||||
date.getFullYear(),
|
||||
@@ -92,7 +92,7 @@ const datetimeToISO = (date) => {
|
||||
return x;
|
||||
};
|
||||
|
||||
const intervalDaysToISO = (days) => {
|
||||
export const intervalDaysToISO = (days: number|string|null): string => {
|
||||
if (null === days) {
|
||||
return 'P0D';
|
||||
}
|
||||
@@ -100,7 +100,7 @@ const intervalDaysToISO = (days) => {
|
||||
return `P${days}D`;
|
||||
}
|
||||
|
||||
const intervalISOToDays = (str) => {
|
||||
export const intervalISOToDays = (str: string|null): number|null => {
|
||||
if (null === str) {
|
||||
return null
|
||||
}
|
||||
@@ -154,12 +154,3 @@ const intervalISOToDays = (str) => {
|
||||
|
||||
return days;
|
||||
}
|
||||
|
||||
export {
|
||||
dateToISO,
|
||||
ISOToDate,
|
||||
ISOToDatetime,
|
||||
datetimeToISO,
|
||||
intervalISOToDays,
|
||||
intervalDaysToISO,
|
||||
};
|
@@ -0,0 +1,4 @@
|
||||
export function fetchResults<T>(uri: string, params: {item_per_page?: number}): Promise<T[]>;
|
||||
|
||||
export function makeFetch<T, B>(method: "GET"|"POST"|"PATCH"|"DELETE", url: string, body: B, options: {[key: string]: string}): Promise<T>;
|
||||
|
@@ -1,110 +0,0 @@
|
||||
/**
|
||||
* Generic api method that can be adapted to any fetch request
|
||||
*/
|
||||
const makeFetch = (method, url, body, options) => {
|
||||
let opts = {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8'
|
||||
},
|
||||
body: (body !== null) ? JSON.stringify(body) : null
|
||||
};
|
||||
|
||||
if (typeof options !== 'undefined') {
|
||||
opts = Object.assign(opts, options);
|
||||
}
|
||||
|
||||
return fetch(url, opts)
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
if (response.status === 422) {
|
||||
return response.json().then(response => {
|
||||
throw ValidationException(response)
|
||||
});
|
||||
}
|
||||
|
||||
if (response.status === 403) {
|
||||
throw AccessException(response);
|
||||
}
|
||||
|
||||
throw {
|
||||
name: 'Exception',
|
||||
sta: response.status,
|
||||
txt: response.statusText,
|
||||
err: new Error(),
|
||||
violations: response.body
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch results with certain parameters
|
||||
*/
|
||||
const _fetchAction = (page, uri, params) => {
|
||||
const item_per_page = 50;
|
||||
if (params === undefined) {
|
||||
params = {};
|
||||
}
|
||||
let url = uri + '?' + new URLSearchParams({ item_per_page, page, ...params });
|
||||
|
||||
return fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8'
|
||||
},
|
||||
}).then(response => {
|
||||
if (response.ok) { return response.json(); }
|
||||
throw Error({ m: response.statusText });
|
||||
});
|
||||
};
|
||||
|
||||
const fetchResults = async (uri, params) => {
|
||||
let promises = [],
|
||||
page = 1;
|
||||
let firstData = await _fetchAction(page, uri, params);
|
||||
|
||||
promises.push(Promise.resolve(firstData.results));
|
||||
|
||||
if (firstData.pagination.more) {
|
||||
do {
|
||||
page = ++page;
|
||||
promises.push(_fetchAction(page, uri, params).then(r => Promise.resolve(r.results)));
|
||||
} while (page * firstData.pagination.items_per_page < firstData.count)
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(values => values.flat());
|
||||
};
|
||||
|
||||
const fetchScopes = () => {
|
||||
return fetchResults('/api/1.0/main/scope.json');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Error objects to be thrown
|
||||
*/
|
||||
const ValidationException = (response) => {
|
||||
const error = {};
|
||||
error.name = 'ValidationException';
|
||||
error.violations = response.violations.map((violation) => `${violation.title}: ${violation.propertyPath}`);
|
||||
error.titles = response.violations.map((violation) => violation.title);
|
||||
error.propertyPaths = response.violations.map((violation) => violation.propertyPath);
|
||||
return error;
|
||||
}
|
||||
|
||||
const AccessException = (response) => {
|
||||
const error = {};
|
||||
error.name = 'AccessException';
|
||||
error.violations = ['You are not allowed to perform this action'];
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
export {
|
||||
makeFetch,
|
||||
fetchResults,
|
||||
fetchScopes
|
||||
}
|
@@ -0,0 +1,220 @@
|
||||
import {Scope} from '../../types';
|
||||
|
||||
export type body = {[key: string]: boolean|string|number|null};
|
||||
export type fetchOption = {[key: string]: boolean|string|number|null};
|
||||
|
||||
export interface Params {
|
||||
[key: string]: number|string
|
||||
}
|
||||
|
||||
export interface PaginationResponse<T> {
|
||||
pagination: {
|
||||
more: boolean;
|
||||
items_per_page: number;
|
||||
};
|
||||
results: T[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface FetchParams {
|
||||
[K: string]: string|number|null;
|
||||
};
|
||||
|
||||
export interface TransportExceptionInterface {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ValidationExceptionInterface extends TransportExceptionInterface {
|
||||
name: 'ValidationException';
|
||||
error: object;
|
||||
violations: string[];
|
||||
titles: string[];
|
||||
propertyPaths: string[];
|
||||
}
|
||||
|
||||
export interface ValidationErrorResponse extends TransportExceptionInterface {
|
||||
violations: {
|
||||
title: string;
|
||||
propertyPath: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface AccessExceptionInterface extends TransportExceptionInterface {
|
||||
name: 'AccessException';
|
||||
violations: string[];
|
||||
}
|
||||
|
||||
export interface NotFoundExceptionInterface extends TransportExceptionInterface {
|
||||
name: 'NotFoundException';
|
||||
}
|
||||
|
||||
export interface ServerExceptionInterface extends TransportExceptionInterface {
|
||||
name: 'ServerException';
|
||||
message: string;
|
||||
code: number;
|
||||
body: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generic api method that can be adapted to any fetch request
|
||||
*/
|
||||
export const makeFetch = <Input, Output>(method: 'POST'|'GET'|'PUT'|'PATCH'|'DELETE', url: string, body?: body | Input | null, options?: FetchParams): Promise<Output> => {
|
||||
let opts = {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8'
|
||||
},
|
||||
body: (body !== null || typeof body !== 'undefined') ? JSON.stringify(body) : null
|
||||
};
|
||||
|
||||
if (typeof options !== 'undefined') {
|
||||
opts = Object.assign(opts, options);
|
||||
}
|
||||
|
||||
return fetch(url, opts)
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
if (response.status === 422) {
|
||||
return response.json().then(response => {
|
||||
throw ValidationException(response)
|
||||
});
|
||||
}
|
||||
|
||||
if (response.status === 403) {
|
||||
throw AccessException(response);
|
||||
}
|
||||
|
||||
throw {
|
||||
name: 'Exception',
|
||||
sta: response.status,
|
||||
txt: response.statusText,
|
||||
err: new Error(),
|
||||
violations: response.body
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch results with certain parameters
|
||||
*/
|
||||
function _fetchAction<T>(page: number, uri: string, params?: FetchParams): Promise<PaginationResponse<T>> {
|
||||
const item_per_page: number = 50;
|
||||
|
||||
let searchParams = new URLSearchParams();
|
||||
searchParams.append('item_per_page', item_per_page.toString());
|
||||
searchParams.append('page', page.toString());
|
||||
|
||||
if (params !== undefined) {
|
||||
Object.keys(params).forEach(key => {
|
||||
let v = params[key];
|
||||
if (typeof v === 'string') {
|
||||
searchParams.append(key, v);
|
||||
} else if (typeof v === 'number') {
|
||||
searchParams.append(key, v.toString());
|
||||
} else if (v === null) {
|
||||
searchParams.append(key, '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let url = uri + '?' + searchParams.toString();
|
||||
|
||||
return fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8'
|
||||
},
|
||||
}).then((response) => {
|
||||
if (response.ok) { return response.json(); }
|
||||
|
||||
if (response.status === 404) {
|
||||
throw NotFoundException(response);
|
||||
}
|
||||
|
||||
if (response.status === 422) {
|
||||
return response.json().then(response => {
|
||||
throw ValidationException(response)
|
||||
});
|
||||
}
|
||||
|
||||
if (response.status === 403) {
|
||||
throw AccessException(response);
|
||||
}
|
||||
|
||||
if (response.status >= 500) {
|
||||
return response.text().then(body => {
|
||||
throw ServerException(response.status, body);
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error("other network error");
|
||||
}).catch((reason: any) => {
|
||||
console.error(reason);
|
||||
throw new Error(reason);
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchResults = async<T> (uri: string, params?: FetchParams): Promise<T[]> => {
|
||||
let promises: Promise<T[]>[] = [],
|
||||
page = 1;
|
||||
let firstData: PaginationResponse<T> = await _fetchAction(page, uri, params) as PaginationResponse<T>;
|
||||
|
||||
promises.push(Promise.resolve(firstData.results));
|
||||
|
||||
if (firstData.pagination.more) {
|
||||
do {
|
||||
page = ++page;
|
||||
promises.push(
|
||||
_fetchAction<T>(page, uri, params)
|
||||
.then(r => Promise.resolve(r.results))
|
||||
);
|
||||
} while (page * firstData.pagination.items_per_page < firstData.count)
|
||||
}
|
||||
|
||||
return Promise.all(promises).then((values) => values.flat());
|
||||
};
|
||||
|
||||
export const fetchScopes = (): Promise<Scope[]> => {
|
||||
return fetchResults('/api/1.0/main/scope.json');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Error objects to be thrown
|
||||
*/
|
||||
const ValidationException = (response: ValidationErrorResponse): ValidationExceptionInterface => {
|
||||
const error = {} as ValidationExceptionInterface;
|
||||
error.name = 'ValidationException';
|
||||
error.violations = response.violations.map((violation) => `${violation.title}: ${violation.propertyPath}`);
|
||||
error.titles = response.violations.map((violation) => violation.title);
|
||||
error.propertyPaths = response.violations.map((violation) => violation.propertyPath);
|
||||
return error;
|
||||
}
|
||||
|
||||
const AccessException = (response: Response): AccessExceptionInterface => {
|
||||
const error = {} as AccessExceptionInterface;
|
||||
error.name = 'AccessException';
|
||||
error.violations = ['You are not allowed to perform this action'];
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
const NotFoundException = (response: Response): NotFoundExceptionInterface => {
|
||||
const error = {} as NotFoundExceptionInterface;
|
||||
error.name = 'NotFoundException';
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
const ServerException = (code: number, body: string): ServerExceptionInterface => {
|
||||
const error = {} as ServerExceptionInterface;
|
||||
error.name = 'ServerException';
|
||||
error.code = code;
|
||||
error.body = body;
|
||||
|
||||
return error;
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
import {fetchResults} from "./apiMethods";
|
||||
import {Location, LocationType} from "../../types";
|
||||
|
||||
export const getLocations = (): Promise<Location[]> => fetchResults('/api/1.0/main/location.json');
|
||||
|
||||
export const getLocationTypes = (): Promise<LocationType[]> => fetchResults('/api/1.0/main/location-type.json');
|
25
src/Bundle/ChillMainBundle/Resources/public/lib/api/user.ts
Normal file
25
src/Bundle/ChillMainBundle/Resources/public/lib/api/user.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import {User} from "../../types";
|
||||
import {makeFetch} from "./apiMethods";
|
||||
|
||||
export const whoami = (): Promise<User> => {
|
||||
const url = `/api/1.0/main/whoami.json`;
|
||||
return fetch(url)
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
throw {
|
||||
msg: 'Error while getting whoami.',
|
||||
sta: response.status,
|
||||
txt: response.statusText,
|
||||
err: new Error(),
|
||||
body: response.body
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const whereami = (): Promise<Location | null> => {
|
||||
const url = `/api/1.0/main/user-current-location.json`;
|
||||
|
||||
return makeFetch<null, Location|null>("GET", url);
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -15,12 +15,12 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
var mime = require('mime-types')
|
||||
var mime = require('mime')
|
||||
|
||||
var download_report = (url, container) => {
|
||||
var download_text = container.dataset.downloadText,
|
||||
alias = container.dataset.alias;
|
||||
|
||||
|
||||
window.fetch(url, { credentials: 'same-origin' })
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
@@ -29,21 +29,21 @@ var download_report = (url, container) => {
|
||||
|
||||
return response.blob();
|
||||
}).then(blob => {
|
||||
|
||||
|
||||
var content = URL.createObjectURL(blob),
|
||||
link = document.createElement("a"),
|
||||
type = blob.type,
|
||||
hasForcedType = 'mimeType' in container.dataset,
|
||||
extension;
|
||||
|
||||
|
||||
if (hasForcedType) {
|
||||
// force a type
|
||||
type = container.dataset.mimeType;
|
||||
blob = new Blob([ blob ], { 'type': type });
|
||||
content = URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
extension = mime.extension(type);
|
||||
|
||||
extension = mime.getExtension(type);
|
||||
|
||||
link.appendChild(document.createTextNode(download_text));
|
||||
link.classList.add("btn", "btn-action");
|
||||
@@ -56,7 +56,7 @@ var download_report = (url, container) => {
|
||||
container.appendChild(link);
|
||||
}).catch(function(error) {
|
||||
console.log(error);
|
||||
var problem_text =
|
||||
var problem_text =
|
||||
document.createTextNode("Problem during download");
|
||||
|
||||
container
|
||||
@@ -64,4 +64,4 @@ var download_report = (url, container) => {
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = download_report;
|
||||
module.exports = download_report;
|
||||
|
@@ -6,6 +6,7 @@ import { appMessages } from 'ChillMainAssets/vuejs/PickEntity/i18n';
|
||||
const i18n = _createI18n(appMessages);
|
||||
|
||||
let appsOnPage = new Map();
|
||||
let appsPerInput = new Map();
|
||||
|
||||
function loadDynamicPicker(element) {
|
||||
|
||||
@@ -78,13 +79,14 @@ function loadDynamicPicker(element) {
|
||||
.mount(el);
|
||||
|
||||
appsOnPage.set(uniqId, app);
|
||||
appsPerInput.set(input.name, app);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener('show-hide-show', function(e) {
|
||||
loadDynamicPicker(e.detail.container)
|
||||
})
|
||||
});
|
||||
|
||||
document.addEventListener('show-hide-hide', function(e) {
|
||||
console.log('hiding event caught')
|
||||
@@ -95,7 +97,23 @@ document.addEventListener('show-hide-hide', function(e) {
|
||||
appsOnPage.delete(uniqId);
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
document.addEventListener('pick-entity-type-action', function (e) {
|
||||
console.log('pick entity event', e);
|
||||
if (!appsPerInput.has(e.detail.name)) {
|
||||
console.error('no app with this name');
|
||||
return;
|
||||
}
|
||||
const app = appsPerInput.get(e.detail.name);
|
||||
if (e.detail.action === 'add') {
|
||||
app.addNewEntity(e.detail.entity);
|
||||
} else if (e.detail.action === 'remove') {
|
||||
app.removeEntity(e.detail.entity);
|
||||
} else {
|
||||
console.error('action not supported: '+e.detail.action);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function(e) {
|
||||
loadDynamicPicker(document)
|
||||
|
139
src/Bundle/ChillMainBundle/Resources/public/types.ts
Normal file
139
src/Bundle/ChillMainBundle/Resources/public/types.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
export interface DateTime {
|
||||
datetime: string;
|
||||
datetime8601: string
|
||||
}
|
||||
|
||||
export interface Civility {
|
||||
id: number;
|
||||
// TODO
|
||||
}
|
||||
|
||||
export interface Job {
|
||||
id: number;
|
||||
type: "user_job";
|
||||
label: {
|
||||
"fr": string; // could have other key. How to do that in ts ?
|
||||
}
|
||||
}
|
||||
|
||||
export interface Center {
|
||||
id: number;
|
||||
type: "center";
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Scope {
|
||||
id: number;
|
||||
type: "scope";
|
||||
name: {
|
||||
"fr": string
|
||||
}
|
||||
}
|
||||
|
||||
export interface User {
|
||||
type: "user";
|
||||
id: number;
|
||||
username: string;
|
||||
text: string;
|
||||
email: string;
|
||||
user_job: Job;
|
||||
label: string;
|
||||
// todo: mainCenter; mainJob; etc..
|
||||
}
|
||||
|
||||
export interface UserAssociatedInterface {
|
||||
type: "user";
|
||||
id: number;
|
||||
};
|
||||
|
||||
export type TranslatableString = {
|
||||
fr?: string;
|
||||
nl?: string;
|
||||
}
|
||||
|
||||
export interface Postcode {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
center: Point;
|
||||
}
|
||||
|
||||
export type Point = {
|
||||
type: "Point";
|
||||
coordinates: [lat: number, lon: number];
|
||||
}
|
||||
|
||||
export interface Country {
|
||||
id: number;
|
||||
name: TranslatableString;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface Address {
|
||||
type: "address";
|
||||
address_id: number;
|
||||
text: string;
|
||||
street: string;
|
||||
streetNumber: string;
|
||||
postcode: Postcode;
|
||||
country: Country;
|
||||
floor: string | null;
|
||||
corridor: string | null;
|
||||
steps: string | null;
|
||||
flat: string | null;
|
||||
buildingName: string | null;
|
||||
distribution: string | null;
|
||||
extra: string | null;
|
||||
confidential: boolean;
|
||||
lines: string[];
|
||||
addressReference: AddressReference | null;
|
||||
validFrom: DateTime;
|
||||
validTo: DateTime | null;
|
||||
}
|
||||
|
||||
export interface AddressReference {
|
||||
id: number;
|
||||
createdAt: DateTime | null;
|
||||
deletedAt: DateTime | null;
|
||||
municipalityCode: string;
|
||||
point: Point;
|
||||
postcode: Postcode;
|
||||
refId: string;
|
||||
source: string;
|
||||
street: string;
|
||||
streetNumber: string;
|
||||
updatedAt: DateTime | null;
|
||||
}
|
||||
|
||||
export interface Location {
|
||||
type: "location";
|
||||
id: number;
|
||||
active: boolean;
|
||||
address: Address | null;
|
||||
availableForUsers: boolean;
|
||||
createdAt: DateTime | null;
|
||||
createdBy: User | null;
|
||||
updatedAt: DateTime | null;
|
||||
updatedBy: User | null;
|
||||
email: string | null
|
||||
name: string;
|
||||
phonenumber1: string | null;
|
||||
phonenumber2: string | null;
|
||||
locationType: LocationType;
|
||||
}
|
||||
|
||||
export interface LocationAssociated {
|
||||
type: "location";
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface LocationType {
|
||||
type: "location-type";
|
||||
id: number;
|
||||
active: boolean;
|
||||
addressRequired: "optional" | "required";
|
||||
availableForUsers: boolean;
|
||||
editableByUsers: boolean;
|
||||
contactData: "optional" | "required";
|
||||
title: TranslatableString;
|
||||
}
|
@@ -54,7 +54,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { dateToISO, ISOToDate } from 'ChillMainAssets/chill/js/date.js';
|
||||
import { dateToISO, ISOToDate } from 'ChillMainAssets/chill/js/date';
|
||||
import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue';
|
||||
import ActionButtons from './ActionButtons.vue';
|
||||
|
||||
|
@@ -146,6 +146,9 @@ export default {
|
||||
}
|
||||
},
|
||||
titleCreate() {
|
||||
if (typeof this.allowedTypes === 'undefined') {
|
||||
return 'onthefly.create.title.default';
|
||||
}
|
||||
return this.allowedTypes.every(t => t === 'person')
|
||||
? 'onthefly.create.title.person'
|
||||
: this.allowedTypes.every(t => t === 'thirdparty')
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ul class="list-suggest remove-items" v-if="picked.length">
|
||||
<ul :class="listClasses" v-if="picked.length && displayPicked">
|
||||
<li v-for="p in picked" @click="removeEntity(p)" :key="p.type+p.id">
|
||||
<span class="chill_denomination">{{ p.text }}</span>
|
||||
</li>
|
||||
@@ -40,6 +40,15 @@ export default {
|
||||
uniqid: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
removableIfSet: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
displayPicked: {
|
||||
// display picked entities.
|
||||
type: Boolean,
|
||||
default: true,
|
||||
}
|
||||
},
|
||||
emits: ['addNewEntity', 'removeEntity'],
|
||||
@@ -78,7 +87,13 @@ export default {
|
||||
} else {
|
||||
return appMessages.fr.pick_entity.modal_title_one + trans.join(', ');
|
||||
}
|
||||
}
|
||||
},
|
||||
listClasses() {
|
||||
return {
|
||||
'list-suggest': true,
|
||||
'remove-items': this.$props.removableIfSet,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addNewEntity({ selected, modal }) {
|
||||
@@ -90,6 +105,9 @@ export default {
|
||||
modal.showModal = false;
|
||||
},
|
||||
removeEntity(entity) {
|
||||
if (!this.$props.removableIfSet) {
|
||||
return;
|
||||
}
|
||||
this.$emit('removeEntity', entity);
|
||||
}
|
||||
},
|
||||
|
@@ -20,7 +20,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {makeFetch} from 'ChillMainAssets/lib/api/apiMethods.js';
|
||||
import {makeFetch} from 'ChillMainAssets/lib/api/apiMethods.ts';
|
||||
|
||||
export default {
|
||||
name: "EntityWorkflowVueSubscriber",
|
||||
|
@@ -26,7 +26,8 @@
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import {defineComponent} from "vue";
|
||||
/*
|
||||
* This Modal component is a mix between Vue3 modal implementation
|
||||
* [+] with 'v-if:showModal' directive:parameter, html scope is added/removed not just shown/hidden
|
||||
@@ -36,20 +37,22 @@
|
||||
* [+] using bootstrap css classes, the modal have a responsive behaviour,
|
||||
* [+] modal design can be configured using css classes (size, scroll)
|
||||
*/
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: 'Modal',
|
||||
props: {
|
||||
modalDialogClass: {
|
||||
type: String,
|
||||
required: false
|
||||
type: Object,
|
||||
required: false,
|
||||
default: {},
|
||||
},
|
||||
hideFooter: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['close']
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
@@ -41,7 +41,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods.js';
|
||||
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods.ts';
|
||||
|
||||
export default {
|
||||
name: "NotificationReadToggle",
|
||||
|
@@ -1,27 +1,6 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import datetimeFormats from '../i18n/datetimeFormats';
|
||||
|
||||
const datetimeFormats = {
|
||||
fr: {
|
||||
short: {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric"
|
||||
},
|
||||
text: {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
},
|
||||
long: {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: false
|
||||
}
|
||||
}
|
||||
};
|
||||
const messages = {
|
||||
fr: {
|
||||
action: {
|
||||
@@ -76,11 +55,13 @@ const messages = {
|
||||
}
|
||||
};
|
||||
|
||||
const _createI18n = (appMessages) => {
|
||||
const _createI18n = (appMessages: any, legacy?: boolean) => {
|
||||
Object.assign(messages.fr, appMessages.fr);
|
||||
return createI18n({
|
||||
legacy: typeof legacy === undefined ? true : legacy,
|
||||
locale: 'fr',
|
||||
fallbackLocale: 'fr',
|
||||
// @ts-ignore
|
||||
datetimeFormats,
|
||||
messages,
|
||||
})
|
@@ -0,0 +1,27 @@
|
||||
export default {
|
||||
fr: {
|
||||
short: {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric"
|
||||
},
|
||||
text: {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
},
|
||||
long: {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: false
|
||||
},
|
||||
hoursOnly: {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: false,
|
||||
}
|
||||
}
|
||||
};
|
@@ -10,6 +10,30 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if form.dateRanges is defined %}
|
||||
{% if form.dateRanges|length > 0 %}
|
||||
{% for dateRangeName, _o in form.dateRanges %}
|
||||
<div class="row gx-2 justify-content-center">
|
||||
{% if form.dateRanges[dateRangeName].vars.label is not same as(false) %}
|
||||
<div class="col-md-5">
|
||||
{{ form_label(form.dateRanges[dateRangeName])}}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-md-6">
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">{{ 'chill_calendar.From'|trans }}</span>
|
||||
{{ form_widget(form.dateRanges[dateRangeName]['from']) }}
|
||||
<span class="input-group-text">{{ 'chill_calendar.To'|trans }}</span>
|
||||
{{ form_widget(form.dateRanges[dateRangeName]['to']) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="submit" class="btn btn-misc"><i class="fa fa-filter"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if form.checkboxes is defined %}
|
||||
{% if form.checkboxes|length > 0 %}
|
||||
{% for checkbox_name, options in form.checkboxes %}
|
||||
|
@@ -233,8 +233,21 @@
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block pick_entity_dynamic_row %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{{ form_label(form) }}
|
||||
{{ form_help(form) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-end">
|
||||
<div class="col-md-7 col-sm-12">
|
||||
{{ form_widget(form) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block pick_entity_dynamic_widget %}
|
||||
{{ form_help(form)}}
|
||||
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}"/>
|
||||
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value|escape('html_attr') }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}"/>
|
||||
<div data-module="pick-dynamic" data-types="{{ form.vars['types']|json_encode }}" data-multiple="{{ form.vars['multiple'] }}" data-uniqid="{{ form.vars['uniqid'] }}"></div>
|
||||
{% endblock %}
|
||||
|
@@ -72,6 +72,7 @@ class DateNormalizer implements ContextAwareNormalizerInterface, DenormalizerInt
|
||||
case 'json':
|
||||
return [
|
||||
'datetime' => $date->format(DateTimeInterface::ISO8601),
|
||||
'datetime8601' => $date->format(DateTimeInterface::ATOM),
|
||||
];
|
||||
|
||||
case 'docgen':
|
||||
|
@@ -0,0 +1,19 @@
|
||||
<?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\ShortMessage;
|
||||
|
||||
class NullShortMessageSender implements ShortMessageSenderInterface
|
||||
{
|
||||
public function send(ShortMessage $shortMessage): void
|
||||
{
|
||||
}
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
<?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\ShortMessage;
|
||||
|
||||
use libphonenumber\PhoneNumber;
|
||||
|
||||
class ShortMessage
|
||||
{
|
||||
public const PRIORITY_LOW = 'low';
|
||||
|
||||
public const PRIORITY_MEDIUM = 'medium';
|
||||
|
||||
private string $content;
|
||||
|
||||
private PhoneNumber $phoneNumber;
|
||||
|
||||
private string $priority = 'low';
|
||||
|
||||
public function __construct(string $content, PhoneNumber $phoneNumber, string $priority = 'low')
|
||||
{
|
||||
$this->content = $content;
|
||||
$this->phoneNumber = $phoneNumber;
|
||||
$this->priority = $priority;
|
||||
}
|
||||
|
||||
public function getContent(): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function getPhoneNumber(): PhoneNumber
|
||||
{
|
||||
return $this->phoneNumber;
|
||||
}
|
||||
|
||||
public function getPriority(): string
|
||||
{
|
||||
return $this->priority;
|
||||
}
|
||||
|
||||
public function setContent(string $content): self
|
||||
{
|
||||
$this->content = $content;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setPhoneNumber(PhoneNumber $phoneNumber): self
|
||||
{
|
||||
$this->phoneNumber = $phoneNumber;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setPriority(string $priority): self
|
||||
{
|
||||
$this->priority = $priority;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
<?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\ShortMessage;
|
||||
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
|
||||
/**
|
||||
* @AsMessageHandler
|
||||
*/
|
||||
class ShortMessageHandler implements MessageHandlerInterface
|
||||
{
|
||||
private ShortMessageTransporterInterface $messageTransporter;
|
||||
|
||||
public function __construct(ShortMessageTransporterInterface $messageTransporter)
|
||||
{
|
||||
$this->messageTransporter = $messageTransporter;
|
||||
}
|
||||
|
||||
public function __invoke(ShortMessage $message): void
|
||||
{
|
||||
$this->messageTransporter->send($message);
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
<?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\ShortMessage;
|
||||
|
||||
interface ShortMessageSenderInterface
|
||||
{
|
||||
public function send(ShortMessage $shortMessage): void;
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
<?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\ShortMessage;
|
||||
|
||||
class ShortMessageTransporter implements ShortMessageTransporterInterface
|
||||
{
|
||||
private ShortMessageSenderInterface $sender;
|
||||
|
||||
public function __construct(
|
||||
ShortMessageSenderInterface $sender // hint: must remain at place 0 for DI
|
||||
) {
|
||||
$this->sender = $sender;
|
||||
}
|
||||
|
||||
public function send(ShortMessage $shortMessage): void
|
||||
{
|
||||
$this->sender->send($shortMessage);
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
<?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\ShortMessage;
|
||||
|
||||
interface ShortMessageTransporterInterface
|
||||
{
|
||||
public function send(ShortMessage $shortMessage);
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
<?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\ShortMessageOvh;
|
||||
|
||||
use Chill\MainBundle\Service\ShortMessage\ShortMessage;
|
||||
use Chill\MainBundle\Service\ShortMessage\ShortMessageSenderInterface;
|
||||
use libphonenumber\PhoneNumberFormat;
|
||||
use libphonenumber\PhoneNumberUtil;
|
||||
use Ovh\Api;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class OvhShortMessageSender implements ShortMessageSenderInterface
|
||||
{
|
||||
private Api $api;
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private PhoneNumberUtil $phoneNumberUtil;
|
||||
|
||||
private string $sender;
|
||||
|
||||
private string $serviceName;
|
||||
|
||||
public function __construct(
|
||||
Api $api, // for DI, must remains as first argument
|
||||
string $serviceName, // for di, must remains as second argument
|
||||
string $sender, // for DI, must remains as third argument
|
||||
LoggerInterface $logger,
|
||||
PhoneNumberUtil $phoneNumberUtil
|
||||
) {
|
||||
$this->api = $api;
|
||||
$this->serviceName = $serviceName;
|
||||
$this->sender = $sender;
|
||||
$this->logger = $logger;
|
||||
$this->phoneNumberUtil = $phoneNumberUtil;
|
||||
}
|
||||
|
||||
public function send(ShortMessage $shortMessage): void
|
||||
{
|
||||
$receiver = $this->phoneNumberUtil->format($shortMessage->getPhoneNumber(), PhoneNumberFormat::E164);
|
||||
|
||||
$response = $this->api->post(
|
||||
strtr('/sms/{serviceName}/jobs', ['{serviceName}' => $this->serviceName]),
|
||||
[
|
||||
'message' => $shortMessage->getContent(),
|
||||
'receivers' => [$receiver],
|
||||
'sender' => $this->sender,
|
||||
'noStopClause' => true,
|
||||
'coding' => '7bit',
|
||||
'charset' => 'UTF-8',
|
||||
'priority' => $shortMessage->getPriority(),
|
||||
]
|
||||
);
|
||||
|
||||
$improved = array_merge([
|
||||
'validReceiversI' => implode(',', $response['validReceivers']),
|
||||
'idsI' => implode(',', $response['ids']),
|
||||
], $response);
|
||||
|
||||
$this->logger->warning('[sms] a sms was sent', $improved);
|
||||
}
|
||||
}
|
@@ -12,7 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Templating\Entity;
|
||||
|
||||
use Chill\MainBundle\Entity\Address;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Symfony\Component\Templating\EngineInterface;
|
||||
|
||||
use function array_merge;
|
||||
@@ -33,9 +33,9 @@ class AddressRender implements ChillEntityRenderInterface
|
||||
|
||||
private EngineInterface $templating;
|
||||
|
||||
private TranslatableStringHelper $translatableStringHelper;
|
||||
private TranslatableStringHelperInterface $translatableStringHelper;
|
||||
|
||||
public function __construct(EngineInterface $templating, TranslatableStringHelper $translatableStringHelper)
|
||||
public function __construct(EngineInterface $templating, TranslatableStringHelperInterface $translatableStringHelper)
|
||||
{
|
||||
$this->templating = $templating;
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
@@ -65,7 +65,7 @@ class AddressRender implements ChillEntityRenderInterface
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function renderLines($addr): array
|
||||
public function renderLines(Address $addr, bool $includeCityLine = true, bool $includeCountry = true): array
|
||||
{
|
||||
$lines = [];
|
||||
|
||||
@@ -75,14 +75,26 @@ class AddressRender implements ChillEntityRenderInterface
|
||||
$lines[] = $this->renderBuildingLine($addr);
|
||||
$lines[] = $this->renderStreetLine($addr);
|
||||
$lines[] = $this->renderDeliveryLine($addr);
|
||||
$lines[] = $this->renderCityLine($addr);
|
||||
$lines[] = $this->renderCountryLine($addr);
|
||||
|
||||
if ($includeCityLine) {
|
||||
$lines[] = $this->renderCityLine($addr);
|
||||
}
|
||||
|
||||
if ($includeCountry) {
|
||||
$lines[] = $this->renderCountryLine($addr);
|
||||
}
|
||||
} else {
|
||||
$lines[] = $this->renderBuildingLine($addr);
|
||||
$lines[] = $this->renderDeliveryLine($addr);
|
||||
$lines[] = $this->renderStreetLine($addr);
|
||||
$lines[] = $this->renderCityLine($addr);
|
||||
$lines[] = $this->renderCountryLine($addr);
|
||||
|
||||
if ($includeCityLine) {
|
||||
$lines[] = $this->renderCityLine($addr);
|
||||
}
|
||||
|
||||
if ($includeCountry) {
|
||||
$lines[] = $this->renderCountryLine($addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Templating\Listing;
|
||||
|
||||
use Chill\MainBundle\Form\Type\Listing\FilterOrderType;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
@@ -23,6 +24,8 @@ class FilterOrderHelper
|
||||
{
|
||||
private array $checkboxes = [];
|
||||
|
||||
private array $dateRanges = [];
|
||||
|
||||
private FormFactoryInterface $formFactory;
|
||||
|
||||
private ?string $formName = 'f';
|
||||
@@ -60,6 +63,13 @@ class FilterOrderHelper
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addDateRange(string $name, ?string $label = null, ?DateTimeImmutable $from = null, ?DateTimeImmutable $to = null): self
|
||||
{
|
||||
$this->dateRanges[$name] = ['from' => $from, 'to' => $to, 'label' => $label];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function buildForm(): FormInterface
|
||||
{
|
||||
return $this->formFactory
|
||||
@@ -81,6 +91,19 @@ class FilterOrderHelper
|
||||
return $this->checkboxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<'to': DateTimeImmutable, 'from': DateTimeImmutable>
|
||||
*/
|
||||
public function getDateRangeData(string $name): array
|
||||
{
|
||||
return $this->getFormData()['dateRanges'][$name];
|
||||
}
|
||||
|
||||
public function getDateRanges(): array
|
||||
{
|
||||
return $this->dateRanges;
|
||||
}
|
||||
|
||||
public function getQueryString(): ?string
|
||||
{
|
||||
return $this->getFormData()['q'];
|
||||
@@ -110,6 +133,11 @@ class FilterOrderHelper
|
||||
$r['checkboxes'][$name] = $c['default'];
|
||||
}
|
||||
|
||||
foreach ($this->dateRanges as $name => $defaults) {
|
||||
$r['dateRanges'][$name]['from'] = $defaults['from'];
|
||||
$r['dateRanges'][$name]['to'] = $defaults['to'];
|
||||
}
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
|
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Templating\Listing;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
@@ -18,6 +19,8 @@ class FilterOrderHelperBuilder
|
||||
{
|
||||
private array $checkboxes = [];
|
||||
|
||||
private array $dateRanges = [];
|
||||
|
||||
private FormFactoryInterface $formFactory;
|
||||
|
||||
private RequestStack $requestStack;
|
||||
@@ -39,6 +42,13 @@ class FilterOrderHelperBuilder
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addDateRange(string $name, ?string $label = null, ?DateTimeImmutable $from = null, ?DateTimeImmutable $to = null): self
|
||||
{
|
||||
$this->dateRanges[$name] = ['from' => $from, 'to' => $to, 'label' => $label];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addSearchBox(?array $fields = [], ?array $options = []): self
|
||||
{
|
||||
$this->searchBoxFields = $fields;
|
||||
@@ -65,6 +75,16 @@ class FilterOrderHelperBuilder
|
||||
$helper->addCheckbox($name, $choices, $default, $trans);
|
||||
}
|
||||
|
||||
foreach (
|
||||
$this->dateRanges as $name => [
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
'label' => $label,
|
||||
]
|
||||
) {
|
||||
$helper->addDateRange($name, $label, $from, $to);
|
||||
}
|
||||
|
||||
return $helper;
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,118 @@
|
||||
<?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 Form\DataTransformer;
|
||||
|
||||
use Chill\MainBundle\Form\DataTransformer\IdToEntityDataTransformer;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
final class IdToEntityDataTransformerTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testReverseTransformMulti()
|
||||
{
|
||||
$o1 = new stdClass();
|
||||
$o2 = new stdClass();
|
||||
|
||||
$repository = $this->prophesize(ObjectRepository::class);
|
||||
$repository->findOneBy(Argument::exact(['id' => 1]))->willReturn($o1);
|
||||
$repository->findOneBy(Argument::exact(['id' => 2]))->willReturn($o2);
|
||||
|
||||
$transformer = new IdToEntityDataTransformer(
|
||||
$repository->reveal(),
|
||||
true
|
||||
);
|
||||
|
||||
$this->assertEquals([], $transformer->reverseTransform(null));
|
||||
$this->assertEquals([], $transformer->reverseTransform(''));
|
||||
$r = $transformer->reverseTransform('1,2');
|
||||
|
||||
$this->assertIsArray($r);
|
||||
$this->assertSame($o1, $r[0]);
|
||||
$this->assertSame($o2, $r[1]);
|
||||
}
|
||||
|
||||
public function testReverseTransformSingle()
|
||||
{
|
||||
$o = new stdClass();
|
||||
|
||||
$repository = $this->prophesize(ObjectRepository::class);
|
||||
$repository->findOneBy(Argument::exact(['id' => 1]))->willReturn($o);
|
||||
|
||||
$transformer = new IdToEntityDataTransformer(
|
||||
$repository->reveal(),
|
||||
false
|
||||
);
|
||||
|
||||
$this->assertEquals(null, $transformer->reverseTransform(null));
|
||||
$this->assertEquals(null, $transformer->reverseTransform(''));
|
||||
$r = $transformer->reverseTransform('1');
|
||||
|
||||
$this->assertSame($o, $r);
|
||||
}
|
||||
|
||||
public function testTransformMulti()
|
||||
{
|
||||
$o1 = new class() {
|
||||
public function getId()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
$o2 = new class() {
|
||||
public function getId()
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
};
|
||||
$repository = $this->prophesize(ObjectRepository::class);
|
||||
|
||||
$transformer = new IdToEntityDataTransformer(
|
||||
$repository->reveal(),
|
||||
true
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
'1,2',
|
||||
$transformer->transform([$o1, $o2])
|
||||
);
|
||||
}
|
||||
|
||||
public function testTransformSingle()
|
||||
{
|
||||
$o = new class() {
|
||||
public function getId()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
$repository = $this->prophesize(ObjectRepository::class);
|
||||
|
||||
$transformer = new IdToEntityDataTransformer(
|
||||
$repository->reveal(),
|
||||
false
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
'1',
|
||||
$transformer->transform($o)
|
||||
);
|
||||
}
|
||||
}
|
@@ -26,6 +26,7 @@ services:
|
||||
tags:
|
||||
- { name: 'doctrine.event_subscriber' }
|
||||
|
||||
|
||||
# workflow related
|
||||
Chill\MainBundle\Workflow\:
|
||||
resource: '../Workflow/'
|
||||
|
@@ -147,3 +147,7 @@ services:
|
||||
Chill\MainBundle\Form\DataMapper\PrivateCommentDataMapper:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\MainBundle\Form\DataTransformer\IdToLocationDataTransformer: ~
|
||||
Chill\MainBundle\Form\DataTransformer\IdToUserDataTransformer: ~
|
||||
Chill\MainBundle\Form\DataTransformer\IdToUsersDataTransformer: ~
|
||||
|
@@ -0,0 +1,5 @@
|
||||
services:
|
||||
Chill\MainBundle\Service\ShortMessage\:
|
||||
resource: '../Service/ShortMessage'
|
||||
autowire: true
|
||||
autoconfigure: true
|
@@ -0,0 +1,35 @@
|
||||
<?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 Version20220506223243 extends AbstractMigration
|
||||
{
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE users ALTER attributes DROP NOT NULL');
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Force user attribute to be an array';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('UPDATE users SET attributes = \'{}\'::jsonb WHERE attributes IS NULL');
|
||||
$this->addSql('ALTER TABLE users ALTER attributes SET NOT NULL');
|
||||
$this->addSql('ALTER TABLE users ALTER attributes SET DEFAULT \'{}\'::jsonb');
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user