Merge remote-tracking branch 'origin/master' into issue520_reference_address_update

This commit is contained in:
2022-05-06 14:47:15 +02:00
486 changed files with 4991 additions and 41741 deletions

View File

@@ -16,9 +16,9 @@ use Chill\MainBundle\Serializer\Model\Collection;
use Exception;
use LogicException;
use RuntimeException;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
@@ -33,11 +33,11 @@ class ApiController extends AbstractCRUDController
* Base method for handling api action.
*
* @param mixed $id
* @param mixed $_format
* @param string $_format
*
* @return void
*/
public function entityApi(Request $request, $id, $_format): Response
public function entityApi(Request $request, $id, ?string $_format = 'json'): Response
{
switch ($request->getMethod()) {
case Request::METHOD_GET:
@@ -55,7 +55,7 @@ class ApiController extends AbstractCRUDController
return $this->entityDelete('_entity', $request, $id, $_format);
default:
throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException('This method is not implemented');
throw new BadRequestHttpException('This method is not implemented');
}
}
@@ -120,7 +120,7 @@ class ApiController extends AbstractCRUDController
return $this->entityPostAction('_entity', $request, $_format);
default:
throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException('This method is not implemented');
throw new BadRequestHttpException('This method is not implemented');
}
}
@@ -160,7 +160,7 @@ class ApiController extends AbstractCRUDController
try {
$entity = $this->deserialize($action, $request, $_format, $entity);
} catch (NotEncodableValueException $e) {
throw new BadRequestException('invalid json', 400, $e);
throw new BadRequestHttpException('invalid json', 400, $e);
}
$errors = $this->validate($action, $request, $_format, $entity);
@@ -273,7 +273,7 @@ class ApiController extends AbstractCRUDController
try {
$postedData = $this->getSerializer()->deserialize($request->getContent(), $postedDataType, $_format, $postedDataContext);
} catch (\Symfony\Component\Serializer\Exception\UnexpectedValueException $e) {
throw new BadRequestException(sprintf('Unable to deserialize posted ' .
throw new BadRequestHttpException(sprintf('Unable to deserialize posted ' .
'data: %s', $e->getMessage()), 0, $e);
}
@@ -290,7 +290,7 @@ class ApiController extends AbstractCRUDController
break;
default:
throw new BadRequestException('this method is not supported');
throw new BadRequestHttpException('this method is not supported');
}
$errors = $this->validate($action, $request, $_format, $entity, [$postedData]);
@@ -408,7 +408,7 @@ class ApiController extends AbstractCRUDController
return $this->json($entity, Response::HTTP_OK, [], $context);
}
throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException('This format is not implemented');
throw new BadRequestHttpException('This format is not implemented');
}
protected function entityPostAction($action, Request $request, string $_format): Response
@@ -418,7 +418,7 @@ class ApiController extends AbstractCRUDController
try {
$entity = $this->deserialize($action, $request, $_format, $entity);
} catch (NotEncodableValueException $e) {
throw new BadRequestException('invalid json', 400, $e);
throw new BadRequestHttpException('invalid json', 400, $e);
}
$errors = $this->validate($action, $request, $_format, $entity);

View File

@@ -25,6 +25,7 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -1140,6 +1141,6 @@ class CRUDController extends AbstractController
return $this->json($entity, Response::HTTP_OK, [], $context);
}
throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException('This format is not implemented');
throw new BadRequestHttpException('This format is not implemented');
}
}

View File

@@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Command;
use Chill\MainBundle\Entity\Language;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
@@ -113,7 +114,7 @@ class LoadAndUpdateLanguagesCommand extends Command
)
);
$langageDB = $em->getRepository('ChillMainBundle:Language')->find($code);
$langageDB = $em->getRepository(Language::class)->find($code);
if (!$excludeCode) {
if (!$langageDB) {

View File

@@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Command;
use Chill\MainBundle\Entity\Country;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
@@ -87,7 +88,7 @@ class LoadCountriesCommand extends Command
$em = $this->entityManager;
foreach ($countries as $country) {
$countryStored = $em->getRepository('ChillMainBundle:Country')
$countryStored = $em->getRepository(Country::class)
->findOneBy(['countryCode' => $country->getCountryCode()]);
if (null === $countryStored) {

View File

@@ -43,7 +43,7 @@ class SetPasswordCommand extends Command
public function _getUser($username)
{
return $this->entityManager
->getRepository('ChillMainBundle:User')
->getRepository(\Chill\MainBundle\Entity\User::class)
->findOneBy(['username' => $username]);
}

View File

@@ -54,7 +54,7 @@ class CenterController extends AbstractController
{
$em = $this->getDoctrine()->getManager();
$center = $em->getRepository('ChillMainBundle:Center')->find($id);
$center = $em->getRepository(\Chill\MainBundle\Entity\Center::class)->find($id);
if (!$center) {
throw $this->createNotFoundException('Unable to find Center entity.');
@@ -75,7 +75,7 @@ class CenterController extends AbstractController
{
$em = $this->getDoctrine()->getManager();
$entities = $em->getRepository('ChillMainBundle:Center')->findAll();
$entities = $em->getRepository(\Chill\MainBundle\Entity\Center::class)->findAll();
return $this->render('@ChillMain/Center/index.html.twig', [
'entities' => $entities,
@@ -105,7 +105,7 @@ class CenterController extends AbstractController
{
$em = $this->getDoctrine()->getManager();
$center = $em->getRepository('ChillMainBundle:Center')->find($id);
$center = $em->getRepository(\Chill\MainBundle\Entity\Center::class)->find($id);
if (!$center) {
throw $this->createNotFoundException('Unable to find Center entity.');
@@ -125,7 +125,7 @@ class CenterController extends AbstractController
{
$em = $this->getDoctrine()->getManager();
$center = $em->getRepository('ChillMainBundle:Center')->find($id);
$center = $em->getRepository(\Chill\MainBundle\Entity\Center::class)->find($id);
if (!$center) {
throw $this->createNotFoundException('Unable to find Center entity.');

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\CRUDController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Symfony\Component\HttpFoundation\Request;
class LocationController extends CRUDController
@@ -29,4 +30,9 @@ class LocationController extends CRUDController
{
$query->where('e.availableForUsers = true'); //TODO not working
}
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
{
return $query->addOrderBy('e.name', 'DESC');
}
}

View File

@@ -22,6 +22,7 @@ use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\NotificationRepository;
use Chill\MainBundle\Security\Authorization\NotificationVoter;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
@@ -31,14 +32,19 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface;
use function in_array;
/**
* @Route("/{_locale}/notification")
*/
class NotificationController extends AbstractController
{
private LoggerInterface $chillLogger;
private EntityManagerInterface $em;
private LoggerInterface $logger;
private NotificationHandlerManager $notificationHandlerManager;
private NotificationRepository $notificationRepository;
@@ -51,6 +57,8 @@ class NotificationController extends AbstractController
public function __construct(
EntityManagerInterface $em,
LoggerInterface $chillLogger,
LoggerInterface $logger,
Security $security,
NotificationRepository $notificationRepository,
NotificationHandlerManager $notificationHandlerManager,
@@ -58,6 +66,8 @@ class NotificationController extends AbstractController
TranslatorInterface $translator
) {
$this->em = $em;
$this->logger = $logger;
$this->chillLogger = $chillLogger;
$this->security = $security;
$this->notificationRepository = $notificationRepository;
$this->notificationHandlerManager = $notificationHandlerManager;
@@ -150,6 +160,52 @@ class NotificationController extends AbstractController
]);
}
/**
* @Route("/{id}/access_key", name="chill_main_notification_grant_access_by_access_key")
*/
public function getAccessByAccessKey(Notification $notification, Request $request): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
if (!$this->security->getUser() instanceof User) {
throw new AccessDeniedHttpException('You must be authenticated and a user to create a notification');
}
foreach (['accessKey'/*, 'email'*/] as $param) {
if (!$request->query->has($param)) {
throw new BadRequestHttpException("Missing {$param} parameter");
}
}
if ($notification->getAccessKey() !== $request->query->getAlnum('accessKey')) {
throw new AccessDeniedHttpException('access key is invalid');
}
/*
desactivated due to escaped '&' in email links
if (!in_array($request->query->get('email'), $notification->getAddressesEmails(), true)) {
return (new Response('The email address is no more associated with this notification'))
->setStatusCode(Response::HTTP_FORBIDDEN);
}
*/
$notification->addAddressee($this->security->getUser());
$this->getDoctrine()->getManager()->flush();
$logMsg = '[Notification] a user is granted access to notification trough an access key';
$context = [
'notificationId' => $notification->getId(),
'email' => $request->query->get('email'),
'user' => $this->security->getUser()->getId(),
];
$this->logger->info($logMsg, $context);
$this->chillLogger->info($logMsg, $context);
return $this->redirectToRoute('chill_main_notification_show', ['id' => $notification->getId()]);
}
/**
* @Route("/inbox", name="chill_main_notification_my")
*/

View File

@@ -87,7 +87,7 @@ class PermissionsGroupController extends AbstractController
{
$em = $this->getDoctrine()->getManager();
$permissionsGroup = $em->getRepository('ChillMainBundle:PermissionsGroup')->find($id);
$permissionsGroup = $em->getRepository(\Chill\MainBundle\Entity\PermissionsGroup::class)->find($id);
if (!$permissionsGroup) {
throw $this->createNotFoundException('Unable to find PermissionsGroup entity.');
@@ -203,8 +203,8 @@ class PermissionsGroupController extends AbstractController
{
$em = $this->getDoctrine()->getManager();
$permissionsGroup = $em->getRepository('ChillMainBundle:PermissionsGroup')->find($pgid);
$roleScope = $em->getRepository('ChillMainBundle:RoleScope')->find($rsid);
$permissionsGroup = $em->getRepository(\Chill\MainBundle\Entity\PermissionsGroup::class)->find($pgid);
$roleScope = $em->getRepository(\Chill\MainBundle\Entity\RoleScope::class)->find($rsid);
if (!$permissionsGroup) {
throw $this->createNotFoundException('Unable to find PermissionsGroup entity.');
@@ -269,7 +269,7 @@ class PermissionsGroupController extends AbstractController
{
$em = $this->getDoctrine()->getManager();
$permissionsGroup = $em->getRepository('ChillMainBundle:PermissionsGroup')->find($id);
$permissionsGroup = $em->getRepository(\Chill\MainBundle\Entity\PermissionsGroup::class)->find($id);
if (!$permissionsGroup) {
throw $this->createNotFoundException('Unable to find PermissionsGroup entity.');
@@ -319,7 +319,7 @@ class PermissionsGroupController extends AbstractController
{
$em = $this->getDoctrine()->getManager();
$entities = $em->getRepository('ChillMainBundle:PermissionsGroup')->findAll();
$entities = $em->getRepository(\Chill\MainBundle\Entity\PermissionsGroup::class)->findAll();
return $this->render('@ChillMain/PermissionsGroup/index.html.twig', [
'entities' => $entities,
@@ -349,7 +349,7 @@ class PermissionsGroupController extends AbstractController
{
$em = $this->getDoctrine()->getManager();
$permissionsGroup = $em->getRepository('ChillMainBundle:PermissionsGroup')->find($id);
$permissionsGroup = $em->getRepository(\Chill\MainBundle\Entity\PermissionsGroup::class)->find($id);
if (!$permissionsGroup) {
throw $this->createNotFoundException('Unable to find PermissionsGroup entity.');
@@ -405,7 +405,7 @@ class PermissionsGroupController extends AbstractController
$em = $this->getDoctrine()->getManager();
$permissionsGroup = $em
->getRepository('ChillMainBundle:PermissionsGroup')
->getRepository(\Chill\MainBundle\Entity\PermissionsGroup::class)
->find($id);
if (!$permissionsGroup) {
@@ -469,7 +469,7 @@ class PermissionsGroupController extends AbstractController
{
$em = $this->getDoctrine()->getManager();
$roleScope = $em->getRepository('ChillMainBundle:RoleScope')
$roleScope = $em->getRepository(\Chill\MainBundle\Entity\RoleScope::class)
->findOneBy(['role' => $role, 'scope' => $scope]);
if (null === $roleScope) {

View File

@@ -54,7 +54,7 @@ class ScopeController extends AbstractController
{
$em = $this->getDoctrine()->getManager();
$scope = $em->getRepository('ChillMainBundle:Scope')->find($id);
$scope = $em->getRepository(\Chill\MainBundle\Entity\Scope::class)->find($id);
if (!$scope) {
throw $this->createNotFoundException('Unable to find Scope entity.');
@@ -75,7 +75,7 @@ class ScopeController extends AbstractController
{
$em = $this->getDoctrine()->getManager();
$entities = $em->getRepository('ChillMainBundle:Scope')->findAll();
$entities = $em->getRepository(\Chill\MainBundle\Entity\Scope::class)->findAll();
return $this->render('@ChillMain/Scope/index.html.twig', [
'entities' => $entities,
@@ -105,7 +105,7 @@ class ScopeController extends AbstractController
{
$em = $this->getDoctrine()->getManager();
$scope = $em->getRepository('ChillMainBundle:Scope')->find($id);
$scope = $em->getRepository(\Chill\MainBundle\Entity\Scope::class)->find($id);
if (!$scope) {
throw $this->createNotFoundException('Unable to find Scope entity.');
@@ -125,7 +125,7 @@ class ScopeController extends AbstractController
{
$em = $this->getDoctrine()->getManager();
$scope = $em->getRepository('ChillMainBundle:Scope')->find($id);
$scope = $em->getRepository(\Chill\MainBundle\Entity\Scope::class)->find($id);
if (!$scope) {
throw $this->createNotFoundException('Unable to find Scope entity.');

View File

@@ -22,7 +22,6 @@ use Chill\MainBundle\Search\UnknowSearchNameException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
@@ -122,7 +121,7 @@ class SearchController extends AbstractController
public function searchAction(Request $request, $_format)
{
$pattern = $request->query->get('q', '');
$pattern = trim($request->query->get('q', ''));
if ('' === $pattern) {
switch ($_format) {
@@ -216,7 +215,7 @@ class SearchController extends AbstractController
$types = $request->query->get('type', []);
if (count($types) === 0) {
throw new BadRequestException('The request must contains at '
throw new BadRequestHttpException('The request must contains at '
. ' one type');
}

View File

@@ -61,7 +61,7 @@ class UserController extends CRUDController
{
$em = $this->getDoctrine()->getManager();
$user = $em->getRepository('ChillMainBundle:User')->find($uid);
$user = $em->getRepository(\Chill\MainBundle\Entity\User::class)->find($uid);
if (!$user) {
throw $this->createNotFoundException('Unable to find User entity.');
@@ -118,13 +118,13 @@ class UserController extends CRUDController
{
$em = $this->getDoctrine()->getManager();
$user = $em->getRepository('ChillMainBundle:User')->find($uid);
$user = $em->getRepository(\Chill\MainBundle\Entity\User::class)->find($uid);
if (!$user) {
throw $this->createNotFoundException('Unable to find User entity.');
}
$groupCenter = $em->getRepository('ChillMainBundle:GroupCenter')
$groupCenter = $em->getRepository(\Chill\MainBundle\Entity\GroupCenter::class)
->find($gcid);
if (!$groupCenter) {
@@ -330,7 +330,7 @@ class UserController extends CRUDController
{
$em = $this->getDoctrine()->getManager();
$groupCenterManaged = $em->getRepository('ChillMainBundle:GroupCenter')
$groupCenterManaged = $em->getRepository(\Chill\MainBundle\Entity\GroupCenter::class)
->findOneBy([
'center' => $groupCenter->getCenter(),
'permissionsGroup' => $groupCenter->getPermissionsGroup(),

View File

@@ -25,7 +25,6 @@ use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
@@ -144,7 +143,7 @@ class WorkflowController extends AbstractController
public function getAccessByAccessKey(EntityWorkflowStep $entityWorkflowStep, Request $request): Response
{
if (null === $accessKey = $request->query->get('accessKey', null)) {
throw new BadRequestException('accessKey is missing');
throw new BadRequestHttpException('accessKey is missing');
}
if (!$this->getUser() instanceof User) {

View File

@@ -16,17 +16,20 @@ use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
trait TrackCreationTrait
{
/**
* @ORM\Column(type="datetime_immutable", nullable=true, options={"default": NULL})
* @Serializer\Groups({"read"})
*/
private ?DateTimeImmutable $createdAt = null;
/**
* @ORM\ManyToOne(targetEntity=User::class)
* @ORM\JoinColumn(nullable=true)
* @Serializer\Groups({"read"})
*/
private ?User $createdBy = null;

View File

@@ -16,17 +16,20 @@ use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
trait TrackUpdateTrait
{
/**
* @ORM\Column(type="datetime_immutable", nullable=true, options={"default": NULL})
* @Serializer\Groups({"read"})
*/
private ?DateTimeImmutable $updatedAt = null;
/**
* @ORM\ManyToOne(targetEntity=User::class)
* @ORM\JoinColumn(nullable=true)
* @Serializer\Groups({"read"})
*/
private ?User $updatedBy = null;

View File

@@ -140,6 +140,7 @@ class Address
/**
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode")
* @ORM\JoinColumn(nullable=false)
* @Groups({"write"})
*/
private ?PostalCode $postcode = null;
@@ -480,8 +481,11 @@ class Address
return $this;
}
public function setStreet(string $street): self
public function setStreet(?string $street): self
{
if (null === $street) {
$street = '';
}
$this->street = $street;
return $this;
@@ -515,8 +519,11 @@ class Address
return $this;
}
public function setStreetNumber(string $streetNumber): self
public function setStreetNumber(?string $streetNumber): self
{
if (null === $streetNumber) {
$streetNumber = '';
}
$this->streetNumber = $streetNumber;
return $this;

View File

@@ -18,6 +18,9 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use function count;
use function in_array;
/**
* @ORM\Entity
@@ -31,15 +34,34 @@ use Symfony\Component\Validator\Constraints as Assert;
*/
class Notification implements TrackUpdateInterface
{
/**
* @ORM\Column(type="text", nullable=false)
*/
private string $accessKey;
private array $addedAddresses = [];
/**
* @ORM\ManyToMany(targetEntity=User::class)
* @ORM\JoinTable(name="chill_main_notification_addresses_user")
* @Assert\Count(min="1", minMessage="notification.At least one addressee")
*/
private Collection $addressees;
/**
* a list of destinee which will receive notifications.
*
* @var array|string[]
* @ORM\Column(type="json")
*/
private array $addressesEmails = [];
/**
* a list of emails adresses which were added to the notification.
*
* @var array|string[]
*/
private array $addressesEmailsAdded = [];
private ?ArrayCollection $addressesOnLoad = null;
/**
@@ -111,6 +133,7 @@ class Notification implements TrackUpdateInterface
$this->unreadBy = new ArrayCollection();
$this->comments = new ArrayCollection();
$this->setDate(new DateTimeImmutable());
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(24));
}
public function addAddressee(User $addressee): self
@@ -123,6 +146,14 @@ class Notification implements TrackUpdateInterface
return $this;
}
public function addAddressesEmail(string $email)
{
if (!in_array($email, $this->addressesEmails, true)) {
$this->addressesEmails[] = $email;
$this->addressesEmailsAdded[] = $email;
}
}
public function addComment(NotificationComment $comment): self
{
if (!$this->comments->contains($comment)) {
@@ -142,6 +173,30 @@ class Notification implements TrackUpdateInterface
return $this;
}
/**
* @Assert\Callback
*
* @param array $payload
*/
public function assertCountAddresses(ExecutionContextInterface $context, $payload): void
{
if (0 === (count($this->getAddressesEmails()) + count($this->getAddressees()))) {
$context->buildViolation('notification.At least one addressee')
->atPath('addressees')
->addViolation();
}
}
public function getAccessKey(): string
{
return $this->accessKey;
}
public function getAddedAddresses(): array
{
return $this->addedAddresses;
}
/**
* @return Collection|User[]
*/
@@ -155,6 +210,22 @@ class Notification implements TrackUpdateInterface
return $this->addressees;
}
/**
* @return array|string[]
*/
public function getAddressesEmails(): array
{
return $this->addressesEmails;
}
/**
* @return array|string[]
*/
public function getAddressesEmailsAdded(): array
{
return $this->addressesEmailsAdded;
}
public function getComments(): Collection
{
return $this->comments;
@@ -271,6 +342,14 @@ class Notification implements TrackUpdateInterface
return $this;
}
public function removeAddressesEmail(string $email)
{
if (in_array($email, $this->addressesEmails, true)) {
$this->addressesEmails = array_filter($this->addressesEmails, static fn ($e) => $e !== $email);
$this->addressesEmailsAdded = array_filter($this->addressesEmailsAdded, static fn ($e) => $e !== $email);
}
}
public function removeComment(NotificationComment $comment): self
{
$this->comments->removeElement($comment);

View File

@@ -160,6 +160,8 @@ class EntityWorkflowStep
/**
* get all the users which are allowed to apply a transition: those added manually, and
* those added automatically bu using an access key.
*
* @psalm-suppress DuplicateArrayKey
*/
public function getAllDestUser(): Collection
{

View File

@@ -12,12 +12,17 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Form\Type\ChillCollectionType;
use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull;
class NotificationType extends AbstractType
{
@@ -30,9 +35,27 @@ class NotificationType extends AbstractType
])
->add('addressees', PickUserDynamicType::class, [
'multiple' => true,
'required' => false,
])
->add('message', ChillTextareaType::class, [
'required' => false,
])
->add('addressesEmails', ChillCollectionType::class, [
'label' => 'notification.dest by email',
'help' => 'notification.dest by email help',
'by_reference' => false,
'allow_add' => true,
'allow_delete' => true,
'entry_type' => EmailType::class,
'button_add_label' => 'notification.Add an email',
'button_remove_label' => 'notification.Remove an email',
'empty_collection_explain' => 'notification.Any email',
'entry_options' => [
'constraints' => [
new NotNull(), new NotBlank(), new Email(['checkMX' => true]),
],
'label' => 'Email',
],
]);
}

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\MainBundle\Form\Type;
use Chill\MainBundle\Entity\Address;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AddressDateType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'validFrom',
ChillDateType::class,
[
'required' => true,
]
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('data_class', Address::class);
}
}

View File

@@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form\Type;
use Chill\MainBundle\Entity\Country;
use Chill\MainBundle\Form\Type\DataTransformer\ObjectToIdTransformer;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\Persistence\ObjectManager;
@@ -50,13 +51,13 @@ class Select2CountryType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new ObjectToIdTransformer($this->em, 'Chill\MainBundle\Entity\Country');
$transformer = new ObjectToIdTransformer($this->em, Country::class);
$builder->addModelTransformer($transformer);
}
public function configureOptions(OptionsResolver $resolver)
{
$countries = $this->em->getRepository('Chill\MainBundle\Entity\Country')->findAll();
$countries = $this->em->getRepository(Country::class)->findAll();
$choices = [];
$preferredCountries = $this->parameterBag->get('chill_main.available_countries');
$preferredChoices = [];

View File

@@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form\Type;
use Chill\MainBundle\Entity\Language;
use Chill\MainBundle\Form\Type\DataTransformer\MultipleObjectsToIdTransformer;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\Persistence\ObjectManager;
@@ -50,13 +51,13 @@ class Select2LanguageType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new MultipleObjectsToIdTransformer($this->em, 'Chill\MainBundle\Entity\Language');
$transformer = new MultipleObjectsToIdTransformer($this->em, Language::class);
$builder->addModelTransformer($transformer);
}
public function configureOptions(OptionsResolver $resolver)
{
$languages = $this->em->getRepository('Chill\MainBundle\Entity\Language')->findAll();
$languages = $this->em->getRepository(Language::class)->findAll();
$preferredLanguages = $this->parameterBag->get('chill_main.available_languages');
$choices = [];
$preferredChoices = [];

View File

@@ -73,6 +73,17 @@ class NotificationMailer
* Send a email after a notification is persisted.
*/
public function postPersistNotification(Notification $notification, LifecycleEventArgs $eventArgs): void
{
$this->sendNotificationEmailsToAddresses($notification);
$this->sendNotificationEmailsToAddressesEmails($notification);
}
public function postUpdateNotification(Notification $notification, LifecycleEventArgs $eventArgs): void
{
$this->sendNotificationEmailsToAddressesEmails($notification);
}
private function sendNotificationEmailsToAddresses(Notification $notification): void
{
foreach ($notification->getAddressees() as $addressee) {
if (null === $addressee->getEmail()) {
@@ -108,4 +119,31 @@ class NotificationMailer
}
}
}
private function sendNotificationEmailsToAddressesEmails(Notification $notification): void
{
foreach ($notification->getAddressesEmailsAdded() as $emailAddress) {
$email = new TemplatedEmail();
$email
->textTemplate('@ChillMain/Notification/email_non_system_notification_content_to_email.fr.md.twig')
->context([
'notification' => $notification,
'dest' => $emailAddress,
]);
$email
->subject($notification->getTitle())
->to($emailAddress);
try {
$this->mailer->send($email);
} catch (TransportExceptionInterface $e) {
$this->logger->warning('[NotificationMailer] could not send an email notification', [
'to' => $emailAddress,
'error_message' => $e->getMessage(),
'error_trace' => $e->getTraceAsString(),
]);
}
}
}
}

View File

@@ -0,0 +1,58 @@
<?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\Notification\EventListener;
use Chill\MainBundle\Notification\NotificationPersisterInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\TerminateEvent;
use function count;
class PersistNotificationOnTerminateEventSubscriber implements EventSubscriberInterface
{
private EntityManagerInterface $em;
private NotificationPersisterInterface $persister;
public function __construct(EntityManagerInterface $em, NotificationPersisterInterface $persister)
{
$this->em = $em;
$this->persister = $persister;
}
public static function getSubscribedEvents()
{
return [
'kernel.terminate' => [
['onKernelTerminate', 1024], // we must ensure that the priority is before sending email
],
];
}
public function onKernelTerminate(TerminateEvent $event): void
{
if ($event->isMasterRequest()) {
$this->persistNotifications();
}
}
private function persistNotifications(): void
{
if (0 < count($this->persister->getWaitingNotifications())) {
foreach ($this->persister->getWaitingNotifications() as $notification) {
$this->em->persist($notification);
}
$this->em->flush();
}
}
}

View File

@@ -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\Notification;
use Chill\MainBundle\Entity\Notification;
class NotificationPersister implements NotificationPersisterInterface
{
private array $waitingNotifications = [];
/**
* @return array|Notification[]
*/
public function getWaitingNotifications(): array
{
return $this->waitingNotifications;
}
public function persist(Notification $notification): void
{
$this->waitingNotifications[] = $notification;
}
}

View File

@@ -0,0 +1,31 @@
<?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\Notification;
use Chill\MainBundle\Entity\Notification;
/**
* Store the notification.
*
* Those notification will be stored into database by the kernel. This
* will also ensure that this happens outside of regular operations
* operated by the entity manager.
*/
interface NotificationPersisterInterface
{
/**
* @return array|Notification[]
*/
public function getWaitingNotifications(): array;
public function persist(Notification $notification): void;
}

View File

@@ -22,7 +22,7 @@ use libphonenumber\PhoneNumber;
*/
interface PhoneNumberHelperInterface
{
public function format(PhoneNumber $phoneNumber): string;
public function format(?PhoneNumber $phoneNumber = null): string;
/**
* Get type (mobile, landline, ...) for phone number.

View File

@@ -78,8 +78,12 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
*
* @throws NumberParseException
*/
public function format(PhoneNumber $phoneNumber): string
public function format(?PhoneNumber $phoneNumber = null): string
{
if (null === $phoneNumber) {
return '';
}
return $this->phoneNumberUtil
->formatOutOfCountryCallingNumber($phoneNumber, $this->config['default_carrier_code']);
}

View File

@@ -56,17 +56,18 @@ final class NotificationRepository implements ObjectRepository
if (null === $this->notificationByRelatedEntityAndUserAssociatedStatement) {
$sql =
'SELECT
SUM((EXISTS (SELECT 1 AS c FROM chill_main_notification_addresses_unread cmnau WHERE user_id = 1812 and cmnau.notification_id = cmn.id))::int) AS unread,
SUM((cmn.sender_id = 1812)::int) AS sent,
SUM((EXISTS (SELECT 1 AS c FROM chill_main_notification_addresses_unread cmnau WHERE user_id = :userid and cmnau.notification_id = cmn.id))::int) AS unread,
SUM((cmn.sender_id = :userid)::int) AS sent,
COUNT(cmn.*) AS total
FROM chill_main_notification cmn
WHERE relatedentityclass = :relatedEntityClass AND relatedentityid = :relatedEntityId AND sender_id IS NOT NULL';
$this->notificationByRelatedEntityAndUserAssociatedStatement =
$this->em->getConnection()->prepare($sql);
}
$results = $this->notificationByRelatedEntityAndUserAssociatedStatement
->executeQuery(['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId]);
->executeQuery(['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId, 'userid' => $user->getId()]);
$result = $results->fetchAssociative();

View File

@@ -16,6 +16,9 @@
// Chill forms
@import './scss/forms';
// Extend bootstrap accordion
@import './scss/accordion';
// Chill record_actions
@import './scss/record_actions';

View File

@@ -0,0 +1,11 @@
.accordion {
// show a folded / label on accordion
button[aria-expanded="true"] > span.folded,
button[aria-expanded="false"] > span.unfolded { display: none; }
button[aria-expanded="false"] > span.folded,
button[aria-expanded="true"] > span.unfolded { display: inline; }
.accordion-item {
margin-bottom: 1rem;
}
}

View File

@@ -8,30 +8,31 @@ window.addEventListener('DOMContentLoaded', function (e) {
document.querySelectorAll('.notification_toggle_read_status')
.forEach(function (el, i) {
createApp({
template: '<notification-read-toggle ' +
':notificationId="notificationId" ' +
':buttonClass="buttonClass" ' +
':buttonNoText="buttonNoText" ' +
':showUrl="showUrl" ' +
':isRead="isRead"' +
'@markRead="onMarkRead" @markUnread="onMarkUnread"' +
'></notification-read-toggle>',
template: `<notification-read-toggle
:notificationId="notificationId"
:buttonClass="buttonClass"
:buttonNoText="buttonNoText"
:showUrl="showUrl"
:isRead="isRead"
@markRead="onMarkRead"
@markUnread="onMarkUnread">
</notification-read-toggle>`,
components: {
NotificationReadToggle,
},
data() {
return {
notificationId: +el.dataset.notificationId,
notificationId: el.dataset.notificationId,
buttonClass: el.dataset.buttonClass,
buttonNoText: 'false' === el.dataset.buttonText,
showUrl: el.dataset.showButtonUrl,
isRead: 1 === +el.dataset.notificationCurrentIsRead,
isRead: 1 === Number.parseInt(el.dataset.notificationCurrentIsRead),
container: el.dataset.container
}
},
computed: {
getContainer() {
return document.querySelectorAll('div.' + this.container);
return document.querySelectorAll(`div.${this.container}`);
}
},
methods: {

View File

@@ -51,9 +51,7 @@ function loadDynamicPicker(element) {
},
methods: {
addNewEntity(entity) {
console.log('addNewEntity', entity);
if (this.multiple) {
console.log('adding multiple');
if (!this.picked.some(el => {
return el.type === entity.type && el.id === entity.id;
})) {
@@ -71,7 +69,6 @@ function loadDynamicPicker(element) {
}
},
removeEntity(entity) {
console.log('removeEntity', entity);
this.picked = this.picked.filter(e => !(e.type === entity.type && e.id === entity.id));
input.value = JSON.stringify(this.picked);
},
@@ -86,7 +83,6 @@ function loadDynamicPicker(element) {
document.addEventListener('show-hide-show', function(e) {
console.log('creation event caught')
loadDynamicPicker(e.detail.container)
})
@@ -94,17 +90,14 @@ document.addEventListener('show-hide-hide', function(e) {
console.log('hiding event caught')
e.detail.container.querySelectorAll('[data-module="pick-dynamic"]').forEach((el) => {
let uniqId = el.dataset.uniqid;
console.log(uniqId);
if (appsOnPage.has(uniqId)) {
appsOnPage.get(uniqId).unmount();
console.log('App has been unmounted')
appsOnPage.delete(uniqId);
}
})
})
document.addEventListener('DOMContentLoaded', function(e) {
console.log('loaded event', e)
loadDynamicPicker(document)
})

View File

@@ -628,6 +628,10 @@ export default {
newAddress = Object.assign(newAddress, {
'addressReference': this.entity.selected.address.addressReference
});
} else {
newAddress = Object.assign(newAddress, {
'addressReference': null
});
}
if (this.validFrom) {

View File

@@ -8,12 +8,15 @@ import L from 'leaflet';
import markerIconPng from 'leaflet/dist/images/marker-icon.png'
import 'leaflet/dist/leaflet.css';
let map;
let marker;
export default {
name: 'AddressMap',
props: ['entity'],
data() {
return {
map: null,
marker: null
}
},
computed: {
center() {
return this.entity.selected.addressMap.center;
@@ -21,30 +24,33 @@ export default {
},
methods:{
init() {
map = L.map('address_map').setView([46.67059, -1.42683], 12);
this.map = L.map('address_map').setView([46.67059, -1.42683], 12);
map.scrollWheelZoom.disable();
this.map.scrollWheelZoom.disable();
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
}).addTo(this.map);
const markerIcon = L.icon({
iconUrl: markerIconPng,
iconAnchor: [12, 41],
});
marker = L.marker([48.8589, 2.3469], {icon: markerIcon}).addTo(map);
this.marker = L.marker([48.8589, 2.3469], {icon: markerIcon});
this.marker.addTo(this.map);
},
update() {
//console.log('update map with : ', this.address.addressMap.center)
marker.setLatLng(this.entity.addressMap.center);
map.setView(this.entity.addressMap.center, 15);
//console.log('update map with : ', this.entity.addressMap.center)
if (this.marker && this.entity.addressMap.center) {
this.marker.setLatLng(this.entity.addressMap.center);
this.map.setView(this.entity.addressMap.center, 15);
}
}
},
mounted(){
this.init()
this.init();
this.update();
}
}
</script>

View File

@@ -119,7 +119,7 @@ export default {
},
listenInputSearch(query) {
//console.log('listenInputSearch', query, this.isAddressSelectorOpen);
if (!this.entity.selected.writeNew.postcode) {
if (!this.entity.selected.writeNew.postcode && 'id' in this.entity.selected.city) {
if (query.length > 2) {
this.isLoading = true;
searchReferenceAddresses(query, this.entity.selected.city).then(

View File

@@ -106,6 +106,9 @@ export default {
this.$emit('getReferenceAddresses', this.value);
if (this.value.center) {
this.updateMapCenter(this.value.center);
if (this.value.center.coordinates) {
this.entity.selected.postcode.coordinates = this.value.center.coordinates;
}
}
}
},

View File

@@ -25,14 +25,14 @@
{{ $t('my_accompanying_courses.tab') }}
</a>
</li>
<li class="nav-item">
<!-- <li class="nav-item">
<a class="nav-link"
:class="{'active': activeTab === 'MyWorks'}"
@click="selectTab('MyWorks')">
{{ $t('my_works.tab') }}
<tab-counter :count="state.works.count"></tab-counter>
</a>
</li>
</li> -->
<li class="nav-item">
<a class="nav-link"
:class="{'active': activeTab === 'MyEvaluations'}"
@@ -135,7 +135,7 @@ export default {
for (const m of [
'MyNotifications',
'MyAccompanyingCourses',
'MyWorks',
// 'MyWorks',
'MyEvaluations',
'MyTasks',
'MyWorkflows',

View File

@@ -1,3 +1,4 @@
// CURRENTLY NOT IN USE
<template>
<div class="accompanying_course_work">
<div class="alert alert-light">{{ $t('my_works.description') }}</div>

View File

@@ -15,11 +15,11 @@ const appMessages = {
description_warning: "Liste des tâches auxquelles je suis assigné et dont la date d'échéance est dépassée.",
},
my_accompanying_courses: {
tab: "Mes parcours",
description: "Liste des parcours d'accompagnement que l'on vient de m'attribuer.",
tab: "Mes nouveaux parcours",
description: "Liste des parcours d'accompagnement que l'on vient de m'attribuer depuis moins de 15 jours.",
},
my_notifications: {
tab: "Mes notifications",
tab: "Mes nouvelles notifications",
description: "Liste des notifications reçues et non lues.",
},
my_workflows: {

View File

@@ -13,7 +13,7 @@ const isEmpty = (obj) => {
const store = createStore({
strict: debug,
state: {
works: {},
// works: {},
evaluations: {},
tasks: {
warning: {},
@@ -26,9 +26,9 @@ const store = createStore({
loading: false
},
getters: {
isWorksLoaded(state) {
return !isEmpty(state.works);
},
// isWorksLoaded(state) {
// return !isEmpty(state.works);
// },
isEvaluationsLoaded(state) {
return !isEmpty(state.evaluations);
},
@@ -49,7 +49,7 @@ const store = createStore({
},
counter(state) {
return {
works: state.works.count,
// works: state.works.count,
evaluations: state.evaluations.count,
tasksWarning: state.tasks.warning.count,
tasksAlert: state.tasks.alert.count,
@@ -60,10 +60,10 @@ const store = createStore({
}
},
mutations: {
addWorks(state, works) {
//console.log('addWorks', works);
state.works = works;
},
// addWorks(state, works) {
// //console.log('addWorks', works);
// state.works = works;
// },
addEvaluations(state, evaluations) {
//console.log('addEvaluations', evaluations);
state.evaluations = evaluations;
@@ -99,22 +99,22 @@ const store = createStore({
switch (tab) {
case 'MyCustoms':
break;
case 'MyWorks':
if (!getters.isWorksLoaded) {
commit('setLoading', true);
const url = `/api/1.0/person/accompanying-period/work/my-near-end${'?'+ param}`;
makeFetch('GET', url)
.then((response) => {
commit('addWorks', response);
commit('setLoading', false);
})
.catch((error) => {
commit('catchError', error);
throw error;
})
;
}
break;
// case 'MyWorks':
// if (!getters.isWorksLoaded) {
// commit('setLoading', true);
// const url = `/api/1.0/person/accompanying-period/work/my-near-end${'?'+ param}`;
// makeFetch('GET', url)
// .then((response) => {
// commit('addWorks', response);
// commit('setLoading', false);
// })
// .catch((error) => {
// commit('catchError', error);
// throw error;
// })
// ;
// }
// break;
case 'MyEvaluations':
if (!getters.isEvaluationsLoaded) {
commit('setLoading', true);

View File

@@ -210,10 +210,10 @@ export default {
let
type = this.type,
data = {} ;
switch (type) {
case 'person':
data = this.$refs.castPerson.$data.person;
console.log('person data are', data);
break;
case 'thirdparty':
@@ -235,8 +235,12 @@ export default {
type = this.$refs.castNew.radioType;
data = this.$refs.castNew.castDataByType();
// console.log('type', type);
data.civility = data.civility !== null ? {type: 'chill_main_civility', id: data.civility.id} : null;
data.profession = data.profession !== null ? {type: 'third_party_profession', id: data.profession.id} : null;
if (typeof data.civility !== 'undefined' && null !== data.civility) {
data.civility = data.civility !== null ? {type: 'chill_main_civility', id: data.civility.id} : null;
}
if (typeof data.profession !== 'undefined' && null !== data.profession) {
data.profession = data.profession !== null ? {type: 'third_party_profession', id: data.profession.id} : null;
}
// console.log('onthefly data', data);
}
} else {

View File

@@ -66,9 +66,18 @@ export default {
translatedListOfTypes() {
let trans = [];
this.types.forEach(t => {
trans.push(appMessages.fr.pick_entity[t].toLowerCase());
if (this.$props.multiple) {
trans.push(appMessages.fr.pick_entity[t].toLowerCase());
} else {
trans.push(appMessages.fr.pick_entity[t + '_one'].toLowerCase());
}
})
return appMessages.fr.pick_entity.modal_title + trans.join(', ');
if (this.$props.multiple) {
return appMessages.fr.pick_entity.modal_title + trans.join(', ');
} else {
return appMessages.fr.pick_entity.modal_title_one + trans.join(', ');
}
}
},
methods: {
@@ -79,15 +88,10 @@ export default {
);
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
console.log(this.picked)
},
removeEntity(entity) {
console.log('remove entity', entity);
this.$emit('removeEntity', entity);
}
},
mounted() {
console.log(this.picked);
}
}
</script>

View File

@@ -11,6 +11,10 @@ const appMessages = {
user: 'Utilisateurs',
person: 'Usagers',
thirdparty: 'Tiers',
modal_title_one: 'Indiquer un ',
user_one: 'Utilisateur',
thirdparty_one: 'Tiers',
person_one: 'Usager',
}
}
}

View File

@@ -81,7 +81,7 @@ export default {
/// [Option] showUrl is href for show page second button.
// When passed, the component return a button-group with 2 buttons.
isButtonGroup() {
return !!this.showUrl
return this.showUrl;
}
},
methods: {

View File

@@ -69,7 +69,7 @@ const messages = {
person: "Usager"
},
holder: "Titulaire",
years_old: "an | {n} an | {n} ans",
years_old: "1 an | {n} an | {n} ans",
residential_address: "Adresse de résidence",
located_at: "réside chez"
}

View File

@@ -1,54 +0,0 @@
<?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);
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\HttpKernel\Kernel;
class AppKernel extends Kernel
{
/**
* @return string
*/
public function getCacheDir()
{
return sys_get_temp_dir() . '/ChillMainBundle/cache';
}
/**
* @return string
*/
public function getLogDir()
{
return sys_get_temp_dir() . '/ChillMainBundle/logs';
}
public function registerBundles()
{
return [
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Chill\MainBundle\ChillMainBundle(),
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\TwigBundle\TwigBundle(),
new \Symfony\Bundle\AsseticBundle\AsseticBundle(),
new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle(),
new Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle(),
new Symfony\Bundle\MonologBundle\MonologBundle(),
new Knp\Bundle\MenuBundle\KnpMenuBundle(),
new Symfony\Bundle\DebugBundle\DebugBundle(),
];
}
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(__DIR__ . '/config/config_' . $this->getEnvironment() . '.yml');
}
}

View File

@@ -1,20 +0,0 @@
<?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);
use Composer\Autoload\ClassLoader;
use Doctrine\Common\Annotations\AnnotationRegistry;
/** @var ClassLoader $loader */
$loader = require __DIR__ . '/../../../../../vendor/autoload.php';
AnnotationRegistry::registerLoader([$loader, 'loadClass']);
return $loader;

View File

@@ -1,42 +0,0 @@
imports:
- { resource: parameters.yml }
framework:
secret: Not very secret
router: { resource: "%kernel.root_dir%/config/routing.yml" }
form: true
csrf_protection: true
session: ~
default_locale: en
translator: { fallback: en }
profiler: { only_exceptions: false }
templating:
engines: ['twig']
# Doctrine Configuration
doctrine:
dbal:
driver: pdo_pgsql
host: "%database_host%"
port: "%database_port%"
dbname: "%database_name%"
user: "%database_user%"
password: "%database_password%"
charset: UTF8
orm:
auto_generate_proxy_classes: "%kernel.debug%"
auto_mapping: true
# Assetic Configuration
assetic:
debug: "%kernel.debug%"
use_controller: false
bundles: [ ]
#java: /usr/bin/java
filters:
cssrewrite: ~
chill_main:
available_languages: [fr, en]
available_countries: [FR]

View File

@@ -1,51 +0,0 @@
imports:
- { resource: config.yml }
framework:
test: ~
session:
storage_id: session.storage.filesystem
security:
role_hierarchy:
CHILL_MASTER_ROLE: [CHILL_INHERITED_ROLE_1]
providers:
chain_provider:
chain :
providers: [in_memory, users]
in_memory:
memory:
users:
admin: { password: "password", roles: 'ROLE_ADMIN' }
users:
entity:
class: Chill\MainBundle\Entity\User
property: username
encoders:
Chill\MainBundle\Entity\User:
algorithm: bcrypt
Symfony\Component\Security\Core\User\User:
algorithm: plaintext
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
default:
anonymous: ~
http_basic: ~
form_login:
csrf_parameter: _csrf_token
csrf_token_id: authenticate
logout: ~
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/[a-z]*/admin, roles: ROLE_ADMIN }
- { path: ^/, roles: ROLE_USER }

View File

@@ -1,11 +0,0 @@
parameters:
database_host: chill__database
database_port: 5432
database_name: postgres
database_user: postgres
database_password: postgres
locale: fr
secret: ThisTokenIsNotSoSecretChangeIt
debug_toolbar: true
debug_redirects: false
use_assetic_controller: true

View File

@@ -1,11 +0,0 @@
parameters:
database_host: 127.0.0.1
database_port: 5435
database_name: chill
database_user: chill
database_password: chill
locale: fr
secret: ThisTokenIsNotSoSecretChangeIt
debug_toolbar: true
debug_redirects: false
use_assetic_controller: true

View File

@@ -1,34 +0,0 @@
hello_bundle:
resource: "@ChillMainBundle/config/routing.yml"
chill_main_dummy_0:
path: /dummy
defaults: { _controller: ChillMainBundle:Default:index }
options:
menus:
dummy0:
order: 50
label: test0
otherkey: othervalue
dummy1:
order: 50
label: test dummy 1
chill_main_dummy_1:
path: /dummy1
defaults: { _controller: ChillMainBundle:Default:index }
options:
menus:
dummy0:
order: 50
label: 'test1'
helper: 'great helper'
chill_main_dummy_2:
path: /dummy2/{param}
defaults: {_controller: ChillMainBundle:Default:index }
options:
menus:
dummy0:
order: 50
label: test2

View File

@@ -1,27 +0,0 @@
#!/usr/bin/env php
<?php
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Debug\Debug;
// if you don't want to setup permissions the proper way, just uncomment the following PHP line
// read https://symfony.com/doc/current/setup.html#checking-symfony-application-configuration-and-setup
// for more information
//umask(0000);
set_time_limit(0);
require __DIR__.'/autoload.php';
$input = new ArgvInput();
$env = $input->getParameterOption(array('--env', '-e'), getenv('SYMFONY_ENV') ?: 'dev');
$debug = getenv('SYMFONY_DEBUG') !== '0' && !$input->hasParameterOption(array('--no-debug', '')) && $env !== 'prod';
if ($debug) {
Debug::enable();
}
$kernel = new AppKernel($env, $debug);
$application = new Application($kernel);
$application->run($input);

View File

@@ -217,6 +217,7 @@
{% 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'] }}"/>
<div data-module="pick-dynamic" data-types="{{ form.vars['types']|json_encode }}" data-multiple="{{ form.vars['multiple'] }}" data-uniqid="{{ form.vars['uniqid'] }}"></div>
{% endblock %}

View File

@@ -47,6 +47,8 @@
</tbody>
</table>
{{ chill_pagination(paginator) }}
<ul class="record_actions">
<li>
<a href="{{ path('chill_crud_main_location_new') }}" class="btn btn-create">

View File

@@ -40,6 +40,11 @@
{{ a|chill_entity_render_string }}
</span>
{% endfor %}
{% for a in c.notification.addressesEmails %}
<span class="badge-user" title="{{ 'notification.Email with access link'|trans|e('html_attr') }}">
{{ a }}
</span>
{% endfor %}
</li>
{% endif %}
</ul>
@@ -61,17 +66,28 @@
<div class="item-row">
<div class="notification-content">
{% if c.full_content is defined and c.full_content == true %}
{{ c.notification.message|chill_markdown_to_html }}
{% if c.notification.message is not empty %}
{{ c.notification.message|chill_markdown_to_html }}
{% else %}
<p class="chill-no-data-statement">{{ 'Any comment'|trans }}</p>
{% endif %}
{% else %}
{{ c.notification.message|u.truncate(250, '…', false)|chill_markdown_to_html }}
<p class="read-more"><a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}">{{ 'Read more'|trans }}</a></p>
{% if c.notification.message is not empty %}
{{ c.notification.message|u.truncate(250, '…', false)|chill_markdown_to_html }}
<p class="read-more"><a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}">{{ 'Read more'|trans }}</a></p>
{% else %}
<p class="chill-no-data-statement">{{ 'Any comment'|trans }}</p>
{% endif %}
{% endif %}
</div>
</div>
{% endmacro %}
{% macro actions(c) %}
{% if c.action_button is not defined or c.action_button != false %}
<div class="item-row separator">
<div class="item-col item-meta">
{% if c.notification.comments|length > 0 %}
<div class="comment-counter">
<span class="counter">
@@ -79,16 +95,16 @@
</span>
</div>
{% endif %}
</div>
<div class="item-col">
<ul class="record_actions">
<li>
{# Vue component #}
<span class="notification_toggle_read_status"
data-notification-id="{{ c.notification.id }}"
data-notification-current-is-read="{{ c.notification.isReadBy(app.user) }}"
data-container="notification-status"
data-notification-id="{{ c.notification.id }}"
data-notification-current-is-read="{{ c.notification.isReadBy(app.user) }}"
data-container="notification-status"
></span>
</li>
{% if is_granted('CHILL_MAIN_NOTIFICATION_UPDATE', c.notification) %}
@@ -122,24 +138,25 @@
<button type="button" class="accordion-button collapsed"
data-bs-toggle="collapse" data-bs-target="#flush-collapse-{{ notification.id }}"
aria-expanded="false" aria-controls="flush-collapse-{{ notification.id }}">
{{ _self.title(_context) }}
</button>
{{ _self.header(_context) }}
</div>
<div id="flush-collapse-{{ notification.id }}"
class="accordion-collapse collapse"
aria-labelledby="flush-heading-{{ notification.id }}"
data-bs-parent="#notification-fold">
{{ _self.content(_context) }}
</div>
{{ _self.actions(_context) }}
{% else %}
{{ _self.title(_context) }}
{{ _self.header(_context) }}
{{ _self.content(_context) }}
{{ _self.actions(_context) }}
{% endif %}
</div>

View File

@@ -20,7 +20,9 @@
{{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
{{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
{{ form_row(form.addressesEmails) }}
{% include handler.template(notification) with handler.templateData(notification) %}
<div class="mb-3 row">

View File

@@ -21,6 +21,8 @@
{{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
{{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
{{ form_row(form.addressesEmails) }}
{% include handler.template(notification) with handler.templateData(notification) %}
<div class="mb-3 row">

View File

@@ -0,0 +1,20 @@
{{ dest }},
{{ notification.sender.label }} a créé une notification pour vous:
> {{ notification.title }}
>
>
{%- for line in notification.message|split("\n") %}
> {{ line }}
{%- if not loop.last %}
>
{%- endif %}
{%- endfor %}
Vous pouvez cliquer sur ce lien pour obtenir un accès permanent à la notification:
{{ absolute_url(path('chill_main_notification_grant_access_by_access_key', {'_locale': 'fr', 'id': notification.id, 'accessKey': notification.accessKey, 'email': dest})) }}
--
Le logiciel Chill

View File

@@ -59,4 +59,10 @@
{{ chill_pagination(paginator) }}
{% endblock %}
<ul class="record_actions sticky-form-buttons">
<li>
<a href="{{ path('chill_crud_admin_user_new') }}" class="btn btn-create">{{ 'Create'|trans }}</a>
</li>
</ul>
{% endblock %}

View File

@@ -2,7 +2,7 @@
{{ 'workflow_'|trans }}
</h2>
<div class="item-row col">
<div class="item-row col" style="display: block;">
{% include handler.template(entity_workflow) with handler.templateData(entity_workflow)|merge({
'description': true,
'breadcrumb': true,

View File

@@ -3,6 +3,7 @@
{% block title 'workflow.Delete workflow ?'|trans %}
{% block display_content %}
<div class="col-10 workflow">
<h2>
{{ handler.entityTitle(entityWorkflow) }}
</h2>
@@ -10,6 +11,7 @@
{% include handler.template(entityWorkflow) with handler.templateData(entityWorkflow)|merge({
'display_action': false
}) %}
{% endblock %}
{% block content %}
@@ -25,5 +27,5 @@
'form' : delete_form
} ) }}
</div>
</div>
{% endblock %}

View File

@@ -36,33 +36,35 @@
{# Flash messages ! #}
{% if app.session.flashbag.keys()|length > 0 %}
<div class="col-8 mb-5 flash_message">
<div class="row justify-content-center">
<div class="col-10 mb-5 flash_message">
{% for flashMessage in app.session.flashbag.get('success') %}
<div class="alert alert-success flash_message">
<span>{{ flashMessage|raw }}</span>
</div>
{% endfor %}
{% for flashMessage in app.session.flashbag.get('error') %}
<div class="alert alert-danger flash_message">
<span>{{ flashMessage|raw }}</span>
</div>
{% endfor %}
{% for flashMessage in app.session.flashbag.get('notice') %}
<div class="alert alert-warning flash_message">
<span>{{ flashMessage|raw }}</span>
</div>
{% endfor %}
{% for flashMessage in app.session.flashbag.get('success') %}
<div class="alert alert-success flash_message">
<span>{{ flashMessage|raw }}</span>
</div>
{% endfor %}
{% for flashMessage in app.session.flashbag.get('error') %}
<div class="alert alert-danger flash_message">
<span>{{ flashMessage|raw }}</span>
</div>
{% endfor %}
{% for flashMessage in app.session.flashbag.get('notice') %}
<div class="alert alert-warning flash_message">
<span>{{ flashMessage|raw }}</span>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% block content %}
<div class="col-8 main_search">
<h2>{{ 'Search'|trans }}</h2>
<form action="{{ path('chill_main_search') }}" method="get">
<input class="form-control form-control-lg" name="q" type="search" placeholder="{{ 'Search persons, ...'|trans }}" />
<center>
@@ -75,11 +77,11 @@
</center>
</form>
</div>
{# DISABLED {{ chill_widget('homepage', {} ) }} #}
{% include '@ChillMain/Homepage/index.html.twig' %}
{% endblock %}
</div>

View File

@@ -45,15 +45,16 @@ class SearchUserApiProvider implements SearchApiInterface
$query
->setSelectKey('user')
->setSelectJsonbMetadata("jsonb_build_object('id', u.id)")
->setSelectPertinence('GREATEST(SIMILARITY(LOWER(UNACCENT(?)), u.label),
->setSelectPertinence('3 + GREATEST(SIMILARITY(LOWER(UNACCENT(?)), u.label),
SIMILARITY(LOWER(UNACCENT(?)), u.usernamecanonical))', [$pattern, $pattern])
->setFromClause('users AS u')
->setWhereClauses('
u.enabled IS TRUE and (
SIMILARITY(LOWER(UNACCENT(?)), u.usernamecanonical) > 0.15
OR u.usernamecanonical LIKE \'%\' || LOWER(UNACCENT(?)) || \'%\'
OR SIMILARITY(LOWER(UNACCENT(?)), LOWER(UNACCENT(u.label))) > 0.15
OR u.label LIKE \'%\' || LOWER(UNACCENT(?)) || \'%\'
', [$pattern, $pattern, $pattern, $pattern]);
)', [$pattern, $pattern, $pattern, $pattern]);
return $query;
}

View File

@@ -59,7 +59,23 @@ class EntityWorkflowVoter extends Voter
return true;
}
return $this->security->isGranted($entityAttribute, $handler->getRelatedEntity($subject));
$relatedEntity = $handler->getRelatedEntity($subject);
if (null === $relatedEntity) {
return true;
}
if ($this->security->isGranted($entityAttribute, $relatedEntity)) {
return true;
}
foreach ($subject->getSteps() as $step) {
if ($step->getAllDestUser()->contains($token->getUser())) {
return true;
}
}
return false;
case self::DELETE:
return $subject->getStep() === 'initial';

View File

@@ -71,6 +71,7 @@ class AddressNormalizer implements ContextAwareNormalizerInterface, NormalizerAw
'id' => $address->getPostCode()->getId(),
'name' => $address->getPostCode()->getName(),
'code' => $address->getPostCode()->getCode(),
'center' => $address->getPostcode()->getCenter(),
],
'country' => [
'id' => $address->getPostCode()->getCountry()->getId(),

View File

@@ -44,14 +44,25 @@ class DateNormalizer implements ContextAwareNormalizerInterface, DenormalizerInt
switch ($type) {
case DateTime::class:
return DateTime::createFromFormat(DateTimeInterface::ISO8601, $data['datetime']);
$result = DateTime::createFromFormat(DateTimeInterface::ISO8601, $data['datetime']);
break;
case DateTimeInterface::class:
case DateTimeImmutable::class:
return DateTimeImmutable::createFromFormat(DateTimeInterface::ISO8601, $data['datetime']);
$result = DateTimeImmutable::createFromFormat(DateTimeInterface::ISO8601, $data['datetime']);
break;
default:
throw new UnexpectedValueException();
}
throw new UnexpectedValueException();
if (false === $result) {
return null;
}
return $result;
}
public function normalize($date, $format = null, array $context = [])

View File

@@ -32,7 +32,7 @@ class PhonenumberNormalizer implements ContextAwareNormalizerInterface, Denormal
}
/**
* @param mixed $data
* @param string|null $data
* @param mixed $type
* @param null|mixed $format
*
@@ -40,7 +40,7 @@ class PhonenumberNormalizer implements ContextAwareNormalizerInterface, Denormal
*/
public function denormalize($data, $type, $format = null, array $context = [])
{
if ('' === trim($data)) {
if ('' === trim((string) $data)) {
return null;
}

View File

@@ -83,9 +83,9 @@ abstract class AbstractExportTest extends WebTestCase
$em = static::$kernel->getContainer()
->get('doctrine.orm.entity_manager');
$centers = $em->getRepository('ChillMainBundle:Center')
$centers = $em->getRepository(\Chill\MainBundle\Entity\Center::class)
->findAll();
$circles = $em->getRepository('ChillMainBundle:Scope')
$circles = $em->getRepository(\Chill\MainBundle\Entity\Scope::class)
->findAll();
if (count($centers) === 0) {

View File

@@ -161,7 +161,7 @@ final class UserControllerTest extends WebTestCase
$user = self::$kernel->getContainer()
->get('doctrine.orm.entity_manager')
->getRepository('ChillMainBundle:User')
->getRepository(\Chill\MainBundle\Entity\User::class)
->findOneBy(['username' => $username]);
$this->assertTrue($passwordEncoder->isPasswordValid($user, $password));

View File

@@ -88,6 +88,24 @@ final class NotificationTest extends KernelTestCase
$this->assertNotContains($user1, $notification->getUnreadBy()->toArray());
}
public function testAddressesEmail(): void
{
$notification = new Notification();
$notification->addAddressesEmail('test');
$notification->addAddressesEmail('other');
$this->assertContains('test', $notification->getAddressesEmails());
$this->assertContains('other', $notification->getAddressesEmails());
$this->assertContains('test', $notification->getAddressesEmailsAdded());
$this->assertContains('other', $notification->getAddressesEmailsAdded());
$notification->removeAddressesEmail('other');
$this->assertNotContains('other', $notification->getAddressesEmails());
$this->assertNotContains('other', $notification->getAddressesEmailsAdded());
}
/**
* @dataProvider generateNotificationData
*/

View File

@@ -672,7 +672,7 @@ final class ExportManagerTest extends KernelTestCase
$localUser = $user ?? self::$container->get(
'doctrine.orm.entity_manager'
)
->getRepository('ChillMainBundle:User')
->getRepository(\Chill\MainBundle\Entity\User::class)
->findOneBy(['username' => 'center a_social']);
$token = new UsernamePasswordToken($localUser, 'password', 'provider');
$tokenStorage = new TokenStorage();

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 Notification\EventListener;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Notification\EventListener\PersistNotificationOnTerminateEventSubscriber;
use Chill\MainBundle\Notification\NotificationPersister;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\HttpKernel\Event\TerminateEvent;
/**
* @internal
* @coversNothing
*/
final class PersistNotificationOnTerminateEventSubscriberTest extends TestCase
{
use ProphecyTrait;
public function testNotificationIsPersisted()
{
$persister = new NotificationPersister();
$em = $this->prophesize(EntityManagerInterface::class);
$em->persist(Argument::type(Notification::class))->shouldBeCalledTimes(1);
$em->flush()->shouldBeCalledTimes(1);
$event = $this->prophesize(TerminateEvent::class);
$event->isMasterRequest()->willReturn(true);
$eventSubscriber = new PersistNotificationOnTerminateEventSubscriber($em->reveal(), $persister);
$notification = new Notification();
$persister->persist($notification);
$eventSubscriber->onKernelTerminate($event->reveal());
}
}

View File

@@ -0,0 +1,54 @@
<?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 Serializer\Normalizer;
use Chill\MainBundle\Serializer\Normalizer\PhonenumberNormalizer;
use libphonenumber\PhoneNumber;
use libphonenumber\PhoneNumberUtil;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
/**
* @internal
* @coversNothing
*/
final class PhonenumberNormalizerTest extends TestCase
{
use ProphecyTrait;
public function dataProviderNormalizePhonenumber()
{
$phonenumberUtil = PhoneNumberUtil::getInstance();
yield [$phonenumberUtil->parse('+32486123465'), 'docgen', ['docgen:expects' => PhoneNumber::class], '0486 12 34 65'];
yield [null, 'docgen', ['docgen:expects' => PhoneNumber::class], ''];
}
/**
* @dataProvider dataProviderNormalizePhonenumber
*
* @param mixed $format
* @param mixed $context
* @param mixed $expected
*/
public function testNormalize(?Phonenumber $phonenumber, $format, $context, $expected)
{
$parameterBag = $this->prophesize(ParameterBagInterface::class);
$parameterBag->get(Argument::exact('chill_main'))->willReturn(['phone_helper' => ['default_carrier_code' => 'BE']]);
$normalizer = new PhonenumberNormalizer($parameterBag->reveal());
$this->assertEquals($expected, $normalizer->normalize($phonenumber, $format, $context));
}
}

View File

@@ -1,16 +0,0 @@
<?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);
if (!is_file($autoloadFile = __DIR__ . '/../vendor/autoload.php')) {
throw new \LogicException('Could not find autoload.php in vendor/. Did you run "composer install --dev"?');
}
require $autoloadFile;

View File

@@ -62,7 +62,7 @@ class WorkflowTwigExtensionRuntime implements RuntimeExtensionInterface
}
/**
* @param array{relatedEntityClass: string, relatedEntityId: int} $supplementaryRelated
* @param array<array{relatedEntityClass: string, relatedEntityId: int}> $supplementaryRelated
*
* @throws \Symfony\Component\Serializer\Exception\ExceptionInterface
* @throws \Twig\Error\LoaderError

View File

@@ -3,6 +3,11 @@ services:
autowire: true
autoconfigure: true
Chill\MainBundle\Notification\:
resource: ../../Notification/
autoconfigure: true
autowire: true
Chill\MainBundle\Notification\Mailer:
arguments:
$logger: '@Psr\Log\LoggerInterface'
@@ -17,12 +22,6 @@ services:
arguments:
$handlers: !tagged_iterator chill_main.notification_handler
Chill\MainBundle\Notification\NotificationPresence: ~
Chill\MainBundle\Notification\Templating\NotificationTwigExtension: ~
Chill\MainBundle\Notification\Templating\NotificationTwigExtensionRuntime: ~
Chill\MainBundle\Notification\Counter\NotificationByUserCounter:
autoconfigure: true
autowire: true
@@ -61,6 +60,15 @@ services:
# set the 'lazy' option to TRUE to only instantiate listeners when they are used
lazy: true
method: 'postPersistNotification'
-
name: 'doctrine.orm.entity_listener'
event: 'postUpdate'
entity: 'Chill\MainBundle\Entity\Notification'
# set the 'lazy' option to TRUE to only instantiate listeners when they are used
lazy: true
method: 'postUpdateNotification'
-
name: 'doctrine.orm.entity_listener'
event: 'postPersist'

View File

@@ -0,0 +1,46 @@
<?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 Version20220413154743 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_notification DROP adressesEmails');
$this->addSql('ALTER TABLE chill_main_notification DROP accessKey');
}
public function getDescription(): string
{
return 'add access keys and emails dest to notifications';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_notification ADD addressesEmails JSON NOT NULL DEFAULT \'[]\';');
$this->addSql('ALTER TABLE chill_main_notification ADD accessKey TEXT DEFAULT NULL');
$this->addSql('WITH randoms AS (select
n.id,
string_agg(substr(characters, (random() * length(characters) + 0.5)::integer, 1), \'\') as random_word
from (values(\'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\')) as symbols(characters)
-- length of word
join generate_series(1, 16) on 1 = 1
JOIN chill_main_notification n ON true
GROUP BY n.id)
UPDATE chill_main_notification SET accessKey = randoms.random_word FROM randoms WHERE chill_main_notification.id = randoms.id');
$this->addSql('ALTER TABLE chill_main_notification ALTER accessKey DROP DEFAULT');
$this->addSql('ALTER TABLE chill_main_notification ALTER accessKey SET 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 Version20220413225830 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_address ALTER COLUMN postcode_id DROP NOT NULL');
}
public function getDescription(): string
{
return 'Set postal code in chill_main_address NOT NULL';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_address ALTER COLUMN postcode_id SET 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 Version20220413230159 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_address DROP CONSTRAINT chill_custom_validfrom_before_validto');
}
public function getDescription(): string
{
return 'in addresses, validfrom must be lower than validto';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_address ADD CONSTRAINT chill_custom_validfrom_before_validto CHECK (validfrom <= COALESCE(validto, \'infinity\'::date))');
}
}

View File

@@ -450,4 +450,10 @@ notification:
subject: Objet
see_comments_thread: Voir le fil de commentaires associé
object_prefix: "[CHILL] notification - "
dest by email: Lien d'accès par email
Any email: Aucun email
Add an email: Ajouter un email
dest by email help: Les adresses email mentionnées ici recevront un lien d'accès. Un compte utilisateur sera toujours nécessaire.
Remove an email: Supprimer l'adresse email
Email with access link: Adresse email ayant reçu un lien d'accès