mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-29 21:46:14 +00:00
Merge remote-tracking branch 'origin/calendar/synchro-msgraph' into calendar/finalization
This commit is contained in:
commit
03d64995d9
@ -1,3 +1,14 @@
|
||||
{#
|
||||
WARNING: this file is in use in both ActivityBundle and CalendarBundle.
|
||||
|
||||
Take care when editing this file.
|
||||
|
||||
Maybe should we think about abstracting this file a bit more ? Moving it to PersonBundle ?
|
||||
#}
|
||||
{% if context == 'calendar_accompanyingCourse' %}
|
||||
{% import "@ChillCalendar/_invite.html.twig" as invite %}
|
||||
{% endif %}
|
||||
|
||||
{% macro href(pathname, key, value) %}
|
||||
{% set parms = { (key): value } %}
|
||||
{{ path(pathname, parms) }}
|
||||
@ -132,6 +143,12 @@
|
||||
{% if bloc.type == 'user' %}
|
||||
<span class="badge-user">
|
||||
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }}
|
||||
{%- if context == 'calendar_accompanyingCourse' %}
|
||||
{% set invite = entity.inviteForUser(item) %}
|
||||
{% if invite is not null %}
|
||||
{{ invite.invite_span(invite) }}
|
||||
{% endif %}
|
||||
{%- endif -%}
|
||||
</span>
|
||||
{% else %}
|
||||
{{ _self.insert_onthefly(bloc.type, item) }}
|
||||
|
@ -0,0 +1,164 @@
|
||||
<?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\CalendarBundle\Command;
|
||||
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\EventsOnUserSubscriptionCreator;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MapCalendarToUser;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSGraphUserRepository;
|
||||
use DateInterval;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class MapAndSubscribeUserCalendarCommand extends Command
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private EventsOnUserSubscriptionCreator $eventsOnUserSubscriptionCreator;
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private MapCalendarToUser $mapCalendarToUser;
|
||||
|
||||
private MSGraphUserRepository $userRepository;
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $em,
|
||||
EventsOnUserSubscriptionCreator $eventsOnUserSubscriptionCreator,
|
||||
LoggerInterface $logger,
|
||||
MapCalendarToUser $mapCalendarToUser,
|
||||
MSGraphUserRepository $userRepository
|
||||
) {
|
||||
parent::__construct('chill:calendar:msgraph-user-map-subscribe');
|
||||
|
||||
$this->em = $em;
|
||||
$this->eventsOnUserSubscriptionCreator = $eventsOnUserSubscriptionCreator;
|
||||
$this->logger = $logger;
|
||||
$this->mapCalendarToUser = $mapCalendarToUser;
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->logger->info(__CLASS__ . ' execute command');
|
||||
|
||||
$limit = 50;
|
||||
$offset = 0;
|
||||
/** @var DateInterval $interval the interval before the end of the expiration */
|
||||
$interval = new DateInterval('P1D');
|
||||
$expiration = (new DateTimeImmutable('now'))->add(new DateInterval('PT15M'));
|
||||
$total = $this->userRepository->countByMostOldSubscriptionOrWithoutSubscriptionOrData($interval);
|
||||
$created = 0;
|
||||
$renewed = 0;
|
||||
|
||||
$this->logger->info(__CLASS__ . ' the number of user to get - renew', [
|
||||
'total' => $total,
|
||||
'expiration' => $expiration->format(DateTimeImmutable::ATOM),
|
||||
]);
|
||||
|
||||
while ($offset < ($total - 1)) {
|
||||
$users = $this->userRepository->findByMostOldSubscriptionOrWithoutSubscriptionOrData(
|
||||
$interval,
|
||||
$limit,
|
||||
$offset
|
||||
);
|
||||
|
||||
foreach ($users as $user) {
|
||||
if (!$this->mapCalendarToUser->hasUserId($user)) {
|
||||
$this->mapCalendarToUser->writeMetadata($user);
|
||||
}
|
||||
|
||||
if ($this->mapCalendarToUser->hasUserId($user)) {
|
||||
// we first try to renew an existing subscription, if any.
|
||||
// if not, or if it fails, we try to create a new one
|
||||
if ($this->mapCalendarToUser->hasActiveSubscription($user)) {
|
||||
$this->logger->debug(__CLASS__ . ' renew a subscription for', [
|
||||
'userId' => $user->getId(),
|
||||
'username' => $user->getUsernameCanonical(),
|
||||
]);
|
||||
|
||||
['secret' => $secret, 'id' => $id, 'expiration' => $expirationTs]
|
||||
= $this->eventsOnUserSubscriptionCreator->renewSubscriptionForUser($user, $expiration);
|
||||
$this->mapCalendarToUser->writeSubscriptionMetadata($user, $expirationTs, $id, $secret);
|
||||
|
||||
if (0 !== $expirationTs) {
|
||||
++$renewed;
|
||||
} else {
|
||||
$this->logger->warning(__CLASS__ . ' could not renew subscription for a user', [
|
||||
'userId' => $user->getId(),
|
||||
'username' => $user->getUsernameCanonical(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->mapCalendarToUser->hasActiveSubscription($user)) {
|
||||
$this->logger->debug(__CLASS__ . ' create a subscription for', [
|
||||
'userId' => $user->getId(),
|
||||
'username' => $user->getUsernameCanonical(),
|
||||
]);
|
||||
|
||||
['secret' => $secret, 'id' => $id, 'expiration' => $expirationTs]
|
||||
= $this->eventsOnUserSubscriptionCreator->createSubscriptionForUser($user, $expiration);
|
||||
$this->mapCalendarToUser->writeSubscriptionMetadata($user, $expirationTs, $id, $secret);
|
||||
|
||||
if (0 !== $expirationTs) {
|
||||
++$created;
|
||||
} else {
|
||||
$this->logger->warning(__CLASS__ . ' could not create subscription for a user', [
|
||||
'userId' => $user->getId(),
|
||||
'username' => $user->getUsernameCanonical(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
++$offset;
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
}
|
||||
|
||||
$this->logger->warning(__CLASS__ . ' process executed', [
|
||||
'created' => $created,
|
||||
'renewed' => $renewed,
|
||||
]);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this
|
||||
->setDescription('MSGraph: collect user metadata and create subscription on events for users')
|
||||
->addOption(
|
||||
'renew-before-end-interval',
|
||||
'r',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'delay before renewing subscription',
|
||||
'P1D'
|
||||
)
|
||||
->addOption(
|
||||
'subscription-duration',
|
||||
's',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'duration for the subscription',
|
||||
'PT4230M'
|
||||
);
|
||||
}
|
||||
}
|
@ -1,58 +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);
|
||||
|
||||
namespace Chill\CalendarBundle\Command;
|
||||
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MapCalendarToUser;
|
||||
use Chill\MainBundle\Repository\UserRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class MapUserCalendarCommand extends Command
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private MapCalendarToUser $mapCalendarToUser;
|
||||
|
||||
private UserRepository $userRepository;
|
||||
|
||||
public function __construct(EntityManagerInterface $em, MapCalendarToUser $mapCalendarToUser, UserRepository $userRepository)
|
||||
{
|
||||
parent::__construct('chill:calendar:map-user');
|
||||
|
||||
$this->em = $em;
|
||||
$this->mapCalendarToUser = $mapCalendarToUser;
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$limit = 2;
|
||||
$offset = 0;
|
||||
$total = $this->userRepository->countByNotHavingAttribute(MapCalendarToUser::METADATA_KEY);
|
||||
|
||||
while ($offset < $total) {
|
||||
$users = $this->userRepository->findByNotHavingAttribute(MapCalendarToUser::METADATA_KEY, $limit, $offset);
|
||||
|
||||
foreach ($users as $user) {
|
||||
$this->mapCalendarToUser->writeMetadata($user);
|
||||
++$offset;
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
<?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\CalendarBundle\Command;
|
||||
|
||||
use Chill\CalendarBundle\Service\ShortMessageNotification\BulkCalendarShortMessageSender;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class SendShortMessageOnEligibleCalendar extends Command
|
||||
{
|
||||
private BulkCalendarShortMessageSender $messageSender;
|
||||
|
||||
public function __construct(BulkCalendarShortMessageSender $messageSender)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->messageSender = $messageSender;
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return 'chill:calendar:send-short-messages';
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$this->messageSender->sendBulkMessageToEligibleCalendars();
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,200 @@
|
||||
<?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\CalendarBundle\Command;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Service\ShortMessageNotification\ShortMessageForCalendarBuilderInterface;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Phonenumber\PhoneNumberHelperInterface;
|
||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||
use Chill\MainBundle\Service\ShortMessage\ShortMessageTransporterInterface;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Repository\PersonRepository;
|
||||
use DateInterval;
|
||||
use DateTimeImmutable;
|
||||
use libphonenumber\PhoneNumber;
|
||||
use libphonenumber\PhoneNumberFormat;
|
||||
use libphonenumber\PhoneNumberType;
|
||||
use libphonenumber\PhoneNumberUtil;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use UnexpectedValueException;
|
||||
use function count;
|
||||
|
||||
class SendTestShortMessageOnCalendarCommand extends Command
|
||||
{
|
||||
private ShortMessageForCalendarBuilderInterface $messageForCalendarBuilder;
|
||||
|
||||
private PersonRepository $personRepository;
|
||||
|
||||
private PhoneNumberHelperInterface $phoneNumberHelper;
|
||||
|
||||
private PhoneNumberUtil $phoneNumberUtil;
|
||||
|
||||
private ShortMessageTransporterInterface $transporter;
|
||||
|
||||
private UserRepositoryInterface $userRepository;
|
||||
|
||||
public function __construct(
|
||||
PersonRepository $personRepository,
|
||||
PhoneNumberUtil $phoneNumberUtil,
|
||||
PhoneNumberHelperInterface $phoneNumberHelper,
|
||||
ShortMessageForCalendarBuilderInterface $messageForCalendarBuilder,
|
||||
ShortMessageTransporterInterface $transporter,
|
||||
UserRepositoryInterface $userRepository
|
||||
) {
|
||||
parent::__construct();
|
||||
|
||||
$this->personRepository = $personRepository;
|
||||
$this->phoneNumberUtil = $phoneNumberUtil;
|
||||
$this->phoneNumberHelper = $phoneNumberHelper;
|
||||
$this->messageForCalendarBuilder = $messageForCalendarBuilder;
|
||||
$this->transporter = $transporter;
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return 'chill:calendar:test-send-short-message';
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this->setDescription('Test sending a SMS for a dummy calendar appointment');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$calendar = new Calendar();
|
||||
$calendar->setSendSMS(true);
|
||||
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
|
||||
// start date
|
||||
$question = new Question('When will start the appointment ? (default: "1 hour") ', '1 hour');
|
||||
$startDate = new DateTimeImmutable($helper->ask($input, $output, $question));
|
||||
|
||||
if (false === $startDate) {
|
||||
throw new UnexpectedValueException('could not create a date with this date and time');
|
||||
}
|
||||
|
||||
$calendar->setStartDate($startDate);
|
||||
|
||||
// end date
|
||||
$question = new Question('How long will last the appointment ? (default: "PT30M") ', 'PT30M');
|
||||
$interval = new DateInterval($helper->ask($input, $output, $question));
|
||||
|
||||
if (false === $interval) {
|
||||
throw new UnexpectedValueException('could not create the interval');
|
||||
}
|
||||
|
||||
$calendar->setEndDate($calendar->getStartDate()->add($interval));
|
||||
|
||||
// a person
|
||||
$question = new Question('Who will participate ? Give an id for a person. ');
|
||||
$question
|
||||
->setValidator(function ($answer): Person {
|
||||
if (!is_numeric($answer)) {
|
||||
throw new UnexpectedValueException('the answer must be numeric');
|
||||
}
|
||||
|
||||
if (0 >= (int) $answer) {
|
||||
throw new UnexpectedValueException('the answer must be greater than zero');
|
||||
}
|
||||
|
||||
$person = $this->personRepository->find((int) $answer);
|
||||
|
||||
if (null === $person) {
|
||||
throw new UnexpectedValueException('The person is not found');
|
||||
}
|
||||
|
||||
return $person;
|
||||
});
|
||||
|
||||
$person = $helper->ask($input, $output, $question);
|
||||
$calendar->addPerson($person);
|
||||
|
||||
// a main user
|
||||
$question = new Question('Who will be the main user ? Give an id for a user. ');
|
||||
$question
|
||||
->setValidator(function ($answer): User {
|
||||
if (!is_numeric($answer)) {
|
||||
throw new UnexpectedValueException('the answer must be numeric');
|
||||
}
|
||||
|
||||
if (0 >= (int) $answer) {
|
||||
throw new UnexpectedValueException('the answer must be greater than zero');
|
||||
}
|
||||
|
||||
$user = $this->userRepository->find((int) $answer);
|
||||
|
||||
if (null === $user) {
|
||||
throw new UnexpectedValueException('The user is not found');
|
||||
}
|
||||
|
||||
return $user;
|
||||
});
|
||||
|
||||
$user = $helper->ask($input, $output, $question);
|
||||
$calendar->setMainUser($user);
|
||||
|
||||
// phonenumber
|
||||
$phonenumberFormatted = null !== $person->getMobilenumber() ?
|
||||
$this->phoneNumberUtil->format($person->getMobilenumber(), PhoneNumberFormat::E164) : '';
|
||||
$question = new Question(
|
||||
sprintf('To which number are we going to send this fake message ? (default to: %s)', $phonenumberFormatted),
|
||||
$phonenumberFormatted
|
||||
);
|
||||
|
||||
$question->setNormalizer(function ($answer): PhoneNumber {
|
||||
if (null === $answer) {
|
||||
throw new UnexpectedValueException('The person is not found');
|
||||
}
|
||||
|
||||
$phone = $this->phoneNumberUtil->parse($answer, 'BE');
|
||||
|
||||
if (!$this->phoneNumberUtil->isPossibleNumberForType($phone, PhoneNumberType::MOBILE)) {
|
||||
throw new UnexpectedValueException('Phone number si not a mobile');
|
||||
}
|
||||
|
||||
return $phone;
|
||||
});
|
||||
|
||||
$phone = $helper->ask($input, $output, $question);
|
||||
|
||||
$question = new ConfirmationQuestion('really send the message to the phone ?');
|
||||
$reallySend = (bool) $helper->ask($input, $output, $question);
|
||||
|
||||
$messages = $this->messageForCalendarBuilder->buildMessageForCalendar($calendar);
|
||||
|
||||
if (0 === count($messages)) {
|
||||
$output->writeln('no message to send to this user');
|
||||
}
|
||||
|
||||
foreach ($messages as $key => $message) {
|
||||
$output->writeln("The short message for SMS {$key} will be: ");
|
||||
$output->writeln($message->getContent());
|
||||
$message->setPhoneNumber($phone);
|
||||
|
||||
if ($reallySend) {
|
||||
$this->transporter->send($message);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -14,18 +14,20 @@ namespace Chill\CalendarBundle\Controller;
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Form\CalendarType;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
|
||||
use Chill\CalendarBundle\Repository\CalendarACLAwareRepositoryInterface;
|
||||
use Chill\CalendarBundle\Repository\CalendarRepository;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Repository\UserRepository;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||
use DateTimeImmutable;
|
||||
use Exception;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
@ -37,11 +39,11 @@ use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
class CalendarController extends AbstractController
|
||||
{
|
||||
private AuthorizationHelper $authorizationHelper;
|
||||
private CalendarACLAwareRepositoryInterface $calendarACLAwareRepository;
|
||||
|
||||
private CalendarRepository $calendarRepository;
|
||||
|
||||
private EventDispatcherInterface $eventDispatcher;
|
||||
private FilterOrderHelperFactoryInterface $filterOrderHelperFactory;
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
@ -54,18 +56,18 @@ class CalendarController extends AbstractController
|
||||
private UserRepository $userRepository;
|
||||
|
||||
public function __construct(
|
||||
AuthorizationHelper $authorizationHelper,
|
||||
CalendarRepository $calendarRepository,
|
||||
EventDispatcherInterface $eventDispatcher,
|
||||
CalendarACLAwareRepositoryInterface $calendarACLAwareRepository,
|
||||
FilterOrderHelperFactoryInterface $filterOrderHelperFactory,
|
||||
LoggerInterface $logger,
|
||||
PaginatorFactory $paginator,
|
||||
RemoteCalendarConnectorInterface $remoteCalendarConnector,
|
||||
SerializerInterface $serializer,
|
||||
UserRepository $userRepository
|
||||
) {
|
||||
$this->authorizationHelper = $authorizationHelper;
|
||||
$this->calendarRepository = $calendarRepository;
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->calendarACLAwareRepository = $calendarACLAwareRepository;
|
||||
$this->filterOrderHelperFactory = $filterOrderHelperFactory;
|
||||
$this->logger = $logger;
|
||||
$this->paginator = $paginator;
|
||||
$this->remoteCalendarConnector = $remoteCalendarConnector;
|
||||
@ -165,7 +167,7 @@ class CalendarController extends AbstractController
|
||||
|
||||
$params = $this->buildParamsToUrl($user, $accompanyingPeriod);
|
||||
|
||||
return $this->redirectToRoute('chill_calendar_calendar_list', $params);
|
||||
return $this->redirectToRoute('chill_calendar_calendar_list_by_period', $params);
|
||||
}
|
||||
|
||||
if ($form->isSubmitted() && !$form->isValid()) {
|
||||
@ -214,48 +216,31 @@ class CalendarController extends AbstractController
|
||||
/**
|
||||
* Lists all Calendar entities.
|
||||
*
|
||||
* @Route("/{_locale}/calendar/calendar/", name="chill_calendar_calendar_list")
|
||||
* @Route("/{_locale}/calendar/calendar/by-period/{id}", name="chill_calendar_calendar_list_by_period")
|
||||
*/
|
||||
public function listAction(Request $request): Response
|
||||
public function listActionByCourse(AccompanyingPeriod $accompanyingPeriod): Response
|
||||
{
|
||||
$view = null;
|
||||
$filterOrder = $this->buildListFilterOrder();
|
||||
['from' => $from, 'to' => $to] = $filterOrder->getDateRangeData('startDate');
|
||||
|
||||
[$user, $accompanyingPeriod] = $this->getEntity($request);
|
||||
$total = $this->calendarACLAwareRepository
|
||||
->countByAccompanyingPeriod($accompanyingPeriod, $from, $to);
|
||||
$paginator = $this->paginator->create($total);
|
||||
$calendarItems = $this->calendarACLAwareRepository->findByAccompanyingPeriod(
|
||||
$accompanyingPeriod,
|
||||
$from,
|
||||
$to,
|
||||
['startDate' => 'DESC'],
|
||||
$paginator->getCurrentPageFirstItemNumber(),
|
||||
$paginator->getItemsPerPage()
|
||||
);
|
||||
|
||||
/*
|
||||
dead code ?
|
||||
if ($user instanceof User) {
|
||||
$calendarItems = $this->calendarRepository->findByUser($user);
|
||||
|
||||
$view = '@ChillCalendar/Calendar/listByUser.html.twig';
|
||||
|
||||
return $this->render($view, [
|
||||
'calendarItems' => $calendarItems,
|
||||
'user' => $user,
|
||||
]);
|
||||
}
|
||||
*/
|
||||
|
||||
if ($accompanyingPeriod instanceof AccompanyingPeriod) {
|
||||
$total = $this->calendarRepository->countByAccompanyingPeriod($accompanyingPeriod);
|
||||
$paginator = $this->paginator->create($total);
|
||||
$calendarItems = $this->calendarRepository->findBy(
|
||||
['accompanyingPeriod' => $accompanyingPeriod],
|
||||
['startDate' => 'DESC'],
|
||||
$paginator->getItemsPerPage(),
|
||||
$paginator->getCurrentPageFirstItemNumber()
|
||||
);
|
||||
|
||||
$view = '@ChillCalendar/Calendar/listByAccompanyingCourse.html.twig';
|
||||
|
||||
return $this->render($view, [
|
||||
'calendarItems' => $calendarItems,
|
||||
'accompanyingCourse' => $accompanyingPeriod,
|
||||
'paginator' => $paginator,
|
||||
]);
|
||||
}
|
||||
|
||||
throw new Exception('Unable to list actions.');
|
||||
return $this->render('@ChillCalendar/Calendar/listByAccompanyingCourse.html.twig', [
|
||||
'calendarItems' => $calendarItems,
|
||||
'accompanyingCourse' => $accompanyingPeriod,
|
||||
'paginator' => $paginator,
|
||||
'filterOrder' => $filterOrder,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -328,7 +313,7 @@ class CalendarController extends AbstractController
|
||||
|
||||
$params = $this->buildParamsToUrl($user, $accompanyingPeriod);
|
||||
|
||||
return $this->redirectToRoute('chill_calendar_calendar_list', $params);
|
||||
return $this->redirectToRoute('chill_calendar_calendar_list_by_period', $params);
|
||||
}
|
||||
|
||||
if ($form->isSubmitted() && !$form->isValid()) {
|
||||
@ -421,6 +406,14 @@ class CalendarController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
private function buildListFilterOrder(): FilterOrderHelper
|
||||
{
|
||||
$filterOrder = $this->filterOrderHelperFactory->create(self::class);
|
||||
$filterOrder->addDateRange('startDate', 'chill_calendar.start date filter', new DateTimeImmutable('3 days ago'), null);
|
||||
|
||||
return $filterOrder->build();
|
||||
}
|
||||
|
||||
private function buildParamsToUrl(?User $user, ?AccompanyingPeriod $accompanyingPeriod): array
|
||||
{
|
||||
$params = [];
|
||||
@ -430,7 +423,7 @@ class CalendarController extends AbstractController
|
||||
}
|
||||
|
||||
if (null !== $accompanyingPeriod) {
|
||||
$params['accompanying_period_id'] = $accompanyingPeriod->getId();
|
||||
$params['id'] = $accompanyingPeriod->getId();
|
||||
}
|
||||
|
||||
return $params;
|
||||
|
@ -13,6 +13,7 @@ namespace Chill\CalendarBundle\Controller;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Entity\Invite;
|
||||
use Chill\CalendarBundle\Messenger\Message\InviteUpdateMessage;
|
||||
use Chill\CalendarBundle\Security\Voter\InviteVoter;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
@ -20,6 +21,7 @@ use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use function in_array;
|
||||
@ -28,12 +30,15 @@ class InviteApiController
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
private MessageBusInterface $messageBus;
|
||||
|
||||
private Security $security;
|
||||
|
||||
public function __construct(Security $security, EntityManagerInterface $entityManager)
|
||||
public function __construct(EntityManagerInterface $entityManager, MessageBusInterface $messageBus, Security $security)
|
||||
{
|
||||
$this->security = $security;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->messageBus = $messageBus;
|
||||
$this->security = $security;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -57,13 +62,15 @@ class InviteApiController
|
||||
throw new AccessDeniedHttpException('not allowed to answer on this invitation');
|
||||
}
|
||||
|
||||
if (!in_array($answer, Invite::STATUSES, true) || Invite::PENDING === $answer) {
|
||||
if (!in_array($answer, Invite::STATUSES, true)) {
|
||||
throw new BadRequestHttpException('answer not valid');
|
||||
}
|
||||
|
||||
$invite->setStatus($answer);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->messageBus->dispatch(new InviteUpdateMessage($invite, $this->security->getUser()));
|
||||
|
||||
return new JsonResponse(null, Response::HTTP_ACCEPTED, [], false);
|
||||
}
|
||||
}
|
||||
|
@ -44,9 +44,7 @@ class RemoteCalendarConnectAzureController
|
||||
|
||||
return $this->clientRegistry
|
||||
->getClient('azure') // key used in config/packages/knpu_oauth2_client.yaml
|
||||
->redirect([
|
||||
'https://graph.microsoft.com/.default', 'offline_access',
|
||||
]);
|
||||
->redirect(['https://graph.microsoft.com/.default', 'offline_access'], []);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -59,7 +57,7 @@ class RemoteCalendarConnectAzureController
|
||||
|
||||
try {
|
||||
/** @var AccessToken $token */
|
||||
$token = $client->getAccessToken();
|
||||
$token = $client->getAccessToken([]);
|
||||
|
||||
$this->MSGraphTokenStorage->setToken($token);
|
||||
} catch (IdentityProviderException $e) {
|
||||
|
@ -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 Chill\CalendarBundle\Controller;
|
||||
|
||||
use Chill\CalendarBundle\Messenger\Message\MSGraphChangeNotificationMessage;
|
||||
use JsonException;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
|
||||
class RemoteCalendarMSGraphSyncController
|
||||
{
|
||||
private MessageBusInterface $messageBus;
|
||||
|
||||
public function __construct(MessageBusInterface $messageBus)
|
||||
{
|
||||
$this->messageBus = $messageBus;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/public/incoming-hook/calendar/msgraph/events/{userId}", name="chill_calendar_remote_msgraph_incoming_webhook_events",
|
||||
* methods={"POST"})
|
||||
*/
|
||||
public function webhookCalendarReceiver(int $userId, Request $request): Response
|
||||
{
|
||||
if ($request->query->has('validationToken')) {
|
||||
return new Response($request->query->get('validationToken'), Response::HTTP_OK, [
|
||||
'content-type' => 'text/plain',
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$body = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException $e) {
|
||||
throw new BadRequestHttpException('could not decode json', $e);
|
||||
}
|
||||
|
||||
$this->messageBus->dispatch(new MSGraphChangeNotificationMessage($body, $userId));
|
||||
|
||||
return new Response('', Response::HTTP_ACCEPTED);
|
||||
}
|
||||
}
|
@ -39,6 +39,12 @@ class ChillCalendarExtension extends Extension implements PrependExtensionInterf
|
||||
$loader->load('services/remote_calendar.yaml');
|
||||
|
||||
$container->setParameter('chill_calendar', $config);
|
||||
|
||||
if ($config['short_messages']['enabled']) {
|
||||
$container->setParameter('chill_calendar.short_messages', $config['short_messages']);
|
||||
} else {
|
||||
$container->setParameter('chill_calendar.short_messages', null);
|
||||
}
|
||||
}
|
||||
|
||||
public function prepend(ContainerBuilder $container)
|
||||
|
@ -26,7 +26,12 @@ class Configuration implements ConfigurationInterface
|
||||
$treeBuilder = new TreeBuilder('chill_calendar');
|
||||
$rootNode = $treeBuilder->getRootNode('chill_calendar');
|
||||
|
||||
$rootNode->children()
|
||||
$rootNode
|
||||
->children()
|
||||
->arrayNode('short_messages')
|
||||
->canBeDisabled()
|
||||
->children()->end()
|
||||
->end() // end for short_messages
|
||||
->arrayNode('remote_calendars_sync')->canBeEnabled()
|
||||
->children()
|
||||
->arrayNode('microsoft_graph')->canBeEnabled()
|
||||
|
@ -38,7 +38,10 @@ use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
use function in_array;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="chill_calendar.calendar", indexes={@ORM\Index(name="idx_calendar_remote", columns={"remoteId"})}))
|
||||
* @ORM\Table(
|
||||
* name="chill_calendar.calendar",
|
||||
* uniqueConstraints={@ORM\UniqueConstraint(name="idx_calendar_remote", columns={"remoteId"}, options={"where": "remoteId <> ''"})}
|
||||
* )
|
||||
* @ORM\Entity
|
||||
*/
|
||||
class Calendar implements TrackCreationInterface, TrackUpdateInterface
|
||||
@ -49,6 +52,12 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface
|
||||
|
||||
use TrackUpdateTrait;
|
||||
|
||||
public const SMS_CANCEL_PENDING = 'sms_cancel_pending';
|
||||
|
||||
public const SMS_PENDING = 'sms_pending';
|
||||
|
||||
public const SMS_SENT = 'sms_sent';
|
||||
|
||||
public const STATUS_CANCELED = 'canceled';
|
||||
|
||||
public const STATUS_MOVED = 'moved';
|
||||
@ -102,7 +111,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface
|
||||
private CommentEmbeddable $comment;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetimetz_immutable")
|
||||
* @ORM\Column(type="datetime_immutable", nullable=false)
|
||||
* @Serializer\Groups({"calendar:read", "read"})
|
||||
*/
|
||||
private ?DateTimeImmutable $endDate = null;
|
||||
@ -166,10 +175,15 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface
|
||||
/**
|
||||
* @ORM\Column(type="boolean", nullable=true)
|
||||
*/
|
||||
private ?bool $sendSMS;
|
||||
private ?bool $sendSMS = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetimetz_immutable")
|
||||
* @ORM\Column(type="text", nullable=false, options={"default": Calendar::SMS_PENDING})
|
||||
*/
|
||||
private string $smsStatus = self::SMS_PENDING;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime_immutable", nullable=false)
|
||||
* @Serializer\Groups({"calendar:read", "read"})
|
||||
*/
|
||||
private ?DateTimeImmutable $startDate = null;
|
||||
@ -365,6 +379,11 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface
|
||||
return $this->sendSMS;
|
||||
}
|
||||
|
||||
public function getSmsStatus(): string
|
||||
{
|
||||
return $this->smsStatus;
|
||||
}
|
||||
|
||||
public function getStartDate(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->startDate;
|
||||
@ -486,7 +505,10 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface
|
||||
}
|
||||
|
||||
$this->calendarRange = $calendarRange;
|
||||
$this->calendarRange->setCalendar($this);
|
||||
|
||||
if ($this->calendarRange instanceof CalendarRange) {
|
||||
$this->calendarRange->setCalendar($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -545,6 +567,13 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setSmsStatus(string $smsStatus): self
|
||||
{
|
||||
$this->smsStatus = $smsStatus;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setStartDate(DateTimeImmutable $startDate): self
|
||||
{
|
||||
$this->startDate = $startDate;
|
||||
@ -556,6 +585,10 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface
|
||||
{
|
||||
$this->status = $status;
|
||||
|
||||
if (self::STATUS_CANCELED === $status && $this->getSmsStatus() === self::SMS_SENT) {
|
||||
$this->setSmsStatus(self::SMS_CANCEL_PENDING);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,10 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="chill_calendar.calendar_range", indexes={@ORM\Index(name="idx_calendar_range_remote", columns={"remoteId"})})
|
||||
* @ORM\Table(
|
||||
* name="chill_calendar.calendar_range",
|
||||
* uniqueConstraints={@ORM\UniqueConstraint(name="idx_calendar_range_remote", columns={"remoteId"}, options={"where": "remoteId <> ''"})}
|
||||
* )
|
||||
* @ORM\Entity
|
||||
*/
|
||||
class CalendarRange implements TrackCreationInterface, TrackUpdateInterface
|
||||
@ -38,7 +41,7 @@ class CalendarRange implements TrackCreationInterface, TrackUpdateInterface
|
||||
private ?Calendar $calendar = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetimetz_immutable")
|
||||
* @ORM\Column(type="datetime_immutable", nullable=false)
|
||||
* @groups({"read", "write", "calendar:read"})
|
||||
*/
|
||||
private ?DateTimeImmutable $endDate = null;
|
||||
@ -52,7 +55,7 @@ class CalendarRange implements TrackCreationInterface, TrackUpdateInterface
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetimetz_immutable")
|
||||
* @ORM\Column(type="datetime_immutable", nullable=false)
|
||||
* @groups({"read", "write", "calendar:read"})
|
||||
*/
|
||||
private ?DateTimeImmutable $startDate = null;
|
||||
@ -63,7 +66,7 @@ class CalendarRange implements TrackCreationInterface, TrackUpdateInterface
|
||||
*/
|
||||
private ?User $user = null;
|
||||
|
||||
public function getCalendar(): Calendar
|
||||
public function getCalendar(): ?Calendar
|
||||
{
|
||||
return $this->calendar;
|
||||
}
|
||||
|
@ -21,11 +21,21 @@ use LogicException;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="chill_calendar.invite")
|
||||
* An invitation for another user to a Calendar.
|
||||
*
|
||||
* The event/calendar in the user may have a different id than the mainUser. We add then fields to store the
|
||||
* remote id of this event in the remote calendar.
|
||||
*
|
||||
* @ORM\Table(
|
||||
* name="chill_calendar.invite",
|
||||
* uniqueConstraints={@ORM\UniqueConstraint(name="idx_calendar_invite_remote", columns={"remoteId"}, options={"where": "remoteId <> ''"})}
|
||||
* )
|
||||
* @ORM\Entity
|
||||
*/
|
||||
class Invite implements TrackUpdateInterface, TrackCreationInterface
|
||||
{
|
||||
use RemoteCalendarTrait;
|
||||
|
||||
use TrackCreationTrait;
|
||||
|
||||
use TrackUpdateTrait;
|
||||
|
@ -18,12 +18,9 @@ use Chill\MainBundle\Form\DataTransformer\IdToLocationDataTransformer;
|
||||
use Chill\MainBundle\Form\DataTransformer\IdToUserDataTransformer;
|
||||
use Chill\MainBundle\Form\DataTransformer\IdToUsersDataTransformer;
|
||||
use Chill\MainBundle\Form\Type\CommentType;
|
||||
use Chill\MainBundle\Form\Type\PrivateCommentType;
|
||||
use Chill\PersonBundle\Form\DataTransformer\PersonsToIdDataTransformer;
|
||||
use Chill\ThirdPartyBundle\Form\DataTransformer\ThirdPartiesToIdDataTransformer;
|
||||
use Chill\MainBundle\Form\Type\PrivateCommentType;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
|
@ -37,9 +37,9 @@ class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
|
||||
|
||||
if ($this->security->isGranted(CalendarVoter::SEE, $period)) {
|
||||
$menu->addChild($this->translator->trans('Calendar'), [
|
||||
'route' => 'chill_calendar_calendar_list',
|
||||
'route' => 'chill_calendar_calendar_list_by_period',
|
||||
'routeParameters' => [
|
||||
'accompanying_period_id' => $period->getId(),
|
||||
'id' => $period->getId(),
|
||||
], ])
|
||||
->setExtras(['order' => 35]);
|
||||
}
|
||||
|
@ -0,0 +1,48 @@
|
||||
<?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\CalendarBundle\Messenger\Handler;
|
||||
|
||||
use Chill\CalendarBundle\Messenger\Message\InviteUpdateMessage;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
|
||||
use Chill\CalendarBundle\Repository\InviteRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
|
||||
/**
|
||||
* @AsMessageHandler
|
||||
*/
|
||||
class InviteUpdateHandler implements MessageHandlerInterface
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private InviteRepository $inviteRepository;
|
||||
|
||||
private RemoteCalendarConnectorInterface $remoteCalendarConnector;
|
||||
|
||||
public function __construct(EntityManagerInterface $em, InviteRepository $inviteRepository, RemoteCalendarConnectorInterface $remoteCalendarConnector)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->inviteRepository = $inviteRepository;
|
||||
$this->remoteCalendarConnector = $remoteCalendarConnector;
|
||||
}
|
||||
|
||||
public function __invoke(InviteUpdateMessage $inviteUpdateMessage): void
|
||||
{
|
||||
if (null === $invite = $this->inviteRepository->find($inviteUpdateMessage->getInviteId())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->remoteCalendarConnector->syncInvite($invite);
|
||||
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
@ -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\CalendarBundle\Messenger\Handler;
|
||||
|
||||
use Chill\CalendarBundle\Messenger\Message\MSGraphChangeNotificationMessage;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MapCalendarToUser;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteToLocalSync\CalendarRangeSyncer;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteToLocalSync\CalendarSyncer;
|
||||
use Chill\CalendarBundle\Repository\CalendarRangeRepository;
|
||||
use Chill\CalendarBundle\Repository\CalendarRepository;
|
||||
use Chill\MainBundle\Repository\UserRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
|
||||
/**
|
||||
* Handle notification of chagnes from MSGraph.
|
||||
*
|
||||
* @AsMessageHandler
|
||||
*/
|
||||
class MSGraphChangeNotificationHandler implements MessageHandlerInterface
|
||||
{
|
||||
private CalendarRangeRepository $calendarRangeRepository;
|
||||
|
||||
private CalendarRangeSyncer $calendarRangeSyncer;
|
||||
|
||||
private CalendarRepository $calendarRepository;
|
||||
|
||||
private CalendarSyncer $calendarSyncer;
|
||||
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private MapCalendarToUser $mapCalendarToUser;
|
||||
|
||||
private UserRepository $userRepository;
|
||||
|
||||
public function __construct(
|
||||
CalendarRangeRepository $calendarRangeRepository,
|
||||
CalendarRangeSyncer $calendarRangeSyncer,
|
||||
CalendarRepository $calendarRepository,
|
||||
CalendarSyncer $calendarSyncer,
|
||||
EntityManagerInterface $em,
|
||||
LoggerInterface $logger,
|
||||
MapCalendarToUser $mapCalendarToUser,
|
||||
UserRepository $userRepository
|
||||
) {
|
||||
$this->calendarRangeRepository = $calendarRangeRepository;
|
||||
$this->calendarRangeSyncer = $calendarRangeSyncer;
|
||||
$this->calendarRepository = $calendarRepository;
|
||||
$this->calendarSyncer = $calendarSyncer;
|
||||
$this->em = $em;
|
||||
$this->logger = $logger;
|
||||
$this->mapCalendarToUser = $mapCalendarToUser;
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
public function __invoke(MSGraphChangeNotificationMessage $changeNotificationMessage): void
|
||||
{
|
||||
$user = $this->userRepository->find($changeNotificationMessage->getUserId());
|
||||
|
||||
if (null === $user) {
|
||||
$this->logger->warning(__CLASS__ . ' notification concern non-existent user, skipping');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($changeNotificationMessage->getContent()['value'] as $notification) {
|
||||
$secret = $this->mapCalendarToUser->getSubscriptionSecret($user);
|
||||
|
||||
if ($secret !== ($notification['clientState'] ?? -1)) {
|
||||
$this->logger->warning(__CLASS__ . ' could not validate secret, skipping');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$remoteId = $notification['resourceData']['id'];
|
||||
|
||||
// is this a calendar range ?
|
||||
if (null !== $calendarRange = $this->calendarRangeRepository->findOneBy(['remoteId' => $remoteId])) {
|
||||
$this->calendarRangeSyncer->handleCalendarRangeSync($calendarRange, $notification, $user);
|
||||
$this->em->flush();
|
||||
} elseif (null !== $calendar = $this->calendarRepository->findOneBy(['remoteId' => $remoteId])) {
|
||||
$this->calendarSyncer->handleCalendarSync($calendar, $notification, $user);
|
||||
$this->em->flush();
|
||||
} else {
|
||||
$this->logger->info(__CLASS__ . ' id not found in any calendar nor calendar range');
|
||||
}
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
<?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\CalendarBundle\Messenger\Message;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Invite;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
|
||||
class InviteUpdateMessage
|
||||
{
|
||||
private int $byUserId;
|
||||
|
||||
private int $inviteId;
|
||||
|
||||
public function __construct(Invite $invite, User $byUser)
|
||||
{
|
||||
$this->inviteId = $invite->getId();
|
||||
$this->byUserId = $byUser->getId();
|
||||
}
|
||||
|
||||
public function getByUserId(): int
|
||||
{
|
||||
return $this->byUserId;
|
||||
}
|
||||
|
||||
public function getInviteId(): int
|
||||
{
|
||||
return $this->inviteId;
|
||||
}
|
||||
}
|
@ -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\CalendarBundle\Messenger\Message;
|
||||
|
||||
class MSGraphChangeNotificationMessage
|
||||
{
|
||||
private array $content;
|
||||
|
||||
private int $userId;
|
||||
|
||||
public function __construct(array $content, int $userId)
|
||||
{
|
||||
$this->content = $content;
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
public function getContent(): array
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function getUserId(): int
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
<?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\CalendarBundle\RemoteCalendar\Connector\MSGraph;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use DateTimeImmutable;
|
||||
use LogicException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
|
||||
/**
|
||||
* Create a subscription for a user.
|
||||
*/
|
||||
class EventsOnUserSubscriptionCreator
|
||||
{
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private MachineHttpClient $machineHttpClient;
|
||||
|
||||
private MapCalendarToUser $mapCalendarToUser;
|
||||
|
||||
private UrlGeneratorInterface $urlGenerator;
|
||||
|
||||
public function __construct(
|
||||
LoggerInterface $logger,
|
||||
MachineHttpClient $machineHttpClient,
|
||||
MapCalendarToUser $mapCalendarToUser,
|
||||
UrlGeneratorInterface $urlGenerator
|
||||
) {
|
||||
$this->logger = $logger;
|
||||
$this->machineHttpClient = $machineHttpClient;
|
||||
$this->mapCalendarToUser = $mapCalendarToUser;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
|
||||
*
|
||||
* @return array<secret: string, id: string, expiration: int>
|
||||
*/
|
||||
public function createSubscriptionForUser(User $user, DateTimeImmutable $expiration): array
|
||||
{
|
||||
if (null === $userId = $this->mapCalendarToUser->getUserId($user)) {
|
||||
throw new LogicException('no user id');
|
||||
}
|
||||
|
||||
$subscription = [
|
||||
'changeType' => 'deleted,updated',
|
||||
'notificationUrl' => $this->urlGenerator->generate(
|
||||
'chill_calendar_remote_msgraph_incoming_webhook_events',
|
||||
['userId' => $user->getId()],
|
||||
UrlGeneratorInterface::ABSOLUTE_URL
|
||||
),
|
||||
'resource' => "/users/{$userId}/calendar/events",
|
||||
'clientState' => $secret = base64_encode(openssl_random_pseudo_bytes(92, $cstrong)),
|
||||
'expirationDateTime' => $expiration->format(DateTimeImmutable::ATOM),
|
||||
];
|
||||
|
||||
try {
|
||||
$subs = $this->machineHttpClient->request(
|
||||
'POST',
|
||||
'/v1.0/subscriptions',
|
||||
[
|
||||
'json' => $subscription,
|
||||
]
|
||||
)->toArray();
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
$this->logger->error('could not create subscription for user events', [
|
||||
'body' => $e->getResponse()->getContent(false),
|
||||
]);
|
||||
|
||||
return ['secret' => '', 'id' => '', 'expiration' => 0];
|
||||
}
|
||||
|
||||
return ['secret' => $secret, 'id' => $subs['id'], 'expiration' => $expiration->getTimestamp()];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
|
||||
*
|
||||
* @return array<secret: string, id: string, expiration: int>
|
||||
*/
|
||||
public function renewSubscriptionForUser(User $user, DateTimeImmutable $expiration): array
|
||||
{
|
||||
if (null === $userId = $this->mapCalendarToUser->getUserId($user)) {
|
||||
throw new LogicException('no user id');
|
||||
}
|
||||
|
||||
if (null === $subscriptionId = $this->mapCalendarToUser->getActiveSubscriptionId($user)) {
|
||||
throw new LogicException('no user id');
|
||||
}
|
||||
|
||||
$subscription = [
|
||||
'expirationDateTime' => $expiration->format(DateTimeImmutable::ATOM),
|
||||
];
|
||||
|
||||
try {
|
||||
$subs = $this->machineHttpClient->request(
|
||||
'PATCH',
|
||||
"/v1.0/subscriptions/{$subscriptionId}",
|
||||
[
|
||||
'json' => $subscription,
|
||||
]
|
||||
)->toArray();
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
$this->logger->error('could not patch subscription for user events', [
|
||||
'body' => $e->getResponse()->getContent(false),
|
||||
]);
|
||||
|
||||
return ['secret' => '', 'id' => '', 'expiration' => 0];
|
||||
}
|
||||
|
||||
return ['secret' => $subs['clientState'], 'id' => $subs['id'], 'expiration' => $expiration->getTimestamp()];
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
<?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\CalendarBundle\RemoteCalendar\Connector\MSGraph;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use DateInterval;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\ORM\Query\ResultSetMappingBuilder;
|
||||
use function strtr;
|
||||
|
||||
/**
|
||||
* Contains classes and methods for fetching users with some calendar metadatas.
|
||||
*/
|
||||
class MSGraphUserRepository
|
||||
{
|
||||
private const MOST_OLD_SUBSCRIPTION_OR_ANY_MS_GRAPH = <<<'SQL'
|
||||
select
|
||||
{select}
|
||||
from users u
|
||||
where
|
||||
NOT attributes ?? 'msgraph'
|
||||
OR NOT attributes->'msgraph' ?? 'subscription_events_expiration'
|
||||
OR (attributes->'msgraph' ?? 'subscription_events_expiration' AND (attributes->'msgraph'->>'subscription_events_expiration')::int < EXTRACT(EPOCH FROM (NOW() + :interval::interval)))
|
||||
LIMIT :limit OFFSET :offset
|
||||
;
|
||||
SQL;
|
||||
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function countByMostOldSubscriptionOrWithoutSubscriptionOrData(DateInterval $interval): int
|
||||
{
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('c', 'c');
|
||||
|
||||
$sql = strtr(self::MOST_OLD_SUBSCRIPTION_OR_ANY_MS_GRAPH, [
|
||||
'{select}' => 'COUNT(u) AS c',
|
||||
'LIMIT :limit OFFSET :offset' => '',
|
||||
]);
|
||||
|
||||
return $this->entityManager->createNativeQuery($sql, $rsm)->setParameters([
|
||||
'interval' => $interval,
|
||||
])->getSingleScalarResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|User[]
|
||||
*/
|
||||
public function findByMostOldSubscriptionOrWithoutSubscriptionOrData(DateInterval $interval, int $limit = 50, int $offset = 0): array
|
||||
{
|
||||
$rsm = new ResultSetMappingBuilder($this->entityManager);
|
||||
$rsm->addRootEntityFromClassMetadata(User::class, 'u');
|
||||
|
||||
return $this->entityManager->createNativeQuery(
|
||||
strtr(self::MOST_OLD_SUBSCRIPTION_OR_ANY_MS_GRAPH, ['{select}' => $rsm->generateSelectClause()]),
|
||||
$rsm
|
||||
)->setParameters([
|
||||
'interval' => $interval,
|
||||
'limit' => $limit,
|
||||
'offset' => $offset,
|
||||
])->getResult();
|
||||
}
|
||||
}
|
@ -34,6 +34,10 @@ class MachineHttpClient implements HttpClientInterface
|
||||
$this->machineTokenStorage = $machineTokenStorage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
|
||||
* @throws LogicException if method is not supported
|
||||
*/
|
||||
public function request(string $method, string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
$options['headers'] = array_merge(
|
||||
|
@ -12,15 +12,24 @@ declare(strict_types=1);
|
||||
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use DateTimeImmutable;
|
||||
use LogicException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use function array_key_exists;
|
||||
|
||||
/**
|
||||
* Write metadata to user, which allow to find his default calendar.
|
||||
*/
|
||||
class MapCalendarToUser
|
||||
{
|
||||
public const EXPIRATION_SUBSCRIPTION_EVENT = 'subscription_events_expiration';
|
||||
|
||||
public const ID_SUBSCRIPTION_EVENT = 'subscription_events_id';
|
||||
|
||||
public const METADATA_KEY = 'msgraph';
|
||||
|
||||
public const SECRET_SUBSCRIPTION_EVENT = 'subscription_events_secret';
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private MachineHttpClient $machineHttpClient;
|
||||
@ -33,6 +42,19 @@ class MapCalendarToUser
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function getActiveSubscriptionId(User $user): string
|
||||
{
|
||||
if (!array_key_exists(self::METADATA_KEY, $user->getAttributes())) {
|
||||
throw new LogicException('do not contains msgraph metadata');
|
||||
}
|
||||
|
||||
if (!array_key_exists(self::ID_SUBSCRIPTION_EVENT, $user->getAttributes()[self::METADATA_KEY])) {
|
||||
throw new LogicException('do not contains metadata for subscription id');
|
||||
}
|
||||
|
||||
return $user->getAttributes()[self::METADATA_KEY][self::ID_SUBSCRIPTION_EVENT];
|
||||
}
|
||||
|
||||
public function getCalendarId(User $user): ?string
|
||||
{
|
||||
if (null === $msKey = ($user->getAttributes()[self::METADATA_KEY] ?? null)) {
|
||||
@ -51,6 +73,19 @@ class MapCalendarToUser
|
||||
return $value[0] ?? null;
|
||||
}
|
||||
|
||||
public function getSubscriptionSecret(User $user): string
|
||||
{
|
||||
if (!array_key_exists(self::METADATA_KEY, $user->getAttributes())) {
|
||||
throw new LogicException('do not contains msgraph metadata');
|
||||
}
|
||||
|
||||
if (!array_key_exists(self::SECRET_SUBSCRIPTION_EVENT, $user->getAttributes()[self::METADATA_KEY])) {
|
||||
throw new LogicException('do not contains secret in msgraph');
|
||||
}
|
||||
|
||||
return $user->getAttributes()[self::METADATA_KEY][self::SECRET_SUBSCRIPTION_EVENT];
|
||||
}
|
||||
|
||||
public function getUserByEmail(string $email): ?array
|
||||
{
|
||||
$value = $this->machineHttpClient->request('GET', 'users', [
|
||||
@ -69,16 +104,48 @@ class MapCalendarToUser
|
||||
return $msKey['id'] ?? null;
|
||||
}
|
||||
|
||||
public function hasActiveSubscription(User $user): bool
|
||||
{
|
||||
if (!array_key_exists(self::METADATA_KEY, $user->getAttributes())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!array_key_exists(self::EXPIRATION_SUBSCRIPTION_EVENT, $user->getAttributes()[self::METADATA_KEY])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->getAttributes()[self::METADATA_KEY][self::EXPIRATION_SUBSCRIPTION_EVENT]
|
||||
>= (new DateTimeImmutable('now'))->getTimestamp();
|
||||
}
|
||||
|
||||
public function hasSubscriptionSecret(User $user): bool
|
||||
{
|
||||
if (!array_key_exists(self::METADATA_KEY, $user->getAttributes())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array_key_exists(self::SECRET_SUBSCRIPTION_EVENT, $user->getAttributes()[self::METADATA_KEY]);
|
||||
}
|
||||
|
||||
public function hasUserId(User $user): bool
|
||||
{
|
||||
if (!array_key_exists(self::METADATA_KEY, $user->getAttributes())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array_key_exists('id', $user->getAttributes()[self::METADATA_KEY]);
|
||||
}
|
||||
|
||||
public function writeMetadata(User $user): User
|
||||
{
|
||||
if (null === $userData = $this->getUserByEmail($user->getEmailCanonical())) {
|
||||
$this->logger->warning('[MapCalendarToUser] could find user on msgraph', ['userId' => $user->getId(), 'email' => $user->getEmailCanonical()]);
|
||||
$this->logger->warning('[MapCalendarToUser] could not find user on msgraph', ['userId' => $user->getId(), 'email' => $user->getEmailCanonical()]);
|
||||
|
||||
return $this->writeNullData($user);
|
||||
}
|
||||
|
||||
if (null === $defaultCalendar = $this->getDefaultUserCalendar($userData['id'])) {
|
||||
$this->logger->warning('[MapCalendarToUser] could find default calendar', ['userId' => $user->getId(), 'email' => $user->getEmailCanonical()]);
|
||||
$this->logger->warning('[MapCalendarToUser] could not find default calendar', ['userId' => $user->getId(), 'email' => $user->getEmailCanonical()]);
|
||||
|
||||
return $this->writeNullData($user);
|
||||
}
|
||||
@ -90,6 +157,26 @@ class MapCalendarToUser
|
||||
]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $expiration the expiration time as unix timestamp
|
||||
*/
|
||||
public function writeSubscriptionMetadata(
|
||||
User $user,
|
||||
int $expiration,
|
||||
?string $id = null,
|
||||
?string $secret = null
|
||||
): void {
|
||||
$user->setAttributeByDomain(self::METADATA_KEY, self::EXPIRATION_SUBSCRIPTION_EVENT, $expiration);
|
||||
|
||||
if (null !== $id) {
|
||||
$user->setAttributeByDomain(self::METADATA_KEY, self::ID_SUBSCRIPTION_EVENT, $id);
|
||||
}
|
||||
|
||||
if (null !== $secret) {
|
||||
$user->setAttributeByDomain(self::METADATA_KEY, self::SECRET_SUBSCRIPTION_EVENT, $secret);
|
||||
}
|
||||
}
|
||||
|
||||
private function writeNullData(User $user): User
|
||||
{
|
||||
return $user->unsetAttribute(self::METADATA_KEY);
|
||||
|
@ -19,6 +19,7 @@ use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Templating\EngineInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
@ -28,10 +29,16 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
*/
|
||||
class RemoteEventConverter
|
||||
{
|
||||
/**
|
||||
* valid when the remote string contains also a timezone, like in
|
||||
* lastModifiedDate.
|
||||
*/
|
||||
public const REMOTE_DATETIMEZONE_FORMAT = 'Y-m-d\\TH:i:s.u?P';
|
||||
|
||||
private const REMOTE_DATE_FORMAT = 'Y-m-d\TH:i:s.u0';
|
||||
|
||||
private const REMOTE_DATETIME_WITHOUT_TZ_FORMAT = 'Y-m-d\TH:i:s.u?';
|
||||
|
||||
private DateTimeZone $defaultDateTimeZone;
|
||||
|
||||
private EngineInterface $engine;
|
||||
@ -112,6 +119,7 @@ class RemoteEventConverter
|
||||
['calendar' => $calendar]
|
||||
),
|
||||
],
|
||||
'responseRequested' => true,
|
||||
],
|
||||
$this->calendarToEventAttendeesOnly($calendar)
|
||||
);
|
||||
@ -146,6 +154,34 @@ class RemoteEventConverter
|
||||
);
|
||||
}
|
||||
|
||||
public static function convertStringDateWithoutTimezone(string $date): DateTimeImmutable
|
||||
{
|
||||
$d = DateTimeImmutable::createFromFormat(
|
||||
self::REMOTE_DATETIME_WITHOUT_TZ_FORMAT,
|
||||
$date,
|
||||
self::getRemoteTimeZone()
|
||||
);
|
||||
|
||||
if (false === $d) {
|
||||
throw new RuntimeException("could not convert string date to datetime: {$date}");
|
||||
}
|
||||
|
||||
return $d->setTimezone((new DateTimeImmutable())->getTimezone());
|
||||
}
|
||||
|
||||
public static function convertStringDateWithTimezone(string $date): DateTimeImmutable
|
||||
{
|
||||
$d = DateTimeImmutable::createFromFormat(self::REMOTE_DATETIMEZONE_FORMAT, $date);
|
||||
|
||||
if (false === $d) {
|
||||
throw new RuntimeException("could not convert string date to datetime: {$date}");
|
||||
}
|
||||
|
||||
$d->setTimezone((new DateTimeImmutable())->getTimezone());
|
||||
|
||||
return $d;
|
||||
}
|
||||
|
||||
public function convertToRemote(array $event): RemoteEvent
|
||||
{
|
||||
$startDate =
|
||||
|
@ -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\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteToLocalSync;
|
||||
|
||||
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineHttpClient;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteEventConverter;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class CalendarRangeSyncer
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private HttpClientInterface $machineHttpClient;
|
||||
|
||||
/**
|
||||
* @param MachineHttpClient $machineHttpClient
|
||||
*/
|
||||
public function __construct(
|
||||
EntityManagerInterface $em,
|
||||
LoggerInterface $logger,
|
||||
HttpClientInterface $machineHttpClient
|
||||
) {
|
||||
$this->em = $em;
|
||||
$this->logger = $logger;
|
||||
$this->machineHttpClient = $machineHttpClient;
|
||||
}
|
||||
|
||||
public function handleCalendarRangeSync(CalendarRange $calendarRange, array $notification, User $user): void
|
||||
{
|
||||
switch ($notification['changeType']) {
|
||||
case 'deleted':
|
||||
// test if the notification is not linked to a Calendar
|
||||
if (null !== $calendarRange->getCalendar()) {
|
||||
return;
|
||||
}
|
||||
$calendarRange->preventEnqueueChanges = true;
|
||||
|
||||
$this->logger->info(__CLASS__ . ' remove a calendar range because deleted on remote calendar');
|
||||
$this->em->remove($calendarRange);
|
||||
|
||||
break;
|
||||
|
||||
case 'updated':
|
||||
try {
|
||||
$new = $this->machineHttpClient->request(
|
||||
'GET',
|
||||
$notification['resource']
|
||||
)->toArray();
|
||||
} catch (ClientExceptionInterface $clientException) {
|
||||
$this->logger->warning(__CLASS__ . ' could not retrieve event from ms graph. Already deleted ?', [
|
||||
'calendarRangeId' => $calendarRange->getId(),
|
||||
'remoteEventId' => $notification['resource'],
|
||||
]);
|
||||
|
||||
throw $clientException;
|
||||
}
|
||||
|
||||
$lastModified = RemoteEventConverter::convertStringDateWithTimezone($new['lastModifiedDateTime']);
|
||||
|
||||
if ($calendarRange->getRemoteAttributes()['lastModifiedDateTime'] === $lastModified->getTimestamp()) {
|
||||
$this->logger->info(__CLASS__ . ' change key is equals. Source is probably a local update', [
|
||||
'calendarRangeId' => $calendarRange->getId(),
|
||||
'remoteEventId' => $notification['resource'],
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$startDate = RemoteEventConverter::convertStringDateWithoutTimezone($new['start']['dateTime']);
|
||||
$endDate = RemoteEventConverter::convertStringDateWithoutTimezone($new['end']['dateTime']);
|
||||
|
||||
$calendarRange
|
||||
->setStartDate($startDate)->setEndDate($endDate)
|
||||
->addRemoteAttributes([
|
||||
'lastModifiedDateTime' => $lastModified->getTimestamp(),
|
||||
'changeKey' => $new['changeKey'],
|
||||
])
|
||||
->preventEnqueueChanges = true;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new RuntimeException('This changeType is not suppored: ' . $notification['changeType']);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,182 @@
|
||||
<?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\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteToLocalSync;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Entity\Invite;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteEventConverter;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||
use LogicException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use function in_array;
|
||||
|
||||
class CalendarSyncer
|
||||
{
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private HttpClientInterface $machineHttpClient;
|
||||
|
||||
private UserRepositoryInterface $userRepository;
|
||||
|
||||
public function __construct(LoggerInterface $logger, HttpClientInterface $machineHttpClient, UserRepositoryInterface $userRepository)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
$this->machineHttpClient = $machineHttpClient;
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
public function handleCalendarSync(Calendar $calendar, array $notification, User $user): void
|
||||
{
|
||||
switch ($notification['changeType']) {
|
||||
case 'deleted':
|
||||
$this->handleDeleteCalendar($calendar, $notification, $user);
|
||||
|
||||
break;
|
||||
|
||||
case 'updated':
|
||||
$this->handleUpdateCalendar($calendar, $notification, $user);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new RuntimeException('this change type is not supported: ' . $notification['changeType']);
|
||||
}
|
||||
}
|
||||
|
||||
private function handleDeleteCalendar(Calendar $calendar, array $notification, User $user): void
|
||||
{
|
||||
$calendar
|
||||
->setStatus(Calendar::STATUS_CANCELED)
|
||||
->setCalendarRange(null);
|
||||
$calendar->preventEnqueueChanges = true;
|
||||
}
|
||||
|
||||
private function handleUpdateCalendar(Calendar $calendar, array $notification, User $user): void
|
||||
{
|
||||
try {
|
||||
$new = $this->machineHttpClient->request(
|
||||
'GET',
|
||||
$notification['resource']
|
||||
)->toArray();
|
||||
} catch (ClientExceptionInterface $clientException) {
|
||||
$this->logger->warning(__CLASS__ . ' could not retrieve event from ms graph. Already deleted ?', [
|
||||
'calendarId' => $calendar->getId(),
|
||||
'remoteEventId' => $notification['resource'],
|
||||
]);
|
||||
|
||||
throw $clientException;
|
||||
}
|
||||
|
||||
if (false === $new['isOrganizer']) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lastModified = RemoteEventConverter::convertStringDateWithTimezone(
|
||||
$new['lastModifiedDateTime']
|
||||
);
|
||||
|
||||
if ($calendar->getRemoteAttributes()['lastModifiedDateTime'] === $lastModified->getTimestamp()) {
|
||||
$this->logger->info(__CLASS__ . ' change key is equals. Source is probably a local update', [
|
||||
'calendarRangeId' => $calendar->getId(),
|
||||
'remoteEventId' => $notification['resource'],
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->syncAttendees($calendar, $new['attendees']);
|
||||
|
||||
$startDate = RemoteEventConverter::convertStringDateWithoutTimezone($new['start']['dateTime']);
|
||||
$endDate = RemoteEventConverter::convertStringDateWithoutTimezone($new['end']['dateTime']);
|
||||
|
||||
if ($startDate->getTimestamp() !== $calendar->getStartDate()->getTimestamp()) {
|
||||
$calendar->setStartDate($startDate)->setStatus(Calendar::STATUS_MOVED);
|
||||
}
|
||||
|
||||
if ($endDate->getTimestamp() !== $calendar->getEndDate()->getTimestamp()) {
|
||||
$calendar->setEndDate($endDate)->setStatus(Calendar::STATUS_MOVED);
|
||||
}
|
||||
|
||||
$calendar
|
||||
->addRemoteAttributes([
|
||||
'lastModifiedDateTime' => $lastModified->getTimestamp(),
|
||||
'changeKey' => $new['changeKey'],
|
||||
])
|
||||
->preventEnqueueChanges = true;
|
||||
}
|
||||
|
||||
private function syncAttendees(Calendar $calendar, array $attendees): void
|
||||
{
|
||||
$emails = [];
|
||||
|
||||
foreach ($attendees as $attendee) {
|
||||
$status = $attendee['status']['response'];
|
||||
|
||||
if ('organizer' === $status) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$email = $attendee['emailAddress']['address'];
|
||||
$emails[] = strtolower($email);
|
||||
$user = $this->userRepository->findOneByUsernameOrEmail($email);
|
||||
|
||||
if (null === $user) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$calendar->isInvited($user)) {
|
||||
$calendar->addUser($user);
|
||||
}
|
||||
|
||||
$invite = $calendar->getInviteForUser($user);
|
||||
|
||||
switch ($status) {
|
||||
// possible cases: none, organizer, tentativelyAccepted, accepted, declined, notResponded.
|
||||
case 'none':
|
||||
case 'notResponded':
|
||||
$invite->setStatus(Invite::PENDING);
|
||||
|
||||
break;
|
||||
|
||||
case 'tentativelyAccepted':
|
||||
$invite->setStatus(Invite::TENTATIVELY_ACCEPTED);
|
||||
|
||||
break;
|
||||
|
||||
case 'accepted':
|
||||
$invite->setStatus(Invite::ACCEPTED);
|
||||
|
||||
break;
|
||||
|
||||
case 'declined':
|
||||
$invite->setStatus(Invite::DECLINED);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new LogicException('should not happens, not implemented: ' . $status);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($calendar->getUsers() as $user) {
|
||||
if (!in_array(strtolower($user->getEmailCanonical()), $emails, true)) {
|
||||
$calendar->removeUser($user);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ namespace Chill\CalendarBundle\RemoteCalendar\Connector;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||
use Chill\CalendarBundle\Entity\Invite;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineHttpClient;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MapCalendarToUser;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\OnBehalfOfUserHttpClient;
|
||||
@ -209,6 +210,67 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
|
||||
}
|
||||
}
|
||||
|
||||
public function syncInvite(Invite $invite): void
|
||||
{
|
||||
if ('' === $remoteId = $invite->getCalendar()->getRemoteId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === $invite->getUser()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === $userId = $this->mapCalendarToUser->getUserId($invite->getUser())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($invite->hasRemoteId()) {
|
||||
$remoteIdAttendeeCalendar = $invite->getRemoteId();
|
||||
} else {
|
||||
$remoteIdAttendeeCalendar = $this->findRemoteIdOnUserCalendar($invite->getCalendar(), $invite->getUser());
|
||||
$invite->setRemoteId($remoteIdAttendeeCalendar);
|
||||
}
|
||||
|
||||
switch ($invite->getStatus()) {
|
||||
case Invite::PENDING:
|
||||
return;
|
||||
|
||||
case Invite::ACCEPTED:
|
||||
$url = "/v1.0/users/{$userId}/calendar/events/{$remoteIdAttendeeCalendar}/accept";
|
||||
|
||||
break;
|
||||
|
||||
case Invite::TENTATIVELY_ACCEPTED:
|
||||
$url = "/v1.0/users/{$userId}/calendar/events/{$remoteIdAttendeeCalendar}/tentativelyAccept";
|
||||
|
||||
break;
|
||||
|
||||
case Invite::DECLINED:
|
||||
$url = "/v1.0/users/{$userId}/calendar/events/{$remoteIdAttendeeCalendar}/decline";
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception('not supported');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->machineHttpClient->request(
|
||||
'POST',
|
||||
$url,
|
||||
['json' => ['sendResponse' => true]]
|
||||
);
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
$this->logger->warning('could not update calendar range to remote', [
|
||||
'exception' => $e->getTraceAsString(),
|
||||
'content' => $e->getResponse()->getContent(),
|
||||
'calendarRangeId' => 'invite_' . $invite->getId(),
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function cancelOnRemote(string $remoteId, string $comment, User $user, string $identifier): void
|
||||
{
|
||||
$userId = $this->mapCalendarToUser->getUserId($user);
|
||||
@ -273,7 +335,7 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
|
||||
'calendar_identifier' => $identifier,
|
||||
]);
|
||||
|
||||
return [];
|
||||
return ['id' => null, 'lastModifiedDateTime' => null, 'changeKey' => null];
|
||||
}
|
||||
|
||||
try {
|
||||
@ -333,6 +395,44 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* the remoteId is not the same across different user calendars. This method allow to find
|
||||
* the correct remoteId in another calendar.
|
||||
*
|
||||
* For achieving this, the iCalUid is used.
|
||||
*/
|
||||
private function findRemoteIdOnUserCalendar(Calendar $calendar, User $user): ?string
|
||||
{
|
||||
// find the icalUid on original user
|
||||
$event = $this->getOnRemote($calendar->getMainUser(), $calendar->getRemoteId());
|
||||
$userId = $this->mapCalendarToUser->getUserId($user);
|
||||
|
||||
if ('' === $iCalUid = ($event['iCalUId'] ?? '')) {
|
||||
throw new Exception('no iCalUid for this event');
|
||||
}
|
||||
|
||||
try {
|
||||
$events = $this->machineHttpClient->request(
|
||||
'GET',
|
||||
"/v1.0/users/{$userId}/calendar/events",
|
||||
[
|
||||
'query' => [
|
||||
'$select' => 'id',
|
||||
'$filter' => "iCalUId eq '{$iCalUid}'",
|
||||
],
|
||||
]
|
||||
)->toArray();
|
||||
} catch (ClientExceptionInterface $clientException) {
|
||||
throw $clientException;
|
||||
}
|
||||
|
||||
if (1 !== count($events['value'])) {
|
||||
throw new Exception('multiple events found with same iCalUid');
|
||||
}
|
||||
|
||||
return $events['value'][0]['id'];
|
||||
}
|
||||
|
||||
private function getOnRemote(User $user, string $remoteId): array
|
||||
{
|
||||
$userId = $this->mapCalendarToUser->getUserId($user);
|
||||
@ -345,10 +445,13 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->machineHttpClient->request(
|
||||
$v = $this->machineHttpClient->request(
|
||||
'GET',
|
||||
'users/' . $userId . '/calendar/events/' . $remoteId
|
||||
)->toArray();
|
||||
dump($v);
|
||||
|
||||
return $v;
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
$this->logger->warning('Could not get event from calendar', [
|
||||
'remoteId' => $remoteId,
|
||||
@ -409,7 +512,8 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
|
||||
$eventDatas[] = $this->remoteEventConverter->calendarToEvent($calendar);
|
||||
|
||||
if (0 < count($newInvites)) {
|
||||
$eventDatas[] = $this->remoteEventConverter->calendarToEventAttendeesOnly($calendar);
|
||||
// it seems that invitaiton are always send, even if attendee changes are mixed with other datas
|
||||
// $eventDatas[] = $this->remoteEventConverter->calendarToEventAttendeesOnly($calendar);
|
||||
}
|
||||
|
||||
foreach ($eventDatas as $eventData) {
|
||||
@ -446,7 +550,7 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
|
||||
'calendar_identifier' => $identifier,
|
||||
]);
|
||||
|
||||
return [];
|
||||
return ['id' => null, 'lastModifiedDateTime' => null, 'changeKey' => null];
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -13,6 +13,7 @@ namespace Chill\CalendarBundle\RemoteCalendar\Connector;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||
use Chill\CalendarBundle\Entity\Invite;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use DateTimeImmutable;
|
||||
use LogicException;
|
||||
@ -46,4 +47,8 @@ class NullRemoteCalendarConnector implements RemoteCalendarConnectorInterface
|
||||
public function syncCalendarRange(CalendarRange $calendarRange): void
|
||||
{
|
||||
}
|
||||
|
||||
public function syncInvite(Invite $invite): void
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ namespace Chill\CalendarBundle\RemoteCalendar\Connector;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||
use Chill\CalendarBundle\Entity\Invite;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Model\RemoteEvent;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use DateTimeImmutable;
|
||||
@ -46,4 +47,6 @@ interface RemoteCalendarConnectorInterface
|
||||
public function syncCalendar(Calendar $calendar, string $action, ?CalendarRange $previousCalendarRange, ?User $previousMainUser, ?array $oldInvites, ?array $newInvites): void;
|
||||
|
||||
public function syncCalendarRange(CalendarRange $calendarRange): void;
|
||||
|
||||
public function syncInvite(Invite $invite): void;
|
||||
}
|
||||
|
@ -12,14 +12,17 @@ declare(strict_types=1);
|
||||
namespace Chill\CalendarBundle\RemoteCalendar\DependencyInjection;
|
||||
|
||||
use Chill\CalendarBundle\Command\AzureGrantAdminConsentAndAcquireToken;
|
||||
use Chill\CalendarBundle\Command\MapUserCalendarCommand;
|
||||
use Chill\CalendarBundle\Command\MapAndSubscribeUserCalendarCommand;
|
||||
use Chill\CalendarBundle\Controller\RemoteCalendarConnectAzureController;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineHttpClient;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineTokenStorage;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraphRemoteCalendarConnector;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\NullRemoteCalendarConnector;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use TheNetworg\OAuth2\Client\Provider\Azure;
|
||||
|
||||
class RemoteCalendarCompilerPass implements CompilerPassInterface
|
||||
@ -35,11 +38,16 @@ class RemoteCalendarCompilerPass implements CompilerPassInterface
|
||||
|
||||
if ($config['remote_calendars_sync']['microsoft_graph']['enabled']) {
|
||||
$connector = MSGraphRemoteCalendarConnector::class;
|
||||
|
||||
$container->setAlias(HttpClientInterface::class . ' $machineHttpClient', MachineHttpClient::class);
|
||||
} else {
|
||||
// remove services which cannot be loaded
|
||||
$container->removeDefinition(MapUserCalendarCommand::class);
|
||||
$container->removeDefinition(MapAndSubscribeUserCalendarCommand::class);
|
||||
$container->removeDefinition(AzureGrantAdminConsentAndAcquireToken::class);
|
||||
$container->removeDefinition(RemoteCalendarConnectAzureController::class);
|
||||
$container->removeDefinition(MachineTokenStorage::class);
|
||||
$container->removeDefinition(MachineHttpClient::class);
|
||||
$container->removeDefinition(MSGraphRemoteCalendarConnector::class);
|
||||
}
|
||||
|
||||
if (!$container->hasAlias(Azure::class) && $container->hasDefinition('knpu.oauth2.client.azure')) {
|
||||
@ -58,7 +66,9 @@ class RemoteCalendarCompilerPass implements CompilerPassInterface
|
||||
->setDecoratedService(RemoteCalendarConnectorInterface::class);
|
||||
} else {
|
||||
// keep the container lighter by removing definitions
|
||||
$container->removeDefinition($serviceId);
|
||||
if ($container->hasDefinition($serviceId)) {
|
||||
$container->removeDefinition($serviceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,80 @@
|
||||
<?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\CalendarBundle\Repository;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
class CalendarACLAwareRepository implements CalendarACLAwareRepositoryInterface
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
public function buildQueryByAccompanyingPeriod(AccompanyingPeriod $period, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): QueryBuilder
|
||||
{
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
$qb->from(Calendar::class, 'c');
|
||||
|
||||
$andX = $qb->expr()->andX($qb->expr()->eq('c.accompanyingPeriod', ':period'));
|
||||
$qb->setParameter('period', $period);
|
||||
|
||||
if (null !== $startDate) {
|
||||
$andX->add($qb->expr()->gte('c.startDate', ':startDate'));
|
||||
$qb->setParameter('startDate', $startDate);
|
||||
}
|
||||
|
||||
if (null !== $endDate) {
|
||||
$andX->add($qb->expr()->lte('c.endDate', ':endDate'));
|
||||
$qb->setParameter('endDate', $endDate);
|
||||
}
|
||||
|
||||
$qb->where($andX);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function countByAccompanyingPeriod(AccompanyingPeriod $period, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): int
|
||||
{
|
||||
$qb = $this->buildQueryByAccompanyingPeriod($period, $startDate, $endDate)->select('count(c)');
|
||||
|
||||
return $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|Calendar[]
|
||||
*/
|
||||
public function findByAccompanyingPeriod(AccompanyingPeriod $period, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate, ?array $orderBy = [], ?int $offset = null, ?int $limit = null): array
|
||||
{
|
||||
$qb = $this->buildQueryByAccompanyingPeriod($period, $startDate, $endDate)->select('c');
|
||||
|
||||
foreach ($orderBy as $sort => $order) {
|
||||
$qb->addOrderBy('c.' . $sort, $order);
|
||||
}
|
||||
|
||||
if (null !== $offset) {
|
||||
$qb->setFirstResult($offset);
|
||||
}
|
||||
|
||||
if (null !== $limit) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
<?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\CalendarBundle\Repository;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use DateTimeImmutable;
|
||||
|
||||
interface CalendarACLAwareRepositoryInterface
|
||||
{
|
||||
public function countByAccompanyingPeriod(AccompanyingPeriod $period, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): int;
|
||||
|
||||
/**
|
||||
* @return array|Calendar[]
|
||||
*/
|
||||
public function findByAccompanyingPeriod(AccompanyingPeriod $period, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate, ?array $orderBy = [], ?int $offset = null, ?int $limit = null): array;
|
||||
}
|
@ -13,8 +13,10 @@ namespace Chill\CalendarBundle\Repository;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
class CalendarRepository implements ObjectRepository
|
||||
@ -67,6 +69,21 @@ class CalendarRepository implements ObjectRepository
|
||||
);
|
||||
}
|
||||
|
||||
public function findByNotificationAvailable(DateTimeImmutable $startDate, DateTimeImmutable $endDate, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
$qb = $this->queryByNotificationAvailable($startDate, $endDate)->select('c');
|
||||
|
||||
if (null !== $limit) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
|
||||
if (null !== $offset) {
|
||||
$qb->setFirstResult($offset);
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria): ?Calendar
|
||||
{
|
||||
return $this->repository->findOneBy($criteria);
|
||||
@ -76,4 +93,31 @@ class CalendarRepository implements ObjectRepository
|
||||
{
|
||||
return Calendar::class;
|
||||
}
|
||||
|
||||
private function queryByNotificationAvailable(DateTimeImmutable $startDate, DateTimeImmutable $endDate): QueryBuilder
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('c');
|
||||
|
||||
$qb->where(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq('c.sendSMS', ':true'),
|
||||
$qb->expr()->gte('c.startDate', ':startDate'),
|
||||
$qb->expr()->lt('c.startDate', ':endDate'),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->eq('c.smsStatus', ':pending'),
|
||||
$qb->expr()->eq('c.smsStatus', ':cancel_pending')
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
$qb->setParameters([
|
||||
'true' => true,
|
||||
'startDate' => $startDate,
|
||||
'endDate' => $endDate,
|
||||
'pending' => Calendar::SMS_PENDING,
|
||||
'cancel_pending' => Calendar::SMS_CANCEL_PENDING,
|
||||
]);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,8 @@ services:
|
||||
|
||||
Chill\CalendarBundle\Repository\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../../Repository/'
|
||||
tags:
|
||||
- { name: 'doctrine.repository_service' }
|
||||
|
||||
Chill\CalendarBundle\Menu\:
|
||||
autowire: true
|
||||
@ -29,8 +28,16 @@ services:
|
||||
arguments:
|
||||
$azure: '@knpu.oauth2.provider.azure'
|
||||
tags: ['console.command']
|
||||
|
||||
|
||||
Chill\CalendarBundle\Security\:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
resource: '../../Security/'
|
||||
|
||||
Chill\CalendarBundle\Service\:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
resource: '../../Service/'
|
||||
|
||||
Chill\CalendarBundle\Service\ShortMessageForCalendarBuilderInterface:
|
||||
alias: Chill\CalendarBundle\Service\DefaultShortMessageForCalendarBuider
|
||||
|
@ -14,7 +14,18 @@ document.addEventListener('DOMContentLoaded', function (e) {
|
||||
components: {
|
||||
Answer,
|
||||
},
|
||||
template: '<answer :calendarId="14" :status="defined"></answer>',
|
||||
data() {
|
||||
return {
|
||||
status: el.dataset.status,
|
||||
calendarId: Number.parseInt(el.dataset.calendarId),
|
||||
}
|
||||
},
|
||||
template: '<answer :calendarId="calendarId" :status="status" @statusChanged="onStatusChanged"></answer>',
|
||||
methods: {
|
||||
onStatusChanged: function(newStatus) {
|
||||
this.$data.status = newStatus;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
app.use(i18n).mount(el);
|
||||
|
@ -1,37 +1,72 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<label>Utilisateur principal</label>
|
||||
</div>
|
||||
<div class="col-md-8 align-content-end">
|
||||
<pick-entity
|
||||
:multiple="false"
|
||||
:types="['user']"
|
||||
:uniqid="'main_user_calendar'"
|
||||
:picked="null !== this.$store.getters.getMainUser ? [this.$store.getters.getMainUser] : []"
|
||||
:removableIfSet="false"
|
||||
@addNewEntity="setMainUser"
|
||||
></pick-entity>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<select v-model="this.slotDuration">
|
||||
<option value="00:05:00">5</option>
|
||||
<option value="00:10:00">10</option>
|
||||
<option value="00:15:00">15</option>
|
||||
<option value="00:30:00">30</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" v-model="this.hideWeekEnds" /><label>Masquer les week-ends</label>
|
||||
</div>
|
||||
<concerned-groups></concerned-groups>
|
||||
|
||||
<teleport to="#mainUser">
|
||||
|
||||
<h2 class="chill-red">Utilisateur principal</h2>
|
||||
<div>
|
||||
<template v-for="u in getActiveUsers" :key="u.id">
|
||||
<calendar-active :user="u" ></calendar-active>
|
||||
</template>
|
||||
<div>
|
||||
<div v-if="null !== this.$store.getters.getMainUser">
|
||||
<calendar-active :user="this.$store.getters.getMainUser" ></calendar-active>
|
||||
</div>
|
||||
<pick-entity
|
||||
:multiple="false"
|
||||
:types="['user']"
|
||||
:uniqid="'main_user_calendar'"
|
||||
:picked="null !== this.$store.getters.getMainUser ? [this.$store.getters.getMainUser] : []"
|
||||
:removableIfSet="false"
|
||||
:displayPicked="false"
|
||||
@addNewEntity="setMainUser"
|
||||
></pick-entity>
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
|
||||
<concerned-groups></concerned-groups>
|
||||
|
||||
<teleport to="#schedule">
|
||||
<div class="row mb-3" v-if="activity.startDate !== null">
|
||||
<label class="col-form-label col-sm-4">Date</label>
|
||||
<div class="col-sm-8">
|
||||
{{ $d(activity.startDate, 'long') }} - {{ $d(activity.endDate, 'hoursOnly') }}
|
||||
<span v-if="activity.calendarRange === null">(Pas de plage de disponibilité sélectionnée)</span>
|
||||
<span v-else>(Une plage de disponibilité sélectionnée)</span>
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
|
||||
<location></location>
|
||||
|
||||
|
||||
|
||||
<teleport to="#fullCalendar">
|
||||
<div class="calendar-actives">
|
||||
<template class="" v-for="u in getActiveUsers" :key="u.id">
|
||||
<calendar-active :user="u" :invite="this.$store.getters.getInviteForUser(u)"></calendar-active>
|
||||
</template>
|
||||
</div>
|
||||
<div class="display-options row justify-content-between">
|
||||
<div class="col-sm col-xs-12">
|
||||
<div class="input-group mb-3">
|
||||
<label class="input-group-text" for="slotDuration">Durée des créneaux</label>
|
||||
<select v-model="this.slotDuration" id="slotDuration" class="form-select">
|
||||
<option value="00:05:00">5 minutes</option>
|
||||
<option value="00:10:00">10 minutes</option>
|
||||
<option value="00:15:00">15 minutes</option>
|
||||
<option value="00:30:00">30 minutes</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm col-xs-12">
|
||||
<div class="float-end">
|
||||
<div class="input-group mb-3">
|
||||
<div class="input-group-text">
|
||||
<input id="showHideWE" class="form-check-input mt-0" type="checkbox" v-model="this.hideWeekEnds">
|
||||
<label for="showHideWE" class="form-check-label"> Masquer les week-ends</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FullCalendar ref="fullCalendar" :options="calendarOptions">
|
||||
<template v-slot:eventContent='arg'>
|
||||
<b>{{ arg.timeText }}</b>
|
||||
@ -39,7 +74,6 @@
|
||||
</template>
|
||||
</FullCalendar>
|
||||
</teleport>
|
||||
<location></location>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -55,7 +89,7 @@ import interactionPlugin from '@fullcalendar/interaction';
|
||||
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||
import CalendarActive from './Components/CalendarActive';
|
||||
import PickEntity from 'ChillMainAssets/vuejs/PickEntity/PickEntity.vue';
|
||||
import {mapGetters} from "vuex";
|
||||
import {mapGetters, mapState} from "vuex";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
@ -76,6 +110,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['getMainUser']),
|
||||
...mapState(['activity']),
|
||||
events() {
|
||||
return this.$store.getters.getEventSources;
|
||||
},
|
||||
@ -84,7 +119,7 @@ export default {
|
||||
locale: frLocale,
|
||||
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin, dayGridPlugin],
|
||||
initialView: 'timeGridWeek',
|
||||
initialDate: this.$store.getters.initialDate,
|
||||
initialDate: this.$store.getters.getInitialDate,
|
||||
eventSources: this.events,
|
||||
selectable: true,
|
||||
datesSet: this.onDatesSet,
|
||||
@ -103,7 +138,7 @@ export default {
|
||||
timeGrid: {
|
||||
slotEventOverlap: false,
|
||||
slotDuration: this.slotDuration,
|
||||
scrollTime: '10:00:00',
|
||||
//scrollTime: '10:00:00',
|
||||
},
|
||||
dayGridThreeDays: {
|
||||
type: 'dayGridWeek',
|
||||
@ -153,8 +188,9 @@ export default {
|
||||
console.log('onDateSelect', payload);
|
||||
|
||||
// show an alert if changing mainUser
|
||||
if (this.$store.getters.getMainUser !== null
|
||||
&& this.$store.state.me.id !== this.$store.getters.getMainUser.id) {
|
||||
if ((this.$store.getters.getMainUser !== null
|
||||
&& this.$store.state.me.id !== this.$store.getters.getMainUser.id)
|
||||
|| this.$store.getters.getMainUser === null) {
|
||||
if (!window.confirm(this.$t('will_change_main_user_for_me'))) {
|
||||
return;
|
||||
} else {
|
||||
@ -191,3 +227,14 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.calendar-actives {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.display-options {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,15 +1,24 @@
|
||||
<template>
|
||||
<span class="badge" :style="style">
|
||||
{{ user.text }}
|
||||
<span class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="flexSwitchCheckDefault" v-model="rangeShow">
|
||||
<label class="form-check-label" for="flexSwitchCheckDefault">Disponibilité</label>
|
||||
</span>
|
||||
<span class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="flexSwitchCheckDefault" v-model="remoteShow">
|
||||
<label class="form-check-label" for="flexSwitchCheckDefault">Agenda</label>
|
||||
</span>
|
||||
</span>
|
||||
<div :style="style" class="calendar-active">
|
||||
<span class="badge-user">
|
||||
{{ user.text }}
|
||||
<template v-if="invite !== null">
|
||||
<i v-if="invite.status === 'accepted'" class="fa fa-check"></i>
|
||||
<i v-else-if="invite.status === 'declined'" class="fa fa-times"></i>
|
||||
<i v-else-if="invite.status === 'pending'" class="fa fa-question-o"></i>
|
||||
<i v-else-if="invite.status === 'tentative'" class="fa fa-question"></i>
|
||||
<span v-else="">{{ invite.status }}</span>
|
||||
</template>
|
||||
</span>
|
||||
<span class="form-check-inline form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="flexSwitchCheckDefault" v-model="rangeShow">
|
||||
<label class="form-check-label" for="flexSwitchCheckDefault" title="Disponibilités"><i class="fa fa-calendar-check-o"></i></label>
|
||||
</span>
|
||||
<span class="form-check-inline form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="flexSwitchCheckDefault" v-model="remoteShow">
|
||||
<label class="form-check-label" for="flexSwitchCheckDefault" title="Agenda"><i class="fa fa-calendar"></i></label>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -17,12 +26,21 @@ import {mapGetters} from 'vuex';
|
||||
|
||||
export default {
|
||||
name: "CalendarActive",
|
||||
props: ['user'],
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
invite: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
return {
|
||||
backgroundColor: this.$store.getters.getUserData(this.user).mainColor,
|
||||
color: 'black',
|
||||
};
|
||||
},
|
||||
rangeShow: {
|
||||
@ -45,6 +63,18 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="scss">
|
||||
|
||||
.calendar-active {
|
||||
margin: 0 0.25rem 0.25rem 0;
|
||||
padding: 0.5rem;
|
||||
|
||||
border-radius: 0.5rem;
|
||||
|
||||
color: var(--bs-blue);
|
||||
|
||||
& > .badge-user {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -81,6 +81,7 @@ export default {
|
||||
},
|
||||
addPersonsInvolved({commit, dispatch}, payload) {
|
||||
console.log('### action addPersonsInvolved', payload.result.type);
|
||||
console.log('### action addPersonsInvolved payload result', payload.result);
|
||||
switch (payload.result.type) {
|
||||
case 'person':
|
||||
let aPersons = document.getElementById("chill_activitybundle_activity_persons");
|
||||
|
@ -62,8 +62,6 @@ export default {
|
||||
const userData = state.usersData.get(userId);
|
||||
|
||||
if (kinds.ranges && userData.calendarRanges.length > 0) {
|
||||
console.log('first range', userData.calendarRanges[0]);
|
||||
console.log('state activity', state.activity);
|
||||
const s = {
|
||||
id: `ranges_${userId}`,
|
||||
events: userData.calendarRanges.filter(r => state.activity.calendarRange === null || r.calendarRangeId !== state.activity.calendarRange.calendarRangeId),
|
||||
@ -91,6 +89,12 @@ export default {
|
||||
|
||||
return sources;
|
||||
},
|
||||
getInitialDate(state) {
|
||||
return state.activity.startDate;
|
||||
},
|
||||
getInviteForUser: (state) => (user) => {
|
||||
return state.activity.invites.find(i => i.user.id === user.id);
|
||||
},
|
||||
/**
|
||||
* get the user data for a specific user
|
||||
*
|
||||
|
@ -47,8 +47,6 @@ const store = createStore({
|
||||
actions,
|
||||
});
|
||||
|
||||
console.log('calendar event', store.state.activity);
|
||||
|
||||
whoami().then(me => {
|
||||
store.commit('setWhoAmiI', me);
|
||||
});
|
||||
@ -57,4 +55,10 @@ if (null !== store.getters.getMainUser) {
|
||||
store.commit('showUserOnCalendar', {ranges: true, remotes: true, user: store.getters.getMainUser});
|
||||
}
|
||||
|
||||
for (let u of store.state.activity.users) {
|
||||
store.commit('showUserOnCalendar', {ranges: false, remotes: false, user: u});
|
||||
}
|
||||
|
||||
console.log('store', store);
|
||||
|
||||
export default store;
|
||||
|
@ -19,9 +19,8 @@ const removeIdFromValue = (string, id) => {
|
||||
* Assign missing keys for the ConcernedGroups component
|
||||
*/
|
||||
const mapEntity = (entity) => {
|
||||
console.log('mapEntity', entity);
|
||||
let calendar = { ...entity};
|
||||
Object.assign(calendar, {thirdParties: entity.professionals, users: entity.invites});
|
||||
Object.assign(calendar, {thirdParties: entity.professionals});
|
||||
|
||||
if (entity.startDate !== null ) {
|
||||
calendar.startDate = ISOToDatetime(entity.startDate.datetime);
|
||||
@ -35,8 +34,6 @@ const mapEntity = (entity) => {
|
||||
calendar.calendarRange.id = `range_${entity.calendarRange.id}`;
|
||||
}
|
||||
|
||||
console.log('new calendar object ', calendar);
|
||||
|
||||
return calendar;
|
||||
};
|
||||
|
||||
@ -56,7 +53,7 @@ const createUserData = (user, colorIndex) => {
|
||||
const calendarRangeToFullCalendarEvent = (entity) => {
|
||||
return {
|
||||
id: `range_${entity.id}`,
|
||||
title: "",
|
||||
title: "(" + entity.user.text + ")",
|
||||
start: entity.startDate.datetime8601,
|
||||
end: entity.endDate.datetime8601,
|
||||
allDay: false,
|
||||
|
@ -1,31 +1,89 @@
|
||||
<template>
|
||||
<div class="btn-group" role="group">
|
||||
<button id="btnGroupDrop1" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{{ $t('answer')}}
|
||||
<button id="btnGroupDrop1" type="button" class="btn btn-misc dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<template v-if="status === Statuses.PENDING">
|
||||
<span class="fa fa-hourglass"></span> {{ $t('Give_an_answer')}}
|
||||
</template>
|
||||
<template v-else-if="status === Statuses.ACCEPTED">
|
||||
<span class="fa fa-check"></span> {{ $t('Accepted')}}
|
||||
</template>
|
||||
<template v-else-if="status === Statuses.DECLINED">
|
||||
<span class="fa fa-times"></span> {{ $t('Declined')}}
|
||||
</template>
|
||||
<template v-else-if="status === Statuses.TENTATIVELY_ACCEPTED">
|
||||
<span class="fa fa-question"></span> {{ $t('Tentative')}}
|
||||
</template>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
|
||||
<li><a class="dropdown-item" href="#"><i class="fa fa-check" aria-hidden="true"></i> {{ $t('accept') }}</a></li>
|
||||
<li><a class="dropdown-item" href="#"><i class="fa fa-times" aria-hidden="true"></i> {{ $t('decline') }}</a></li>
|
||||
<li><a class="dropdown-item" href="#"><i class="fa fa-question"></i> {{ $t('tentatively_accept') }}</a></li>
|
||||
<li><a class="dropdown-item" href="#"><i class="fa fa-hourglass-o"></i> {{ $t('pending') }}</a></li>
|
||||
<li v-if="status !== Statuses.ACCEPTED"><a class="dropdown-item" @click="changeStatus(Statuses.ACCEPTED)"><i class="fa fa-check" aria-hidden="true"></i> {{ $t('Accept') }}</a></li>
|
||||
<li v-if="status !== Statuses.DECLINED"><a class="dropdown-item" @click="changeStatus(Statuses.DECLINED)"><i class="fa fa-times" aria-hidden="true"></i> {{ $t('Decline') }}</a></li>
|
||||
<li v-if="status !== Statuses.TENTATIVELY_ACCEPTED"><a class="dropdown-item" @click="changeStatus(Statuses.TENTATIVELY_ACCEPTED)"><i class="fa fa-question"></i> {{ $t('Tentatively_accept') }}</a></li>
|
||||
<li v-if="status !== Statuses.PENDING"><a class="dropdown-item" @click="changeStatus(Statuses.PENDING)"><i class="fa fa-hourglass-o"></i> {{ $t('Set_pending') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
import {defineComponent, PropType} from 'vue';
|
||||
|
||||
interface Props {
|
||||
calendarId: number,
|
||||
status: string
|
||||
}
|
||||
const ACCEPTED = 'accepted';
|
||||
const DECLINED = 'declined';
|
||||
const PENDING = 'pending';
|
||||
const TENTATIVELY_ACCEPTED = 'tentative';
|
||||
|
||||
const i18n = {
|
||||
messages: {
|
||||
fr: {
|
||||
"Give_an_answer": "Répondre",
|
||||
"Accepted": "Accepté",
|
||||
"Declined": "Refusé",
|
||||
"Tentative": "Accepté provisoirement",
|
||||
"Accept": "Accepter",
|
||||
"Decline": "Refuser",
|
||||
"Tentatively_accept": "Accepter provisoirement",
|
||||
"Set_pending": "Ne pas répondre",
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "Answer",
|
||||
i18n,
|
||||
props: {
|
||||
calendarId: { type: Number, required: true},
|
||||
status: {type: String, required: true},
|
||||
status: {type: String as PropType<"accepted" | "declined" | "pending" | "tentative">, required: true},
|
||||
},
|
||||
emits: {
|
||||
statusChanged(payload: "accepted" | "declined" | "pending" | "tentative") {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
Statuses: {
|
||||
ACCEPTED,
|
||||
DECLINED,
|
||||
PENDING,
|
||||
TENTATIVELY_ACCEPTED,
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeStatus: function (newStatus: "accepted" | "declined" | "pending" | "tentative") {
|
||||
console.log('changeStatus', newStatus);
|
||||
const url = `/api/1.0/calendar/calendar/${this.$props.calendarId}/answer/${newStatus}.json`;
|
||||
window.fetch(url, {
|
||||
method: 'POST',
|
||||
}).then((r: Response) => {
|
||||
if (!r.ok) {
|
||||
console.error('could not confirm answer', newStatus);
|
||||
return;
|
||||
}
|
||||
console.log('answer sent', newStatus);
|
||||
this.$emit('statusChanged', newStatus);
|
||||
});
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@ -34,3 +92,4 @@ export default defineComponent({
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
||||
|
@ -9,8 +9,8 @@
|
||||
{
|
||||
'title' : 'Remove calendar item'|trans,
|
||||
'confirm_question' : 'Are you sure you want to remove the calendar item?'|trans,
|
||||
'cancel_route' : 'chill_calendar_calendar_list',
|
||||
'cancel_parameters' : { 'accompanying_period_id' : accompanyingCourse.id, 'id' : calendar.id },
|
||||
'cancel_route' : 'chill_calendar_calendar_list_by_period',
|
||||
'cancel_parameters' : { 'id' : accompanyingCourse.id },
|
||||
'form' : delete_form
|
||||
} ) }}
|
||||
{% endblock %}
|
||||
|
@ -3,6 +3,9 @@
|
||||
{{ form_start(form) }}
|
||||
{{ form_errors(form) }}
|
||||
|
||||
<div id="calendar"></div> {# <=== vue component #}
|
||||
|
||||
<div id="mainUser"></div> {# <=== vue component: mainUser #}
|
||||
|
||||
<h2 class="chill-red">{{ 'Concerned groups'|trans }}</h2>
|
||||
|
||||
@ -23,6 +26,13 @@
|
||||
|
||||
<h2 class="chill-red">{{ 'Calendar data'|trans }}</h2>
|
||||
|
||||
<div id="schedule"></div>
|
||||
|
||||
{%- if form.location is defined -%}
|
||||
{{ form_row(form.location) }}
|
||||
<div id="location"></div>
|
||||
{% endif %}
|
||||
|
||||
{%- if form.startDate is defined -%}
|
||||
{{ form_row(form.startDate) }}
|
||||
{% endif %}
|
||||
@ -35,10 +45,7 @@
|
||||
{{ form_row(form.calendarRange) }}
|
||||
{% endif %}
|
||||
|
||||
{%- if form.location is defined -%}
|
||||
{{ form_row(form.location) }}
|
||||
<div id="location"></div>
|
||||
{% endif %}
|
||||
<div id="fullCalendar"></div>
|
||||
|
||||
{%- if form.comment is defined -%}
|
||||
{{ form_row(form.comment) }}
|
||||
@ -56,7 +63,6 @@
|
||||
<div id="calendarControls"></div>
|
||||
{% endif %}
|
||||
|
||||
<div id="fullCalendar"></div>
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
@ -65,7 +71,7 @@
|
||||
{%- if context == 'user' -%}
|
||||
href="{{ chill_return_path_or('chill_calendar_calendar_list', { 'user_id': user.id } )}}"
|
||||
{%- elseif context == 'accompanyingCourse' -%}
|
||||
href="{{ chill_return_path_or('chill_calendar_calendar_list', { 'accompanying_period_id': accompanyingCourse.id } )}}"
|
||||
href="{{ chill_return_path_or('chill_calendar_calendar_list_by_period', { 'id': accompanyingCourse.id } )}}"
|
||||
{%- endif -%}
|
||||
>
|
||||
{{ 'Cancel'|trans|chill_return_path_label }}
|
||||
@ -73,7 +79,7 @@
|
||||
</li>
|
||||
<li>
|
||||
<button class="btn btn-create" type="submit">
|
||||
{{ 'Update'|trans }}
|
||||
{{ 'Save'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -7,7 +7,6 @@
|
||||
{% block content %}
|
||||
<div class="calendar-edit">
|
||||
|
||||
<div id="calendar"></div> {# <=== vue component #}
|
||||
{% include 'ChillCalendarBundle:Calendar:edit.html.twig' with {'context': 'accompanyingCourse'} %}
|
||||
|
||||
</div>
|
||||
@ -16,10 +15,6 @@
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
<script type="text/javascript">
|
||||
window.addEventListener('DOMContentLoaded', function (e) {
|
||||
chill.displayAlertWhenLeavingModifiedForm('form[name="{{ form.vars.form.vars.name }}"]',
|
||||
'{{ "You are going to leave a page with unsubmitted data. Are you sure you want to leave ?"|trans }}');
|
||||
});
|
||||
window.entity = {{ entity_json|json_encode|raw }};
|
||||
window.startDate = {{ entity.startDate|date('Y-m-d H:i:s')|json_encode|raw }};
|
||||
window.endDate = {{ entity.endDate|date('Y-m-d H:i:s')|json_encode|raw }};
|
||||
|
@ -8,18 +8,21 @@
|
||||
{% set accompanying_course_id = accompanyingCourse.id %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_answer') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_answer') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>{{ 'Calendar list' |trans }}</h1>
|
||||
{{ filterOrder|chill_render_filter_order_helper }}
|
||||
|
||||
{% if calendarItems|length == 0 %}
|
||||
{% if calendarItems|length == 0 %}
|
||||
<p class="chill-no-data-statement">
|
||||
{{ "There is no calendar items."|trans }}
|
||||
<a href="{{ path('chill_calendar_calendar_new', {'user_id': user_id, 'accompanying_period_id': accompanying_course_id}) }}" class="btn btn-create button-small"></a>
|
||||
@ -33,63 +36,35 @@
|
||||
<div class="item-bloc">
|
||||
<div class="item-row main">
|
||||
<div class="item-col">
|
||||
<div class="wrap-header">
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
{% if calendar.endDate.diff(calendar.startDate).days >= 1 %}
|
||||
<p class="date-label">{{ calendar.startDate|format_datetime('short', 'short') }} - {{ calendar.endDate|format_datetime('short', 'short') }}</p>
|
||||
{% else %}
|
||||
<p class="date-label">{{ calendar.startDate|format_datetime('short', 'short') }} - {{ calendar.endDate|format_datetime('none', 'short') }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if calendar.startDate and calendar.endDate %}
|
||||
{% if calendar.endDate.diff(calendar.startDate).days >= 1 %}
|
||||
<h3>{{ "From the day"|trans }} {{ calendar.startDate|format_datetime('medium', 'short') }} </h3>
|
||||
<h3>{{ "to the day"|trans }} {{ calendar.endDate|format_datetime('medium', 'short') }}</h3>
|
||||
{% else %}
|
||||
<h3>{{ calendar.startDate|format_date('full') }} </h3>
|
||||
<h3>{{ calendar.startDate|format_datetime('none', 'short', locale='fr') }} - {{ calendar.endDate|format_datetime('none', 'short', locale='fr') }}</h3>
|
||||
<div class="duration">
|
||||
<p>
|
||||
<i class="fa fa-fw fa-hourglass-end"></i>
|
||||
{{ calendar.duration|date('%H:%I')}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="duration">
|
||||
<p>
|
||||
<i class="fa fa-fw fa-hourglass-end"></i>
|
||||
{{ calendar.duration|date('%H:%I')}}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
<div class="item-col">
|
||||
<ul class="list-content">
|
||||
{% if calendar.mainUser is not empty %}
|
||||
<span class="badge-user">{{ calendar.mainUser|chill_entity_render_box }}</span>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="item-col">
|
||||
<ul class="list-content">
|
||||
{% if calendar.mainUser is not empty %}
|
||||
<li>
|
||||
<b>{{ 'main user concerned'|trans }}: {{ calendar.mainUser.usernameCanonical }}</b>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
<ul class="record_actions">
|
||||
{% if (calendar.isInvited(app.user)) %}
|
||||
{% set invite = calendar.inviteForUser(app.user) %}
|
||||
<li>
|
||||
<div invite-answer data-status="{{ invite.status|e('html_attr') }}" data-calendar-id="{{ calendar.id|e('html_attr') }}"></div>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="{{ path('chill_calendar_calendar_show', { 'id': calendar.id, 'user_id': user_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-show "></a>
|
||||
</li>
|
||||
{# TOOD
|
||||
{% if is_granted('CHILL_ACTIVITY_UPDATE', calendar) %}
|
||||
#}
|
||||
<li>
|
||||
<a href="{{ path('chill_calendar_calendar_edit', { 'id': calendar.id, 'user_id': user_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-update "></a>
|
||||
</li>
|
||||
{# TOOD
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACTIVITY_DELETE', calendar) %}
|
||||
#}
|
||||
<li>
|
||||
<a href="{{ path('chill_calendar_calendar_delete', { 'id': calendar.id, 'user_id' : user_id, 'accompanying_period_id': accompanying_course_id } ) }}" class="btn btn-delete "></a>
|
||||
</li>
|
||||
{#
|
||||
{% endif %}
|
||||
#}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{%
|
||||
@ -98,11 +73,11 @@
|
||||
or calendar.thirdParties|length > 0
|
||||
or calendar.users|length > 0
|
||||
%}
|
||||
<div class="item-row details">
|
||||
<div class="item-row details separator">
|
||||
<div class="item-col">
|
||||
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {
|
||||
'context': 'calendar_accompanyingCourse',
|
||||
'render': 'row',
|
||||
'render': 'wrap-list',
|
||||
'entity': calendar
|
||||
} %}
|
||||
</div>
|
||||
@ -115,6 +90,37 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="item-row separator">
|
||||
<ul class="record_actions">
|
||||
{% if (calendar.isInvited(app.user)) %}
|
||||
{% set invite = calendar.inviteForUser(app.user) %}
|
||||
<li>
|
||||
<div invite-answer data-status="{{ invite.status|e('html_attr') }}" data-calendar-id="{{ calendar.id|e('html_attr') }}"></div>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="{{ path('chill_calendar_calendar_show', { 'id': calendar.id, 'user_id': user_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-show "></a>
|
||||
</li>
|
||||
{# TOOD
|
||||
{% if is_granted('CHILL_ACTIVITY_UPDATE', calendar) %}
|
||||
#}
|
||||
<li>
|
||||
<a href="{{ path('chill_calendar_calendar_edit', { 'id': calendar.id, 'user_id': user_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-update "></a>
|
||||
</li>
|
||||
{# TOOD
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACTIVITY_DELETE', calendar) %}
|
||||
#}
|
||||
<li>
|
||||
<a href="{{ path('chill_calendar_calendar_delete', { 'id': calendar.id, 'user_id' : user_id, 'accompanying_period_id': accompanying_course_id } ) }}" class="btn btn-delete "></a>
|
||||
</li>
|
||||
{#
|
||||
{% endif %}
|
||||
#}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
|
@ -3,6 +3,9 @@
|
||||
{{ form_start(form) }}
|
||||
{{ form_errors(form) }}
|
||||
|
||||
<div id="calendar"></div> {# <=== vue component #}
|
||||
|
||||
<div id="mainUser"></div> {# <=== vue component: mainUser #}
|
||||
|
||||
<h2 class="chill-red">{{ 'Concerned groups'|trans }}</h2>
|
||||
|
||||
@ -27,6 +30,13 @@
|
||||
|
||||
<h2 class="chill-red">{{ 'Calendar data'|trans }}</h2>
|
||||
|
||||
<div id="schedule"></div>
|
||||
|
||||
{%- if form.location is defined -%}
|
||||
{{ form_row(form.location) }}
|
||||
<div id="location"></div>
|
||||
{% endif %}
|
||||
|
||||
{%- if form.startDate is defined -%}
|
||||
{{ form_row(form.startDate) }}
|
||||
{% endif %}
|
||||
@ -35,11 +45,12 @@
|
||||
{{ form_row(form.endDate) }}
|
||||
{% endif %}
|
||||
|
||||
{%- if form.location is defined -%}
|
||||
{{ form_row(form.location) }}
|
||||
<div id="location"></div>
|
||||
{%- if form.calendarRange is defined -%}
|
||||
{{ form_row(form.calendarRange) }}
|
||||
{% endif %}
|
||||
|
||||
<div id="fullCalendar"></div>
|
||||
|
||||
{%- if form.comment is defined -%}
|
||||
{{ form_row(form.comment) }}
|
||||
{% endif %}
|
||||
@ -52,7 +63,6 @@
|
||||
{{ form_row(form.sendSMS) }}
|
||||
{% endif %}
|
||||
|
||||
<div id="fullCalendar"></div>
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
@ -61,7 +71,7 @@
|
||||
{%- if context == 'person' -%}
|
||||
href="{{ chill_return_path_or('chill_calendar_calendar_list', { 'person_id': person.id } )}}"
|
||||
{%- else -%}
|
||||
href="{{ chill_return_path_or('chill_calendar_calendar_list', { 'accompanying_period_id': accompanyingCourse.id } )}}"
|
||||
href="{{ chill_return_path_or('chill_calendar_calendar_list_by_period', { 'id': accompanyingCourse.id } )}}"
|
||||
{%- endif -%}
|
||||
>
|
||||
{{ 'Cancel'|trans|chill_return_path_label }}
|
||||
|
@ -17,10 +17,6 @@
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||
<script type="text/javascript">
|
||||
window.addEventListener('DOMContentLoaded', function (e) {
|
||||
chill.displayAlertWhenLeavingUnsubmittedForm('form[name="{{ form.vars.form.vars.name }}"]',
|
||||
'{{ "You are going to leave a page with unsubmitted data. Are you sure you want to leave ?"|trans }}');
|
||||
});
|
||||
window.entity = {{ entity_json|json_encode|raw }};
|
||||
</script>
|
||||
{{ encore_entry_script_tags('vue_calendar') }}
|
||||
|
@ -0,0 +1 @@
|
||||
Votre travailleur social {{ calendar.mainUser.label }} vous rencontrera le {{ calendar.startDate|format_date('short', locale='fr') }} à {{ calendar.startDate|format_time('short', locale='fr') }} - LIEU.{% if calendar.mainUser.mainLocation is not null and calendar.mainUser.mainLocation.phonenumber1 is not null %} En cas d'indisponibilité, appelez-nous au {{ calendar.mainUser.mainLocation.phonenumber1|chill_format_phonenumber }}.{% endif %}
|
@ -0,0 +1 @@
|
||||
Votre RDV avec votre travailleur social {{ calendar.mainUser.label }} prévu le {{ calendar.startDate|format_date('short', locale='fr') }} à {{ calendar.startDate|format_time('short', locale='fr') }} est annulé. {% if calendar.mainUser.mainLocation is not null and calendar.mainUser.mainLocation.phonenumber1 is not null %} En cas de question, appelez-nous au {{ calendar.mainUser.mainLocation.phonenumber1|chill_format_phonenumber }}.{% endif %}
|
@ -0,0 +1,14 @@
|
||||
{% macro invite_span(invite) %}
|
||||
{% if invite.status == 'accepted' %}
|
||||
{% set fa = 'check' %}
|
||||
{% elseif invite.status == 'declined' %}
|
||||
{% set fa = 'times' %}
|
||||
{% elseif invite.status == 'pending' %}
|
||||
{% set fa = 'hourglass' %}
|
||||
{% elseif invite.status == 'tentative' %}
|
||||
{% set fa = 'question' %}
|
||||
{% else %}
|
||||
{% set fa = invite.status %}
|
||||
{% endif %}
|
||||
<i class="fa fa-{{ fa }}" title="{{ ('invite.'~invite.status)|trans|e('html_attr') }}"></i>
|
||||
{% endmacro %}
|
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Service\ShortMessageNotification;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
class BulkCalendarShortMessageSender
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private MessageBusInterface $messageBus;
|
||||
|
||||
private ShortMessageForCalendarBuilderInterface $messageForCalendarBuilder;
|
||||
|
||||
private CalendarForShortMessageProvider $provider;
|
||||
|
||||
public function __construct(CalendarForShortMessageProvider $provider, EntityManagerInterface $em, LoggerInterface $logger, MessageBusInterface $messageBus, ShortMessageForCalendarBuilderInterface $messageForCalendarBuilder)
|
||||
{
|
||||
$this->provider = $provider;
|
||||
$this->em = $em;
|
||||
$this->logger = $logger;
|
||||
$this->messageBus = $messageBus;
|
||||
$this->messageForCalendarBuilder = $messageForCalendarBuilder;
|
||||
}
|
||||
|
||||
public function sendBulkMessageToEligibleCalendars()
|
||||
{
|
||||
$countCalendars = 0;
|
||||
$countSms = 0;
|
||||
|
||||
foreach ($this->provider->getCalendars(new DateTimeImmutable('now')) as $calendar) {
|
||||
$smses = $this->messageForCalendarBuilder->buildMessageForCalendar($calendar);
|
||||
|
||||
foreach ($smses as $sms) {
|
||||
$this->messageBus->dispatch($sms);
|
||||
++$countSms;
|
||||
}
|
||||
|
||||
$this->em
|
||||
->createQuery('UPDATE ' . Calendar::class . ' c SET c.smsStatus = :smsStatus WHERE c.id = :id')
|
||||
->setParameters(['smsStatus' => Calendar::SMS_SENT, 'id' => $calendar->getId()])
|
||||
->execute();
|
||||
++$countCalendars;
|
||||
$this->em->refresh($calendar);
|
||||
}
|
||||
|
||||
$this->logger->info(__CLASS__ . 'a bulk of messages was sent', ['count_calendars' => $countCalendars, 'count_sms' => $countSms]);
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
<?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\CalendarBundle\Service\ShortMessageNotification;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Repository\CalendarRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use function count;
|
||||
|
||||
class CalendarForShortMessageProvider
|
||||
{
|
||||
private CalendarRepository $calendarRepository;
|
||||
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private RangeGeneratorInterface $rangeGenerator;
|
||||
|
||||
public function __construct(
|
||||
CalendarRepository $calendarRepository,
|
||||
EntityManagerInterface $em,
|
||||
RangeGeneratorInterface $rangeGenerator
|
||||
) {
|
||||
$this->calendarRepository = $calendarRepository;
|
||||
$this->em = $em;
|
||||
$this->rangeGenerator = $rangeGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate calendars instance.
|
||||
*
|
||||
* Warning: this method takes care of clearing the EntityManager at regular interval
|
||||
*
|
||||
* @return iterable|Calendar[]
|
||||
*/
|
||||
public function getCalendars(DateTimeImmutable $at): iterable
|
||||
{
|
||||
['startDate' => $startDate, 'endDate' => $endDate] = $this->rangeGenerator
|
||||
->generateRange($at);
|
||||
|
||||
$offset = 0;
|
||||
$batchSize = 10;
|
||||
|
||||
$calendars = $this->calendarRepository
|
||||
->findByNotificationAvailable($startDate, $endDate, $batchSize, $offset);
|
||||
|
||||
do {
|
||||
foreach ($calendars as $calendar) {
|
||||
++$offset;
|
||||
|
||||
yield $calendar;
|
||||
}
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
$calendars = $this->calendarRepository
|
||||
->findByNotificationAvailable($startDate, $endDate, $batchSize, $offset);
|
||||
} while (count($calendars) === $batchSize);
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
<?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\CalendarBundle\Service\ShortMessageNotification;
|
||||
|
||||
use DateInterval;
|
||||
use Monolog\DateTimeImmutable;
|
||||
use UnexpectedValueException;
|
||||
|
||||
/**
|
||||
* * Lundi => Envoi des rdv du mardi et mercredi.
|
||||
* * Mardi => Envoi des rdv du jeudi.
|
||||
* * Mercredi => Envoi des rdv du vendredi
|
||||
* * Jeudi => envoi des rdv du samedi et dimanche
|
||||
* * Vendredi => Envoi des rdv du lundi.
|
||||
*/
|
||||
class DefaultRangeGenerator implements RangeGeneratorInterface
|
||||
{
|
||||
public function generateRange(\DateTimeImmutable $date): array
|
||||
{
|
||||
$onMidnight = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $date->format('Y-m-d') . ' 00:00:00');
|
||||
|
||||
switch ($dow = (int) $onMidnight->format('w')) {
|
||||
case 6: // Saturday
|
||||
case 0: // Sunday
|
||||
return ['startDate' => null, 'endDate' => null];
|
||||
|
||||
case 1: // Monday
|
||||
// send for Tuesday and Wednesday
|
||||
$startDate = $onMidnight->add(new DateInterval('P1D'));
|
||||
$endDate = $startDate->add(new DateInterval('P2D'));
|
||||
|
||||
break;
|
||||
|
||||
case 2: // tuesday
|
||||
case 3: // wednesday
|
||||
$startDate = $onMidnight->add(new DateInterval('P2D'));
|
||||
$endDate = $startDate->add(new DateInterval('P1D'));
|
||||
|
||||
break;
|
||||
|
||||
case 4: // thursday
|
||||
$startDate = $onMidnight->add(new DateInterval('P2D'));
|
||||
$endDate = $startDate->add(new DateInterval('P2D'));
|
||||
|
||||
break;
|
||||
|
||||
case 5: // friday
|
||||
$startDate = $onMidnight->add(new DateInterval('P3D'));
|
||||
$endDate = $startDate->add(new DateInterval('P1D'));
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new UnexpectedValueException('a day of a week should not have the value: ' . $dow);
|
||||
}
|
||||
|
||||
return ['startDate' => $startDate, 'endDate' => $endDate];
|
||||
}
|
||||
}
|
@ -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\CalendarBundle\Service\ShortMessageNotification;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\MainBundle\Service\ShortMessage\ShortMessage;
|
||||
use Symfony\Component\Templating\EngineInterface;
|
||||
|
||||
class DefaultShortMessageForCalendarBuilder implements ShortMessageForCalendarBuilderInterface
|
||||
{
|
||||
private EngineInterface $engine;
|
||||
|
||||
public function __construct(
|
||||
EngineInterface $engine
|
||||
) {
|
||||
$this->engine = $engine;
|
||||
}
|
||||
|
||||
public function buildMessageForCalendar(Calendar $calendar): array
|
||||
{
|
||||
if (true !== $calendar->getSendSMS()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$toUsers = [];
|
||||
|
||||
foreach ($calendar->getPersons() as $person) {
|
||||
if (false === $person->getAcceptSMS() || null === $person->getAcceptSMS() || null === $person->getMobilenumber()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Calendar::SMS_PENDING === $calendar->getSmsStatus()) {
|
||||
$toUsers[] = new ShortMessage(
|
||||
$this->engine->render('@ChillCalendar/CalendarShortMessage/short_message.txt.twig', ['calendar' => $calendar]),
|
||||
$person->getMobilenumber(),
|
||||
ShortMessage::PRIORITY_LOW
|
||||
);
|
||||
} elseif (Calendar::SMS_CANCEL_PENDING === $calendar->getSmsStatus()) {
|
||||
$toUsers[] = new ShortMessage(
|
||||
$this->engine->render('@ChillCalendar/CalendarShortMessage/short_message_canceled.txt.twig', ['calendar' => $calendar]),
|
||||
$person->getMobilenumber(),
|
||||
ShortMessage::PRIORITY_LOW
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $toUsers;
|
||||
}
|
||||
}
|
@ -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\CalendarBundle\Service\ShortMessageNotification;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
interface RangeGeneratorInterface
|
||||
{
|
||||
/**
|
||||
* @return array<startDate: \DateTimeImmutable, endDate: \DateTimeImmutable>
|
||||
*/
|
||||
public function generateRange(DateTimeImmutable $date): array;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Service\ShortMessageNotification;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\MainBundle\Service\ShortMessage\ShortMessage;
|
||||
|
||||
interface ShortMessageForCalendarBuilderInterface
|
||||
{
|
||||
/**
|
||||
* @return array|ShortMessage[]
|
||||
*/
|
||||
public function buildMessageForCalendar(Calendar $calendar): array;
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
<?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\CalendarBundle\Tests\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use Symfony\Component\Messenger\Transport\InMemoryTransport;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
final class RemoteCalendarMSGraphSyncControllerTest extends WebTestCase
|
||||
{
|
||||
private const SAMPLE_BODY = <<<'JSON'
|
||||
{
|
||||
"value": [
|
||||
{
|
||||
"subscriptionId": "739703eb-80c4-4c03-b15a-ca370f19624b",
|
||||
"subscriptionExpirationDateTime": "2022-06-09T02:40:28-07:00",
|
||||
"changeType": "updated",
|
||||
"resource": "Users/4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4/Events/AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA=",
|
||||
"resourceData": {
|
||||
"@odata.type": "#Microsoft.Graph.Event",
|
||||
"@odata.id": "Users/4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4/Events/AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA=",
|
||||
"@odata.etag": "W/\"DwAAABYAAAAHduaxajFfTpv0kchk+m1FAAAlyzAU\"",
|
||||
"id": "AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA="
|
||||
},
|
||||
"clientState": "2k05qlr3ds2KzvUP3Ps4A+642fYaI8ThxHGIGbNr2p0MnNkmzxLTNEMxpMc/UEuDkBHfID7OYWj4DQc94vlEkPBdsh9sGTTkHxIE68hqkKkDKhwvfvdj6lS6Dus=",
|
||||
"tenantId": "421bf216-3f48-47bd-a7cf-8b1995cb24bd"
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
}
|
||||
|
||||
public function testSendNotification(): void
|
||||
{
|
||||
$client = self::createClient();
|
||||
$client->request(
|
||||
'POST',
|
||||
'/public/incoming-hook/calendar/msgraph/events/23',
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
self::SAMPLE_BODY
|
||||
);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertResponseStatusCodeSame(202);
|
||||
|
||||
/** @var InMemoryTransport $transport */
|
||||
$transport = self::$container->get('messenger.transport.async');
|
||||
$this->assertCount(1, $transport->getSent());
|
||||
}
|
||||
|
||||
public function testValidateSubscription(): void
|
||||
{
|
||||
$client = self::createClient();
|
||||
$client->request(
|
||||
'POST',
|
||||
'/public/incoming-hook/calendar/msgraph/events/23?validationToken=something%20to%20decode'
|
||||
);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$response = $client->getResponse();
|
||||
|
||||
$this->assertResponseHasHeader('Content-Type');
|
||||
$this->assertStringContainsString('text/plain', $response->headers->get('Content-Type'));
|
||||
$this->assertEquals('something to decode', $response->getContent());
|
||||
}
|
||||
}
|
@ -0,0 +1,248 @@
|
||||
<?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\CalendarBundle\Tests\RemoteCalendar\Connector\MSGraph;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteToLocalSync\CalendarRangeSyncer;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
final class CalendarRangeSyncerTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private const NOTIF_DELETE = <<<'JSON'
|
||||
{
|
||||
"value": [
|
||||
{
|
||||
"subscriptionId": "077e8d19-68b3-4d8e-9b1e-8b4ba6733799",
|
||||
"subscriptionExpirationDateTime": "2022-06-09T06:22:02-07:00",
|
||||
"changeType": "deleted",
|
||||
"resource": "Users/4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4/Events/AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA=",
|
||||
"resourceData": {
|
||||
"@odata.type": "#Microsoft.Graph.Event",
|
||||
"@odata.id": "Users/4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4/Events/AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA=",
|
||||
"@odata.etag": "W/\"CQAAAA==\"",
|
||||
"id": "AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA="
|
||||
},
|
||||
"clientState": "uds18apRCByqWIodFCHKeM0kJqhfr+qXL/rJWYn7xmtdQ4t03W2OHEOdGJ0Ceo52NAzOYVDpbfRM3TdrZDUiE09OxZkPX/vkpdcnipoiVnPPMFBQn05p8KhklOM=",
|
||||
"tenantId": "421bf216-3f48-47bd-a7cf-8b1995cb24bd"
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON;
|
||||
|
||||
private const NOTIF_UPDATE = <<<'JSON'
|
||||
{
|
||||
"value": [
|
||||
{
|
||||
"subscriptionId": "739703eb-80c4-4c03-b15a-ca370f19624b",
|
||||
"subscriptionExpirationDateTime": "2022-06-09T02:40:28-07:00",
|
||||
"changeType": "updated",
|
||||
"resource": "Users/4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4/Events/AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA=",
|
||||
"resourceData": {
|
||||
"@odata.type": "#Microsoft.Graph.Event",
|
||||
"@odata.id": "Users/4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4/Events/AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA=",
|
||||
"@odata.etag": "W/\"DwAAABYAAAAHduaxajFfTpv0kchk+m1FAAAlyzAU\"",
|
||||
"id": "AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA="
|
||||
},
|
||||
"clientState": "2k05qlr3ds2KzvUP3Ps4A+642fYaI8ThxHGIGbNr2p0MnNkmzxLTNEMxpMc/UEuDkBHfID7OYWj4DQc94vlEkPBdsh9sGTTkHxIE68hqkKkDKhwvfvdj6lS6Dus=",
|
||||
"tenantId": "421bf216-3f48-47bd-a7cf-8b1995cb24bd"
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON;
|
||||
|
||||
private const REMOTE_CALENDAR_RANGE = <<<'JSON'
|
||||
{
|
||||
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/events/$entity",
|
||||
"@odata.etag": "W/\"B3bmsWoxX06b9JHIZPptRQAAJcswFA==\"",
|
||||
"id": "AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA=",
|
||||
"createdDateTime": "2022-06-08T15:22:24.0096697Z",
|
||||
"lastModifiedDateTime": "2022-06-09T09:27:09.9223729Z",
|
||||
"changeKey": "B3bmsWoxX06b9JHIZPptRQAAJcswFA==",
|
||||
"categories": [],
|
||||
"transactionId": "90c23105-a6b1-b594-1811-e4ffa612092a",
|
||||
"originalStartTimeZone": "Romance Standard Time",
|
||||
"originalEndTimeZone": "Romance Standard Time",
|
||||
"iCalUId": "040000008200E00074C5B7101A82E00800000000A971DA8D4B7BD801000000000000000010000000BE3F4A21C9008E4FB35A4DE1F80E0118",
|
||||
"reminderMinutesBeforeStart": 15,
|
||||
"isReminderOn": true,
|
||||
"hasAttachments": false,
|
||||
"subject": "test notif",
|
||||
"bodyPreview": "",
|
||||
"importance": "normal",
|
||||
"sensitivity": "normal",
|
||||
"isAllDay": false,
|
||||
"isCancelled": false,
|
||||
"isOrganizer": true,
|
||||
"responseRequested": true,
|
||||
"seriesMasterId": null,
|
||||
"showAs": "busy",
|
||||
"type": "singleInstance",
|
||||
"webLink": "https://outlook.office365.com/owa/?itemid=AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk%2Bm1FAAAAAAENAAAHduaxajFfTpv0kchk%2Bm1FAAAl1BupAAA%3D&exvsurl=1&path=/calendar/item",
|
||||
"onlineMeetingUrl": null,
|
||||
"isOnlineMeeting": false,
|
||||
"onlineMeetingProvider": "unknown",
|
||||
"allowNewTimeProposals": true,
|
||||
"occurrenceId": null,
|
||||
"isDraft": false,
|
||||
"hideAttendees": false,
|
||||
"responseStatus": {
|
||||
"response": "organizer",
|
||||
"time": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
"body": {
|
||||
"contentType": "html",
|
||||
"content": ""
|
||||
},
|
||||
"start": {
|
||||
"dateTime": "2022-06-10T13:30:00.0000000",
|
||||
"timeZone": "UTC"
|
||||
},
|
||||
"end": {
|
||||
"dateTime": "2022-06-10T15:30:00.0000000",
|
||||
"timeZone": "UTC"
|
||||
},
|
||||
"location": {
|
||||
"displayName": "",
|
||||
"locationType": "default",
|
||||
"uniqueIdType": "unknown",
|
||||
"address": {},
|
||||
"coordinates": {}
|
||||
},
|
||||
"locations": [],
|
||||
"recurrence": null,
|
||||
"attendees": [],
|
||||
"organizer": {
|
||||
"emailAddress": {
|
||||
"name": "Diego Siciliani",
|
||||
"address": "DiegoS@2zy74l.onmicrosoft.com"
|
||||
}
|
||||
},
|
||||
"onlineMeeting": null,
|
||||
"calendar@odata.associationLink": "https://graph.microsoft.com/v1.0/Users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/calendar/$ref",
|
||||
"calendar@odata.navigationLink": "https://graph.microsoft.com/v1.0/Users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/calendar"
|
||||
}
|
||||
JSON;
|
||||
|
||||
public function testDeleteCalendarRangeWithAssociation(): void
|
||||
{
|
||||
$em = $this->prophesize(EntityManagerInterface::class);
|
||||
$em->remove(Argument::type(CalendarRange::class))->shouldNotBeCalled();
|
||||
|
||||
$machineHttpClient = new MockHttpClient([
|
||||
new MockResponse(self::REMOTE_CALENDAR_RANGE, ['http_code' => 200]),
|
||||
]);
|
||||
|
||||
$calendarRangeSyncer = new CalendarRangeSyncer(
|
||||
$em->reveal(),
|
||||
new NullLogger(),
|
||||
$machineHttpClient
|
||||
);
|
||||
|
||||
$calendarRange = new CalendarRange();
|
||||
$calendarRange
|
||||
->setUser($user = new User());
|
||||
$calendar = new Calendar();
|
||||
$calendar->setCalendarRange($calendarRange);
|
||||
|
||||
$notification = json_decode(self::NOTIF_DELETE, true);
|
||||
|
||||
$calendarRangeSyncer->handleCalendarRangeSync(
|
||||
$calendarRange,
|
||||
$notification['value'][0],
|
||||
$user
|
||||
);
|
||||
}
|
||||
|
||||
public function testDeleteCalendarRangeWithoutAssociation(): void
|
||||
{
|
||||
$em = $this->prophesize(EntityManagerInterface::class);
|
||||
$em->remove(Argument::type(CalendarRange::class))->shouldBeCalled();
|
||||
|
||||
$machineHttpClient = new MockHttpClient([
|
||||
new MockResponse(self::REMOTE_CALENDAR_RANGE, ['http_code' => 200]),
|
||||
]);
|
||||
|
||||
$calendarRangeSyncer = new CalendarRangeSyncer(
|
||||
$em->reveal(),
|
||||
new NullLogger(),
|
||||
$machineHttpClient
|
||||
);
|
||||
|
||||
$calendarRange = new CalendarRange();
|
||||
$calendarRange
|
||||
->setUser($user = new User());
|
||||
$notification = json_decode(self::NOTIF_DELETE, true);
|
||||
|
||||
$calendarRangeSyncer->handleCalendarRangeSync(
|
||||
$calendarRange,
|
||||
$notification['value'][0],
|
||||
$user
|
||||
);
|
||||
$this->assertTrue($calendarRange->preventEnqueueChanges);
|
||||
}
|
||||
|
||||
public function testUpdateCalendarRange(): void
|
||||
{
|
||||
$em = $this->prophesize(EntityManagerInterface::class);
|
||||
$machineHttpClient = new MockHttpClient([
|
||||
new MockResponse(self::REMOTE_CALENDAR_RANGE, ['http_code' => 200]),
|
||||
]);
|
||||
|
||||
$calendarRangeSyncer = new CalendarRangeSyncer(
|
||||
$em->reveal(),
|
||||
new NullLogger(),
|
||||
$machineHttpClient
|
||||
);
|
||||
|
||||
$calendarRange = new CalendarRange();
|
||||
$calendarRange
|
||||
->setUser($user = new User())
|
||||
->setStartDate(new DateTimeImmutable('2020-01-01 15:00:00'))
|
||||
->setEndDate(new DateTimeImmutable('2020-01-01 15:30:00'))
|
||||
->addRemoteAttributes([
|
||||
'lastModifiedDateTime' => 0,
|
||||
'changeKey' => 'abc',
|
||||
]);
|
||||
$notification = json_decode(self::NOTIF_UPDATE, true);
|
||||
|
||||
$calendarRangeSyncer->handleCalendarRangeSync(
|
||||
$calendarRange,
|
||||
$notification['value'][0],
|
||||
$user
|
||||
);
|
||||
|
||||
$this->assertStringContainsString(
|
||||
'2022-06-10T15:30:00',
|
||||
$calendarRange->getStartDate()->format(DateTimeImmutable::ATOM)
|
||||
);
|
||||
$this->assertStringContainsString(
|
||||
'2022-06-10T17:30:00',
|
||||
$calendarRange->getEndDate()->format(DateTimeImmutable::ATOM)
|
||||
);
|
||||
$this->assertTrue($calendarRange->preventEnqueueChanges);
|
||||
}
|
||||
}
|
@ -0,0 +1,589 @@
|
||||
<?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\CalendarBundle\Tests\RemoteCalendar\Connector\MSGraph;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||
use Chill\CalendarBundle\Entity\Invite;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteToLocalSync\CalendarSyncer;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
final class CalendarSyncerTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private const NOTIF_DELETE = <<<'JSON'
|
||||
{
|
||||
"value": [
|
||||
{
|
||||
"subscriptionId": "077e8d19-68b3-4d8e-9b1e-8b4ba6733799",
|
||||
"subscriptionExpirationDateTime": "2022-06-09T06:22:02-07:00",
|
||||
"changeType": "deleted",
|
||||
"resource": "Users/4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4/Events/AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA=",
|
||||
"resourceData": {
|
||||
"@odata.type": "#Microsoft.Graph.Event",
|
||||
"@odata.id": "Users/4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4/Events/AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA=",
|
||||
"@odata.etag": "W/\"CQAAAA==\"",
|
||||
"id": "AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA="
|
||||
},
|
||||
"clientState": "uds18apRCByqWIodFCHKeM0kJqhfr+qXL/rJWYn7xmtdQ4t03W2OHEOdGJ0Ceo52NAzOYVDpbfRM3TdrZDUiE09OxZkPX/vkpdcnipoiVnPPMFBQn05p8KhklOM=",
|
||||
"tenantId": "421bf216-3f48-47bd-a7cf-8b1995cb24bd"
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON;
|
||||
|
||||
private const NOTIF_UPDATE = <<<'JSON'
|
||||
{
|
||||
"value": [
|
||||
{
|
||||
"subscriptionId": "739703eb-80c4-4c03-b15a-ca370f19624b",
|
||||
"subscriptionExpirationDateTime": "2022-06-09T02:40:28-07:00",
|
||||
"changeType": "updated",
|
||||
"resource": "Users/4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4/Events/AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA=",
|
||||
"resourceData": {
|
||||
"@odata.type": "#Microsoft.Graph.Event",
|
||||
"@odata.id": "Users/4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4/Events/AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA=",
|
||||
"@odata.etag": "W/\"DwAAABYAAAAHduaxajFfTpv0kchk+m1FAAAlyzAU\"",
|
||||
"id": "AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA="
|
||||
},
|
||||
"clientState": "2k05qlr3ds2KzvUP3Ps4A+642fYaI8ThxHGIGbNr2p0MnNkmzxLTNEMxpMc/UEuDkBHfID7OYWj4DQc94vlEkPBdsh9sGTTkHxIE68hqkKkDKhwvfvdj6lS6Dus=",
|
||||
"tenantId": "421bf216-3f48-47bd-a7cf-8b1995cb24bd"
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON;
|
||||
|
||||
private const REMOTE_CALENDAR_NO_ATTENDEES = <<<'JSON'
|
||||
{
|
||||
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/events/$entity",
|
||||
"@odata.etag": "W/\"B3bmsWoxX06b9JHIZPptRQAAJcswFA==\"",
|
||||
"id": "AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA=",
|
||||
"createdDateTime": "2022-06-08T15:22:24.0096697Z",
|
||||
"lastModifiedDateTime": "2022-06-09T09:27:09.9223729Z",
|
||||
"changeKey": "B3bmsWoxX06b9JHIZPptRQAAJcswFA==",
|
||||
"categories": [],
|
||||
"transactionId": "90c23105-a6b1-b594-1811-e4ffa612092a",
|
||||
"originalStartTimeZone": "Romance Standard Time",
|
||||
"originalEndTimeZone": "Romance Standard Time",
|
||||
"iCalUId": "040000008200E00074C5B7101A82E00800000000A971DA8D4B7BD801000000000000000010000000BE3F4A21C9008E4FB35A4DE1F80E0118",
|
||||
"reminderMinutesBeforeStart": 15,
|
||||
"isReminderOn": true,
|
||||
"hasAttachments": false,
|
||||
"subject": "test notif",
|
||||
"bodyPreview": "",
|
||||
"importance": "normal",
|
||||
"sensitivity": "normal",
|
||||
"isAllDay": false,
|
||||
"isCancelled": false,
|
||||
"isOrganizer": true,
|
||||
"responseRequested": true,
|
||||
"seriesMasterId": null,
|
||||
"showAs": "busy",
|
||||
"type": "singleInstance",
|
||||
"webLink": "https://outlook.office365.com/owa/?itemid=AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk%2Bm1FAAAAAAENAAAHduaxajFfTpv0kchk%2Bm1FAAAl1BupAAA%3D&exvsurl=1&path=/calendar/item",
|
||||
"onlineMeetingUrl": null,
|
||||
"isOnlineMeeting": false,
|
||||
"onlineMeetingProvider": "unknown",
|
||||
"allowNewTimeProposals": true,
|
||||
"occurrenceId": null,
|
||||
"isDraft": false,
|
||||
"hideAttendees": false,
|
||||
"responseStatus": {
|
||||
"response": "organizer",
|
||||
"time": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
"body": {
|
||||
"contentType": "html",
|
||||
"content": ""
|
||||
},
|
||||
"start": {
|
||||
"dateTime": "2022-06-10T13:30:00.0000000",
|
||||
"timeZone": "UTC"
|
||||
},
|
||||
"end": {
|
||||
"dateTime": "2022-06-10T15:30:00.0000000",
|
||||
"timeZone": "UTC"
|
||||
},
|
||||
"location": {
|
||||
"displayName": "",
|
||||
"locationType": "default",
|
||||
"uniqueIdType": "unknown",
|
||||
"address": {},
|
||||
"coordinates": {}
|
||||
},
|
||||
"locations": [],
|
||||
"recurrence": null,
|
||||
"attendees": [],
|
||||
"organizer": {
|
||||
"emailAddress": {
|
||||
"name": "Diego Siciliani",
|
||||
"address": "DiegoS@2zy74l.onmicrosoft.com"
|
||||
}
|
||||
},
|
||||
"onlineMeeting": null,
|
||||
"calendar@odata.associationLink": "https://graph.microsoft.com/v1.0/Users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/calendar/$ref",
|
||||
"calendar@odata.navigationLink": "https://graph.microsoft.com/v1.0/Users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/calendar"
|
||||
}
|
||||
JSON;
|
||||
|
||||
private const REMOTE_CALENDAR_NOT_ORGANIZER = <<<'JSON'
|
||||
{
|
||||
"@odata.etag": "W/\"B3bmsWoxX06b9JHIZPptRQAAJcrv7A==\"",
|
||||
"id": "AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BuqAAA=",
|
||||
"createdDateTime": "2022-06-08T16:19:18.997293Z",
|
||||
"lastModifiedDateTime": "2022-06-08T16:22:18.7276485Z",
|
||||
"changeKey": "B3bmsWoxX06b9JHIZPptRQAAJcrv7A==",
|
||||
"categories": [],
|
||||
"transactionId": "42ac0b77-313e-20ca-2cac-1a3f58b3452d",
|
||||
"originalStartTimeZone": "Romance Standard Time",
|
||||
"originalEndTimeZone": "Romance Standard Time",
|
||||
"iCalUId": "040000008200E00074C5B7101A82E00800000000A8955A81537BD801000000000000000010000000007EB443987CD641B6794C4BA48EE356",
|
||||
"reminderMinutesBeforeStart": 15,
|
||||
"isReminderOn": true,
|
||||
"hasAttachments": false,
|
||||
"subject": "test 2",
|
||||
"bodyPreview": "________________________________________________________________________________\r\nRéunion Microsoft Teams\r\nRejoindre sur votre ordinateur ou application mobile\r\nCliquez ici pour participer à la réunion\r\nPour en savoir plus | Options de réunion\r\n________",
|
||||
"importance": "normal",
|
||||
"sensitivity": "normal",
|
||||
"isAllDay": false,
|
||||
"isCancelled": false,
|
||||
"isOrganizer": false,
|
||||
"responseRequested": true,
|
||||
"seriesMasterId": null,
|
||||
"showAs": "busy",
|
||||
"type": "singleInstance",
|
||||
"webLink": "https://outlook.office365.com/owa/?itemid=AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk%2Bm1FAAAAAAENAAAHduaxajFfTpv0kchk%2Bm1FAAAl1BuqAAA%3D&exvsurl=1&path=/calendar/item",
|
||||
"onlineMeetingUrl": null,
|
||||
"isOnlineMeeting": true,
|
||||
"onlineMeetingProvider": "teamsForBusiness",
|
||||
"allowNewTimeProposals": true,
|
||||
"occurrenceId": null,
|
||||
"isDraft": false,
|
||||
"hideAttendees": false,
|
||||
"responseStatus": {
|
||||
"response": "organizer",
|
||||
"time": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
"body": {
|
||||
"contentType": "html",
|
||||
"content": "<html>\r\n<head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n</head>\r\n<body>\r\n<div></div>\r\n<br>\r\n<div style=\"width:100%; height:20px\"><span style=\"white-space:nowrap; color:#5F5F5F; opacity:.36\">________________________________________________________________________________</span>\r\n</div>\r\n<div class=\"me-email-text\" lang=\"fr-FR\" style=\"color:#252424; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">\r\n<div style=\"margin-top:24px; margin-bottom:20px\"><span style=\"font-size:24px; color:#252424\">Réunion Microsoft Teams</span>\r\n</div>\r\n<div style=\"margin-bottom:20px\">\r\n<div style=\"margin-top:0px; margin-bottom:0px; font-weight:bold\"><span style=\"font-size:14px; color:#252424\">Rejoindre sur votre ordinateur ou application mobile</span>\r\n</div>\r\n<a href=\"https://teams.microsoft.com/l/meetup-join/19%3ameeting_NTE3ODUxY2ItNGJhNi00Y2UwLTljN2QtMmQ3YjAxNWY1Nzk2%40thread.v2/0?context=%7b%22Tid%22%3a%22421bf216-3f48-47bd-a7cf-8b1995cb24bd%22%2c%22Oid%22%3a%224feb0ae3-7ffb-48dd-891e-c86b2cdeefd4%22%7d\" class=\"me-email-headline\" style=\"font-size:14px; font-family:'Segoe UI Semibold','Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif; text-decoration:underline; color:#6264a7\">Cliquez\r\n ici pour participer à la réunion</a> </div>\r\n<div style=\"margin-bottom:24px; margin-top:20px\"><a href=\"https://aka.ms/JoinTeamsMeeting\" class=\"me-email-link\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">Pour en savoir\r\n plus</a> | <a href=\"https://teams.microsoft.com/meetingOptions/?organizerId=4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4&tenantId=421bf216-3f48-47bd-a7cf-8b1995cb24bd&threadId=19_meeting_NTE3ODUxY2ItNGJhNi00Y2UwLTljN2QtMmQ3YjAxNWY1Nzk2@thread.v2&messageId=0&language=fr-FR\" class=\"me-email-link\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">\r\nOptions de réunion</a> </div>\r\n</div>\r\n<div style=\"font-size:14px; margin-bottom:4px; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">\r\n</div>\r\n<div style=\"font-size:12px\"></div>\r\n<div></div>\r\n<div style=\"width:100%; height:20px\"><span style=\"white-space:nowrap; color:#5F5F5F; opacity:.36\">________________________________________________________________________________</span>\r\n</div>\r\n</body>\r\n</html>\r\n"
|
||||
},
|
||||
"start": {
|
||||
"dateTime": "2022-06-11T12:30:00.0000000",
|
||||
"timeZone": "UTC"
|
||||
},
|
||||
"end": {
|
||||
"dateTime": "2022-06-11T13:30:00.0000000",
|
||||
"timeZone": "UTC"
|
||||
},
|
||||
"location": {
|
||||
"displayName": "",
|
||||
"locationType": "default",
|
||||
"uniqueIdType": "unknown",
|
||||
"address": {},
|
||||
"coordinates": {}
|
||||
},
|
||||
"locations": [],
|
||||
"recurrence": null,
|
||||
"attendees": [
|
||||
{
|
||||
"type": "required",
|
||||
"status": {
|
||||
"response": "accepted",
|
||||
"time": "2022-06-08T16:22:15.1392583Z"
|
||||
},
|
||||
"emailAddress": {
|
||||
"name": "Alex Wilber",
|
||||
"address": "AlexW@2zy74l.onmicrosoft.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "required",
|
||||
"status": {
|
||||
"response": "declined",
|
||||
"time": "2022-06-08T16:22:15.1392583Z"
|
||||
},
|
||||
"emailAddress": {
|
||||
"name": "Alfred Nobel",
|
||||
"address": "alfredN@2zy74l.onmicrosoft.com"
|
||||
}
|
||||
}
|
||||
],
|
||||
"organizer": {
|
||||
"emailAddress": {
|
||||
"name": "Diego Siciliani",
|
||||
"address": "DiegoS@2zy74l.onmicrosoft.com"
|
||||
}
|
||||
},
|
||||
"onlineMeeting": {
|
||||
"joinUrl": "https://teams.microsoft.com/l/meetup-join/19%3ameeting_NTE3ODUxY2ItNGJhNi00Y2UwLTljN2QtMmQ3YjAxNWY1Nzk2%40thread.v2/0?context=%7b%22Tid%22%3a%22421bf216-3f48-47bd-a7cf-8b1995cb24bd%22%2c%22Oid%22%3a%224feb0ae3-7ffb-48dd-891e-c86b2cdeefd4%22%7d"
|
||||
}
|
||||
}
|
||||
JSON;
|
||||
|
||||
private const REMOTE_CALENDAR_WITH_ATTENDEES = <<<'JSON'
|
||||
{
|
||||
"@odata.etag": "W/\"B3bmsWoxX06b9JHIZPptRQAAJcrv7A==\"",
|
||||
"id": "AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BuqAAA=",
|
||||
"createdDateTime": "2022-06-08T16:19:18.997293Z",
|
||||
"lastModifiedDateTime": "2022-06-08T16:22:18.7276485Z",
|
||||
"changeKey": "B3bmsWoxX06b9JHIZPptRQAAJcrv7A==",
|
||||
"categories": [],
|
||||
"transactionId": "42ac0b77-313e-20ca-2cac-1a3f58b3452d",
|
||||
"originalStartTimeZone": "Romance Standard Time",
|
||||
"originalEndTimeZone": "Romance Standard Time",
|
||||
"iCalUId": "040000008200E00074C5B7101A82E00800000000A8955A81537BD801000000000000000010000000007EB443987CD641B6794C4BA48EE356",
|
||||
"reminderMinutesBeforeStart": 15,
|
||||
"isReminderOn": true,
|
||||
"hasAttachments": false,
|
||||
"subject": "test 2",
|
||||
"bodyPreview": "________________________________________________________________________________\r\nRéunion Microsoft Teams\r\nRejoindre sur votre ordinateur ou application mobile\r\nCliquez ici pour participer à la réunion\r\nPour en savoir plus | Options de réunion\r\n________",
|
||||
"importance": "normal",
|
||||
"sensitivity": "normal",
|
||||
"isAllDay": false,
|
||||
"isCancelled": false,
|
||||
"isOrganizer": true,
|
||||
"responseRequested": true,
|
||||
"seriesMasterId": null,
|
||||
"showAs": "busy",
|
||||
"type": "singleInstance",
|
||||
"webLink": "https://outlook.office365.com/owa/?itemid=AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk%2Bm1FAAAAAAENAAAHduaxajFfTpv0kchk%2Bm1FAAAl1BuqAAA%3D&exvsurl=1&path=/calendar/item",
|
||||
"onlineMeetingUrl": null,
|
||||
"isOnlineMeeting": true,
|
||||
"onlineMeetingProvider": "teamsForBusiness",
|
||||
"allowNewTimeProposals": true,
|
||||
"occurrenceId": null,
|
||||
"isDraft": false,
|
||||
"hideAttendees": false,
|
||||
"responseStatus": {
|
||||
"response": "organizer",
|
||||
"time": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
"body": {
|
||||
"contentType": "html",
|
||||
"content": "<html>\r\n<head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n</head>\r\n<body>\r\n<div></div>\r\n<br>\r\n<div style=\"width:100%; height:20px\"><span style=\"white-space:nowrap; color:#5F5F5F; opacity:.36\">________________________________________________________________________________</span>\r\n</div>\r\n<div class=\"me-email-text\" lang=\"fr-FR\" style=\"color:#252424; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">\r\n<div style=\"margin-top:24px; margin-bottom:20px\"><span style=\"font-size:24px; color:#252424\">Réunion Microsoft Teams</span>\r\n</div>\r\n<div style=\"margin-bottom:20px\">\r\n<div style=\"margin-top:0px; margin-bottom:0px; font-weight:bold\"><span style=\"font-size:14px; color:#252424\">Rejoindre sur votre ordinateur ou application mobile</span>\r\n</div>\r\n<a href=\"https://teams.microsoft.com/l/meetup-join/19%3ameeting_NTE3ODUxY2ItNGJhNi00Y2UwLTljN2QtMmQ3YjAxNWY1Nzk2%40thread.v2/0?context=%7b%22Tid%22%3a%22421bf216-3f48-47bd-a7cf-8b1995cb24bd%22%2c%22Oid%22%3a%224feb0ae3-7ffb-48dd-891e-c86b2cdeefd4%22%7d\" class=\"me-email-headline\" style=\"font-size:14px; font-family:'Segoe UI Semibold','Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif; text-decoration:underline; color:#6264a7\">Cliquez\r\n ici pour participer à la réunion</a> </div>\r\n<div style=\"margin-bottom:24px; margin-top:20px\"><a href=\"https://aka.ms/JoinTeamsMeeting\" class=\"me-email-link\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">Pour en savoir\r\n plus</a> | <a href=\"https://teams.microsoft.com/meetingOptions/?organizerId=4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4&tenantId=421bf216-3f48-47bd-a7cf-8b1995cb24bd&threadId=19_meeting_NTE3ODUxY2ItNGJhNi00Y2UwLTljN2QtMmQ3YjAxNWY1Nzk2@thread.v2&messageId=0&language=fr-FR\" class=\"me-email-link\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">\r\nOptions de réunion</a> </div>\r\n</div>\r\n<div style=\"font-size:14px; margin-bottom:4px; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">\r\n</div>\r\n<div style=\"font-size:12px\"></div>\r\n<div></div>\r\n<div style=\"width:100%; height:20px\"><span style=\"white-space:nowrap; color:#5F5F5F; opacity:.36\">________________________________________________________________________________</span>\r\n</div>\r\n</body>\r\n</html>\r\n"
|
||||
},
|
||||
"start": {
|
||||
"dateTime": "2022-06-11T12:30:00.0000000",
|
||||
"timeZone": "UTC"
|
||||
},
|
||||
"end": {
|
||||
"dateTime": "2022-06-11T13:30:00.0000000",
|
||||
"timeZone": "UTC"
|
||||
},
|
||||
"location": {
|
||||
"displayName": "",
|
||||
"locationType": "default",
|
||||
"uniqueIdType": "unknown",
|
||||
"address": {},
|
||||
"coordinates": {}
|
||||
},
|
||||
"locations": [],
|
||||
"recurrence": null,
|
||||
"attendees": [
|
||||
{
|
||||
"type": "required",
|
||||
"status": {
|
||||
"response": "accepted",
|
||||
"time": "2022-06-08T16:22:15.1392583Z"
|
||||
},
|
||||
"emailAddress": {
|
||||
"name": "Alex Wilber",
|
||||
"address": "AlexW@2zy74l.onmicrosoft.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "required",
|
||||
"status": {
|
||||
"response": "accepted",
|
||||
"time": "2022-06-08T16:22:15.1392583Z"
|
||||
},
|
||||
"emailAddress": {
|
||||
"name": "External User",
|
||||
"address": "external@example.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "required",
|
||||
"status": {
|
||||
"response": "declined",
|
||||
"time": "2022-06-08T16:22:15.1392583Z"
|
||||
},
|
||||
"emailAddress": {
|
||||
"name": "Alfred Nobel",
|
||||
"address": "alfredN@2zy74l.onmicrosoft.com"
|
||||
}
|
||||
}
|
||||
],
|
||||
"organizer": {
|
||||
"emailAddress": {
|
||||
"name": "Diego Siciliani",
|
||||
"address": "DiegoS@2zy74l.onmicrosoft.com"
|
||||
}
|
||||
},
|
||||
"onlineMeeting": {
|
||||
"joinUrl": "https://teams.microsoft.com/l/meetup-join/19%3ameeting_NTE3ODUxY2ItNGJhNi00Y2UwLTljN2QtMmQ3YjAxNWY1Nzk2%40thread.v2/0?context=%7b%22Tid%22%3a%22421bf216-3f48-47bd-a7cf-8b1995cb24bd%22%2c%22Oid%22%3a%224feb0ae3-7ffb-48dd-891e-c86b2cdeefd4%22%7d"
|
||||
}
|
||||
}
|
||||
JSON;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// all tests should run when timezone = +02:00
|
||||
$brussels = new DateTimeZone('Europe/Brussels');
|
||||
|
||||
if (7200 === $brussels->getOffset(new DateTimeImmutable())) {
|
||||
date_default_timezone_set('Europe/Brussels');
|
||||
} else {
|
||||
date_default_timezone_set('Europe/Moscow');
|
||||
}
|
||||
}
|
||||
|
||||
public function testHandleAttendeesConfirmingCalendar(): void
|
||||
{
|
||||
$machineHttpClient = new MockHttpClient([
|
||||
new MockResponse(self::REMOTE_CALENDAR_WITH_ATTENDEES, ['http_code' => 200]),
|
||||
]);
|
||||
|
||||
$userA = (new User())->setEmail('alexw@2zy74l.onmicrosoft.com')
|
||||
->setEmailCanonical('alexw@2zy74l.onmicrosoft.com');
|
||||
$userB = (new User())->setEmail('zzzzz@2zy74l.onmicrosoft.com')
|
||||
->setEmailCanonical('zzzzz@2zy74l.onmicrosoft.com');
|
||||
$userC = (new User())->setEmail('alfredN@2zy74l.onmicrosoft.com')
|
||||
->setEmailCanonical('alfredn@2zy74l.onmicrosoft.com');
|
||||
|
||||
$userRepository = $this->prophesize(UserRepositoryInterface::class);
|
||||
$userRepository->findOneByUsernameOrEmail(Argument::exact('AlexW@2zy74l.onmicrosoft.com'))
|
||||
->willReturn($userA);
|
||||
$userRepository->findOneByUsernameOrEmail(Argument::exact('zzzzz@2zy74l.onmicrosoft.com'))
|
||||
->willReturn($userB);
|
||||
$userRepository->findOneByUsernameOrEmail(Argument::exact('alfredN@2zy74l.onmicrosoft.com'))
|
||||
->willReturn($userC);
|
||||
$userRepository->findOneByUsernameOrEmail(Argument::exact('external@example.com'))
|
||||
->willReturn(null);
|
||||
|
||||
$calendarSyncer = new CalendarSyncer(
|
||||
new NullLogger(),
|
||||
$machineHttpClient,
|
||||
$userRepository->reveal()
|
||||
);
|
||||
|
||||
$calendar = new Calendar();
|
||||
$calendar
|
||||
->setMainUser($user = new User())
|
||||
->setStartDate(new DateTimeImmutable('2022-06-11 14:30:00'))
|
||||
->setEndDate(new DateTimeImmutable('2022-06-11 15:30:00'))
|
||||
->addUser($userA)
|
||||
->addUser($userB)
|
||||
->setCalendarRange(new CalendarRange())
|
||||
->addRemoteAttributes([
|
||||
'lastModifiedDateTime' => 0,
|
||||
'changeKey' => 'abcd',
|
||||
]);
|
||||
|
||||
$notification = json_decode(self::NOTIF_UPDATE, true);
|
||||
|
||||
$calendarSyncer->handleCalendarSync(
|
||||
$calendar,
|
||||
$notification['value'][0],
|
||||
$user
|
||||
);
|
||||
|
||||
$this->assertTrue($calendar->preventEnqueueChanges);
|
||||
$this->assertEquals(Calendar::STATUS_VALID, $calendar->getStatus());
|
||||
// user A is invited, and accepted
|
||||
$this->assertTrue($calendar->isInvited($userA));
|
||||
$this->assertEquals(Invite::ACCEPTED, $calendar->getInviteForUser($userA)->getStatus());
|
||||
$this->assertFalse($calendar->getInviteForUser($userA)->preventEnqueueChanges);
|
||||
// user B is no more invited
|
||||
$this->assertFalse($calendar->isInvited($userB));
|
||||
// user C is invited, but declined
|
||||
$this->assertFalse($calendar->getInviteForUser($userC)->preventEnqueueChanges);
|
||||
$this->assertTrue($calendar->isInvited($userC));
|
||||
$this->assertEquals(Invite::DECLINED, $calendar->getInviteForUser($userC)->getStatus());
|
||||
}
|
||||
|
||||
public function testHandleDeleteCalendar(): void
|
||||
{
|
||||
$machineHttpClient = new MockHttpClient([]);
|
||||
$userRepository = $this->prophesize(UserRepositoryInterface::class);
|
||||
|
||||
$calendarSyncer = new CalendarSyncer(
|
||||
new NullLogger(),
|
||||
$machineHttpClient,
|
||||
$userRepository->reveal()
|
||||
);
|
||||
|
||||
$calendar = new Calendar();
|
||||
$calendar
|
||||
->setMainUser($user = new User())
|
||||
->setCalendarRange($calendarRange = new CalendarRange());
|
||||
|
||||
$notification = json_decode(self::NOTIF_DELETE, true);
|
||||
|
||||
$calendarSyncer->handleCalendarSync(
|
||||
$calendar,
|
||||
$notification['value'][0],
|
||||
$user
|
||||
);
|
||||
|
||||
$this->assertEquals(Calendar::STATUS_CANCELED, $calendar->getStatus());
|
||||
$this->assertNull($calendar->getCalendarRange());
|
||||
$this->assertTrue($calendar->preventEnqueueChanges);
|
||||
}
|
||||
|
||||
public function testHandleMoveCalendar(): void
|
||||
{
|
||||
$machineHttpClient = new MockHttpClient([
|
||||
new MockResponse(self::REMOTE_CALENDAR_NO_ATTENDEES, ['http_code' => 200]),
|
||||
]);
|
||||
$userRepository = $this->prophesize(UserRepositoryInterface::class);
|
||||
|
||||
$calendarSyncer = new CalendarSyncer(
|
||||
new NullLogger(),
|
||||
$machineHttpClient,
|
||||
$userRepository->reveal()
|
||||
);
|
||||
|
||||
$calendar = new Calendar();
|
||||
$calendar
|
||||
->setMainUser($user = new User())
|
||||
->setStartDate(new DateTimeImmutable('2020-01-01 10:00:00'))
|
||||
->setEndDate(new DateTimeImmutable('2020-01-01 12:00:00'))
|
||||
->setCalendarRange(new CalendarRange())
|
||||
->addRemoteAttributes([
|
||||
'lastModifiedDateTime' => 0,
|
||||
'changeKey' => 'abcd',
|
||||
]);
|
||||
$notification = json_decode(self::NOTIF_UPDATE, true);
|
||||
|
||||
$calendarSyncer->handleCalendarSync(
|
||||
$calendar,
|
||||
$notification['value'][0],
|
||||
$user
|
||||
);
|
||||
|
||||
$this->assertStringContainsString(
|
||||
'2022-06-10T15:30:00',
|
||||
$calendar->getStartDate()->format(DateTimeImmutable::ATOM)
|
||||
);
|
||||
$this->assertStringContainsString(
|
||||
'2022-06-10T17:30:00',
|
||||
$calendar->getEndDate()->format(DateTimeImmutable::ATOM)
|
||||
);
|
||||
$this->assertTrue($calendar->preventEnqueueChanges);
|
||||
$this->assertEquals(Calendar::STATUS_MOVED, $calendar->getStatus());
|
||||
}
|
||||
|
||||
public function testHandleNotMovedCalendar(): void
|
||||
{
|
||||
$machineHttpClient = new MockHttpClient([
|
||||
new MockResponse(self::REMOTE_CALENDAR_NO_ATTENDEES, ['http_code' => 200]),
|
||||
]);
|
||||
$userRepository = $this->prophesize(UserRepositoryInterface::class);
|
||||
|
||||
$calendarSyncer = new CalendarSyncer(
|
||||
new NullLogger(),
|
||||
$machineHttpClient,
|
||||
$userRepository->reveal()
|
||||
);
|
||||
|
||||
$calendar = new Calendar();
|
||||
$calendar
|
||||
->setMainUser($user = new User())
|
||||
->setStartDate(new DateTimeImmutable('2022-06-10 15:30:00'))
|
||||
->setEndDate(new DateTimeImmutable('2022-06-10 17:30:00'))
|
||||
->setCalendarRange(new CalendarRange())
|
||||
->addRemoteAttributes([
|
||||
'lastModifiedDateTime' => 0,
|
||||
'changeKey' => 'abcd',
|
||||
]);
|
||||
$notification = json_decode(self::NOTIF_UPDATE, true);
|
||||
|
||||
$calendarSyncer->handleCalendarSync(
|
||||
$calendar,
|
||||
$notification['value'][0],
|
||||
$user
|
||||
);
|
||||
|
||||
$this->assertStringContainsString(
|
||||
'2022-06-10T15:30:00',
|
||||
$calendar->getStartDate()->format(DateTimeImmutable::ATOM)
|
||||
);
|
||||
$this->assertStringContainsString(
|
||||
'2022-06-10T17:30:00',
|
||||
$calendar->getEndDate()->format(DateTimeImmutable::ATOM)
|
||||
);
|
||||
$this->assertTrue($calendar->preventEnqueueChanges);
|
||||
$this->assertEquals(Calendar::STATUS_VALID, $calendar->getStatus());
|
||||
}
|
||||
|
||||
public function testHandleNotOrganizer(): void
|
||||
{
|
||||
// when isOrganiser === false, nothing should happens
|
||||
$machineHttpClient = new MockHttpClient([
|
||||
new MockResponse(self::REMOTE_CALENDAR_NOT_ORGANIZER, ['http_code' => 200]),
|
||||
]);
|
||||
$userRepository = $this->prophesize(UserRepositoryInterface::class);
|
||||
|
||||
$calendarSyncer = new CalendarSyncer(
|
||||
new NullLogger(),
|
||||
$machineHttpClient,
|
||||
$userRepository->reveal()
|
||||
);
|
||||
|
||||
$calendar = new Calendar();
|
||||
$calendar
|
||||
->setMainUser($user = new User())
|
||||
->setStartDate(new DateTimeImmutable('2020-01-01 10:00:00'))
|
||||
->setEndDate(new DateTimeImmutable('2020-01-01 12:00:00'))
|
||||
->setCalendarRange(new CalendarRange())
|
||||
->addRemoteAttributes([
|
||||
'lastModifiedDateTime' => 0,
|
||||
'changeKey' => 'abcd',
|
||||
]);
|
||||
$notification = json_decode(self::NOTIF_UPDATE, true);
|
||||
|
||||
$calendarSyncer->handleCalendarSync(
|
||||
$calendar,
|
||||
$notification['value'][0],
|
||||
$user
|
||||
);
|
||||
|
||||
$this->assertStringContainsString(
|
||||
'2020-01-01T10:00:00',
|
||||
$calendar->getStartDate()->format(DateTimeImmutable::ATOM)
|
||||
);
|
||||
$this->assertStringContainsString(
|
||||
'2020-01-01T12:00:00',
|
||||
$calendar->getEndDate()->format(DateTimeImmutable::ATOM)
|
||||
);
|
||||
|
||||
$this->assertEquals(Calendar::STATUS_VALID, $calendar->getStatus());
|
||||
}
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
<?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\CalendarBundle\Tests\Service\ShortMessageNotification;
|
||||
|
||||
use ArrayIterator;
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Service\ShortMessageNotification\BulkCalendarShortMessageSender;
|
||||
use Chill\CalendarBundle\Service\ShortMessageNotification\CalendarForShortMessageProvider;
|
||||
use Chill\CalendarBundle\Service\ShortMessageNotification\ShortMessageForCalendarBuilderInterface;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Service\ShortMessage\ShortMessage;
|
||||
use Chill\MainBundle\Test\PrepareUserTrait;
|
||||
use Chill\PersonBundle\DataFixtures\Helper\PersonRandomHelper;
|
||||
use DateInterval;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use libphonenumber\PhoneNumberUtil;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use stdClass;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
final class BulkCalendarShortMessageSenderTest extends KernelTestCase
|
||||
{
|
||||
use PersonRandomHelper;
|
||||
|
||||
use PrepareUserTrait;
|
||||
|
||||
use ProphecyTrait;
|
||||
|
||||
private array $toDelete = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
self::bootKernel();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
|
||||
foreach ($this->toDelete as [$entity, $id]) {
|
||||
$entity = $em->find($entity, $id);
|
||||
$em->remove($entity);
|
||||
}
|
||||
|
||||
$em->flush();
|
||||
}
|
||||
|
||||
public function testSendBulkMessageToEligibleCalendar()
|
||||
{
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
$calendar = new Calendar();
|
||||
$calendar
|
||||
->addPerson($this->getRandomPerson($em))
|
||||
->setMainUser($user = $this->prepareUser([]))
|
||||
->setStartDate(new DateTimeImmutable('now'))
|
||||
->setEndDate($calendar->getStartDate()->add(new DateInterval('PT30M')))
|
||||
->setSendSMS(true);
|
||||
|
||||
$user->setUsername(uniqid());
|
||||
$user->setEmail(uniqid() . '@gmail.com');
|
||||
$calendar->getPersons()->first()->setAcceptSMS(true);
|
||||
|
||||
// hack to prevent side effect with messages
|
||||
$calendar->preventEnqueueChanges = true;
|
||||
|
||||
$em->persist($user);
|
||||
//$this->toDelete[] = [User::class, $user->getId()];
|
||||
$em->persist($calendar);
|
||||
//$this->toDelete[] = [Calendar::class, $calendar->getId()];
|
||||
$em->flush();
|
||||
|
||||
$provider = $this->prophesize(CalendarForShortMessageProvider::class);
|
||||
$provider->getCalendars(Argument::type(DateTimeImmutable::class))
|
||||
->willReturn(new ArrayIterator([$calendar]));
|
||||
|
||||
$messageBuilder = $this->prophesize(ShortMessageForCalendarBuilderInterface::class);
|
||||
$messageBuilder->buildMessageForCalendar(Argument::type(Calendar::class))
|
||||
->willReturn(
|
||||
[
|
||||
new ShortMessage(
|
||||
'content',
|
||||
PhoneNumberUtil::getInstance()->parse('+32470123456', 'BE'),
|
||||
ShortMessage::PRIORITY_MEDIUM
|
||||
),
|
||||
]
|
||||
);
|
||||
|
||||
$bus = $this->prophesize(MessageBusInterface::class);
|
||||
$bus->dispatch(Argument::type(ShortMessage::class))
|
||||
->willReturn(new Envelope(new stdClass()))
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
$bulk = new BulkCalendarShortMessageSender(
|
||||
$provider->reveal(),
|
||||
$em,
|
||||
new NullLogger(),
|
||||
$bus->reveal(),
|
||||
$messageBuilder->reveal()
|
||||
);
|
||||
|
||||
$bulk->sendBulkMessageToEligibleCalendars();
|
||||
|
||||
$em->clear();
|
||||
$calendar = $em->find(Calendar::class, $calendar->getId());
|
||||
|
||||
$this->assertEquals(Calendar::SMS_SENT, $calendar->getSmsStatus());
|
||||
}
|
||||
}
|
@ -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\CalendarBundle\Tests\Service\ShortMessageNotification;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Repository\CalendarRepository;
|
||||
use Chill\CalendarBundle\Service\ShortMessageNotification\CalendarForShortMessageProvider;
|
||||
use Chill\CalendarBundle\Service\ShortMessageNotification\DefaultRangeGenerator;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
final class CalendarForShortMessageProviderTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testGetCalendars()
|
||||
{
|
||||
$calendarRepository = $this->prophesize(CalendarRepository::class);
|
||||
$calendarRepository->findByNotificationAvailable(
|
||||
Argument::type(DateTimeImmutable::class),
|
||||
Argument::type(DateTimeImmutable::class),
|
||||
Argument::type('int'),
|
||||
Argument::exact(0)
|
||||
)->will(static function ($args) {
|
||||
return array_fill(0, $args[2], new Calendar());
|
||||
})->shouldBeCalledTimes(1);
|
||||
$calendarRepository->findByNotificationAvailable(
|
||||
Argument::type(DateTimeImmutable::class),
|
||||
Argument::type(DateTimeImmutable::class),
|
||||
Argument::type('int'),
|
||||
Argument::not(0)
|
||||
)->will(static function ($args) {
|
||||
return array_fill(0, $args[2] - 1, new Calendar());
|
||||
})->shouldBeCalledTimes(1);
|
||||
|
||||
$em = $this->prophesize(EntityManagerInterface::class);
|
||||
$em->clear()->shouldBeCalled();
|
||||
|
||||
$provider = new CalendarForShortMessageProvider(
|
||||
$calendarRepository->reveal(),
|
||||
$em->reveal(),
|
||||
new DefaultRangeGenerator()
|
||||
);
|
||||
|
||||
$calendars = iterator_to_array($provider->getCalendars(new DateTimeImmutable('now')));
|
||||
|
||||
$this->assertGreaterThan(1, count($calendars));
|
||||
$this->assertLessThan(100, count($calendars));
|
||||
$this->assertContainsOnly(Calendar::class, $calendars);
|
||||
}
|
||||
|
||||
public function testGetCalendarsWithOnlyOneCalendar()
|
||||
{
|
||||
$calendarRepository = $this->prophesize(CalendarRepository::class);
|
||||
$calendarRepository->findByNotificationAvailable(
|
||||
Argument::type(DateTimeImmutable::class),
|
||||
Argument::type(DateTimeImmutable::class),
|
||||
Argument::type('int'),
|
||||
Argument::exact(0)
|
||||
)->will(static function ($args) {
|
||||
return array_fill(0, 1, new Calendar());
|
||||
})->shouldBeCalledTimes(1);
|
||||
$calendarRepository->findByNotificationAvailable(
|
||||
Argument::type(DateTimeImmutable::class),
|
||||
Argument::type(DateTimeImmutable::class),
|
||||
Argument::type('int'),
|
||||
Argument::not(0)
|
||||
)->will(static function ($args) {
|
||||
return [];
|
||||
})->shouldBeCalledTimes(1);
|
||||
|
||||
$em = $this->prophesize(EntityManagerInterface::class);
|
||||
$em->clear()->shouldBeCalled();
|
||||
|
||||
$provider = new CalendarForShortMessageProvider(
|
||||
$calendarRepository->reveal(),
|
||||
$em->reveal(),
|
||||
new DefaultRangeGenerator()
|
||||
);
|
||||
|
||||
$calendars = iterator_to_array($provider->getCalendars(new DateTimeImmutable('now')));
|
||||
|
||||
$this->assertEquals(1, count($calendars));
|
||||
$this->assertContainsOnly(Calendar::class, $calendars);
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
<?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\CalendarBundle\Tests\Service\ShortMessageNotification;
|
||||
|
||||
use Chill\CalendarBundle\Service\ShortMessageNotification\DefaultRangeGenerator;
|
||||
use DateTimeImmutable;
|
||||
use Iterator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
final class DefaultRangeGeneratorTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* * Lundi => Envoi des rdv du mardi et mercredi.
|
||||
* * Mardi => Envoi des rdv du jeudi.
|
||||
* * Mercredi => Envoi des rdv du vendredi
|
||||
* * Jeudi => envoi des rdv du samedi et dimanche
|
||||
* * Vendredi => Envoi des rdv du lundi.
|
||||
*/
|
||||
public function generateData(): Iterator
|
||||
{
|
||||
yield [
|
||||
new DateTimeImmutable('2022-06-13 10:45:00'),
|
||||
new DateTimeImmutable('2022-06-14 00:00:00'),
|
||||
new DateTimeImmutable('2022-06-16 00:00:00'),
|
||||
];
|
||||
|
||||
yield [
|
||||
new DateTimeImmutable('2022-06-14 15:45:00'),
|
||||
new DateTimeImmutable('2022-06-16 00:00:00'),
|
||||
new DateTimeImmutable('2022-06-17 00:00:00'),
|
||||
];
|
||||
|
||||
yield [
|
||||
new DateTimeImmutable('2022-06-15 13:45:18'),
|
||||
new DateTimeImmutable('2022-06-17 00:00:00'),
|
||||
new DateTimeImmutable('2022-06-18 00:00:00'),
|
||||
];
|
||||
|
||||
yield [
|
||||
new DateTimeImmutable('2022-06-16 01:30:55'),
|
||||
new DateTimeImmutable('2022-06-18 00:00:00'),
|
||||
new DateTimeImmutable('2022-06-20 00:00:00'),
|
||||
];
|
||||
|
||||
yield [
|
||||
new DateTimeImmutable('2022-06-17 21:30:55'),
|
||||
new DateTimeImmutable('2022-06-20 00:00:00'),
|
||||
new DateTimeImmutable('2022-06-21 00:00:00'),
|
||||
];
|
||||
|
||||
yield [
|
||||
new DateTimeImmutable('2022-06-18 21:30:55'),
|
||||
null,
|
||||
null,
|
||||
];
|
||||
|
||||
yield [
|
||||
new DateTimeImmutable('2022-06-19 21:30:55'),
|
||||
null,
|
||||
null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider generateData
|
||||
*/
|
||||
public function testGenerateRange(DateTimeImmutable $date, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate)
|
||||
{
|
||||
$generator = new DefaultRangeGenerator();
|
||||
|
||||
['startDate' => $actualStartDate, 'endDate' => $actualEndDate] = $generator->generateRange($date);
|
||||
|
||||
if (null === $startDate) {
|
||||
$this->assertNull($actualStartDate);
|
||||
$this->assertNull($actualEndDate);
|
||||
} else {
|
||||
$this->assertEquals($startDate->format(DateTimeImmutable::ATOM), $actualStartDate->format(DateTimeImmutable::ATOM));
|
||||
$this->assertEquals($endDate->format(DateTimeImmutable::ATOM), $actualEndDate->format(DateTimeImmutable::ATOM));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
<?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\CalendarBundle\Tests\Service\ShortMessageNotification;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Service\ShortMessageNotification\DefaultShortMessageForCalendarBuilder;
|
||||
use Chill\MainBundle\Entity\Location;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use DateInterval;
|
||||
use DateTimeImmutable;
|
||||
use libphonenumber\PhoneNumberFormat;
|
||||
use libphonenumber\PhoneNumberUtil;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Templating\EngineInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
final class DefaultShortMessageForCalendarBuilderTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private PhoneNumberUtil $phoneNumberUtil;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->phoneNumberUtil = PhoneNumberUtil::getInstance();
|
||||
}
|
||||
|
||||
public function testBuildMessageForCalendar()
|
||||
{
|
||||
$calendar = new Calendar();
|
||||
$calendar
|
||||
->setStartDate(new DateTimeImmutable('now'))
|
||||
->setEndDate($calendar->getStartDate()->add(new DateInterval('PT30M')))
|
||||
->setMainUser($user = new User())
|
||||
->addPerson($person = new Person())
|
||||
->setSendSMS(false);
|
||||
$user
|
||||
->setLabel('Alex')
|
||||
->setMainLocation($location = new Location());
|
||||
$location->setName('LOCAMAT');
|
||||
$person
|
||||
->setMobilenumber($this->phoneNumberUtil->parse('+32470123456', 'BE'))
|
||||
->setAcceptSMS(false);
|
||||
|
||||
$engine = $this->prophesize(EngineInterface::class);
|
||||
$engine->render(Argument::exact('@ChillCalendar/CalendarShortMessage/short_message.txt.twig'), Argument::withKey('calendar'))
|
||||
->willReturn('message content')
|
||||
->shouldBeCalledTimes(1);
|
||||
$engine->render(Argument::exact('@ChillCalendar/CalendarShortMessage/short_message_canceled.txt.twig'), Argument::withKey('calendar'))
|
||||
->willReturn('message canceled')
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
$builder = new DefaultShortMessageForCalendarBuilder(
|
||||
$engine->reveal()
|
||||
);
|
||||
|
||||
// if the calendar should not send sms
|
||||
$sms = $builder->buildMessageForCalendar($calendar);
|
||||
$this->assertCount(0, $sms);
|
||||
|
||||
// if the person do not accept sms
|
||||
$calendar->setSendSMS(true);
|
||||
$sms = $builder->buildMessageForCalendar($calendar);
|
||||
$this->assertCount(0, $sms);
|
||||
|
||||
// person accepts sms
|
||||
$person->setAcceptSMS(true);
|
||||
$sms = $builder->buildMessageForCalendar($calendar);
|
||||
|
||||
$this->assertCount(1, $sms);
|
||||
$this->assertEquals(
|
||||
'+32470123456',
|
||||
$this->phoneNumberUtil->format($sms[0]->getPhoneNumber(), PhoneNumberFormat::E164)
|
||||
);
|
||||
$this->assertEquals('message content', $sms[0]->getContent());
|
||||
$this->assertEquals('low', $sms[0]->getPriority());
|
||||
|
||||
// if the calendar is canceled
|
||||
$calendar
|
||||
->setSmsStatus(Calendar::SMS_SENT)
|
||||
->setStatus(Calendar::STATUS_CANCELED);
|
||||
|
||||
$sms = $builder->buildMessageForCalendar($calendar);
|
||||
|
||||
$this->assertCount(1, $sms);
|
||||
$this->assertEquals(
|
||||
'+32470123456',
|
||||
$this->phoneNumberUtil->format($sms[0]->getPhoneNumber(), PhoneNumberFormat::E164)
|
||||
);
|
||||
$this->assertEquals('message canceled', $sms[0]->getContent());
|
||||
$this->assertEquals('low', $sms[0]->getPriority());
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
<?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\Calendar;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20220606153851 extends AbstractMigration
|
||||
{
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_calendar.calendar ALTER endDate TYPE TIMESTAMP(0) WITH TIME ZONE');
|
||||
$this->addSql('ALTER TABLE chill_calendar.calendar ALTER endDate DROP DEFAULT');
|
||||
$this->addSql('ALTER TABLE chill_calendar.calendar ALTER startDate TYPE TIMESTAMP(0) WITH TIME ZONE');
|
||||
$this->addSql('ALTER TABLE chill_calendar.calendar ALTER startDate DROP DEFAULT');
|
||||
$this->addSql('COMMENT ON COLUMN chill_calendar.calendar.enddate IS \'(DC2Type:datetimetz_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN chill_calendar.calendar.startdate IS \'(DC2Type:datetimetz_immutable)\'');
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'remove timezone from dates in calendar entity';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_calendar.calendar ALTER startdate TYPE TIMESTAMP(0) WITHOUT TIME ZONE');
|
||||
$this->addSql('ALTER TABLE chill_calendar.calendar ALTER startdate DROP DEFAULT');
|
||||
$this->addSql('ALTER TABLE chill_calendar.calendar ALTER enddate TYPE TIMESTAMP(0) WITHOUT TIME ZONE');
|
||||
$this->addSql('ALTER TABLE chill_calendar.calendar ALTER enddate DROP DEFAULT');
|
||||
$this->addSql('COMMENT ON COLUMN chill_calendar.calendar.startDate IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN chill_calendar.calendar.endDate IS \'(DC2Type:datetime_immutable)\'');
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
<?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\Calendar;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20220606154119 extends AbstractMigration
|
||||
{
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_calendar.calendar_range ALTER endDate TYPE TIMESTAMP(0) WITH TIME ZONE');
|
||||
$this->addSql('ALTER TABLE chill_calendar.calendar_range ALTER endDate DROP DEFAULT');
|
||||
$this->addSql('ALTER TABLE chill_calendar.calendar_range ALTER startDate TYPE TIMESTAMP(0) WITH TIME ZONE');
|
||||
$this->addSql('ALTER TABLE chill_calendar.calendar_range ALTER startDate DROP DEFAULT');
|
||||
$this->addSql('COMMENT ON COLUMN chill_calendar.calendar_range.enddate IS \'(DC2Type:datetimetz_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN chill_calendar.calendar_range.startdate IS \'(DC2Type:datetimetz_immutable)\'');
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'remove timezone from calendar range';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_calendar.calendar_range ALTER startdate TYPE TIMESTAMP(0) WITHOUT TIME ZONE');
|
||||
$this->addSql('ALTER TABLE chill_calendar.calendar_range ALTER startdate DROP DEFAULT');
|
||||
$this->addSql('ALTER TABLE chill_calendar.calendar_range ALTER enddate TYPE TIMESTAMP(0) WITHOUT TIME ZONE');
|
||||
$this->addSql('ALTER TABLE chill_calendar.calendar_range ALTER enddate DROP DEFAULT');
|
||||
$this->addSql('COMMENT ON COLUMN chill_calendar.calendar_range.startDate IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN chill_calendar.calendar_range.endDate IS \'(DC2Type:datetime_immutable)\'');
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Calendar;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20220608084052 extends AbstractMigration
|
||||
{
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_calendar.invite DROP remoteAttributes');
|
||||
$this->addSql('ALTER TABLE chill_calendar.invite DROP remoteId');
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add remoteId for invitation';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_calendar.invite ADD remoteAttributes JSON DEFAULT \'[]\' NOT NULL');
|
||||
$this->addSql('ALTER TABLE chill_calendar.invite ADD remoteId TEXT DEFAULT \'\' NOT NULL');
|
||||
$this->addSql('CREATE INDEX idx_calendar_invite_remote ON chill_calendar.invite (remoteId)');
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
<?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\Calendar;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20220609200857 extends AbstractMigration
|
||||
{
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX chill_calendar.idx_calendar_range_remote');
|
||||
$this->addSql('CREATE INDEX idx_calendar_range_remote ON chill_calendar.calendar_range (remoteId)');
|
||||
$this->addSql('DROP INDEX chill_calendar.idx_calendar_remote');
|
||||
$this->addSql('CREATE INDEX idx_calendar_remote ON chill_calendar.calendar (remoteId)');
|
||||
$this->addSql('DROP INDEX chill_calendar.idx_calendar_invite_remote');
|
||||
$this->addSql('CREATE INDEX idx_calendar_invite_remote ON chill_calendar.invite (remoteId)');
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Set an unique contraint on remoteId on calendar object which are synced to a remote';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX chill_calendar.idx_calendar_range_remote');
|
||||
$this->addSql('CREATE UNIQUE INDEX idx_calendar_range_remote ON chill_calendar.calendar_range (remoteId) WHERE remoteId <> \'\'');
|
||||
$this->addSql('DROP INDEX chill_calendar.idx_calendar_remote');
|
||||
$this->addSql('CREATE UNIQUE INDEX idx_calendar_remote ON chill_calendar.calendar (remoteId) WHERE remoteId <> \'\'');
|
||||
$this->addSql('DROP INDEX chill_calendar.idx_calendar_invite_remote');
|
||||
$this->addSql('CREATE UNIQUE INDEX idx_calendar_invite_remote ON chill_calendar.invite (remoteId) WHERE remoteId <> \'\'');
|
||||
}
|
||||
}
|
@ -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\Calendar;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20220613202636 extends AbstractMigration
|
||||
{
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_calendar.calendar DROP smsStatus');
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add sms status on calendars';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_calendar.calendar ADD smsStatus TEXT DEFAULT \'sms_pending\' NOT NULL');
|
||||
}
|
||||
}
|
@ -43,6 +43,9 @@ chill_calendar:
|
||||
form:
|
||||
The main user is mandatory. He will organize the appointment.: L'utilisateur principal est obligatoire. Il est l'organisateur de l'événement.
|
||||
Create for referrer: Créer pour le référent
|
||||
start date filter: Début du rendez-vous
|
||||
From: Du
|
||||
To: Au
|
||||
|
||||
remote_ms_graph:
|
||||
freebusy_statuses:
|
||||
@ -56,3 +59,9 @@ remote_ms_graph:
|
||||
|
||||
remote_calendar:
|
||||
calendar_range_title: Plage de disponibilité Chill
|
||||
|
||||
invite:
|
||||
accepted: Accepté
|
||||
declined: Refusé
|
||||
pending: En attente
|
||||
tentative: Accepté provisoirement
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
@ -313,6 +316,13 @@ class ChillMainExtension extends Extension implements
|
||||
// Note: the controller are loaded inside compiler pass
|
||||
}
|
||||
|
||||
protected function configureSms(array $config, ContainerBuilder $container, Loader\YamlFileLoader $loader): void
|
||||
{
|
||||
$dsn = parse_url($config['dsn']);
|
||||
|
||||
dump($dsn);
|
||||
}
|
||||
|
||||
protected function prependCruds(ContainerBuilder $container)
|
||||
{
|
||||
$container->prependExtensionConfig('chill_main', [
|
||||
|
@ -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,10 +359,17 @@ class User implements AdvancedUserInterface
|
||||
}
|
||||
}
|
||||
|
||||
public function setAttributeByDomain(string $domain, string $key, $value): self
|
||||
{
|
||||
$this->attributes[$domain][$key] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the attributes with existing attributes.
|
||||
*
|
||||
* Only the key provided will be created or updated.
|
||||
* Only the key provided will be created or updated. For a two-level array, use @see{User::setAttributeByDomain}
|
||||
*/
|
||||
public function setAttributes(array $attributes): self
|
||||
{
|
||||
|
@ -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' => $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':
|
||||
|
@ -15,14 +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;
|
||||
|
||||
@ -144,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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -206,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;
|
||||
}
|
@ -1,6 +1,3 @@
|
||||
// import bootstrap variables
|
||||
@import 'bootstrap/scss/variables';
|
||||
|
||||
// Variables
|
||||
//
|
||||
// Variables should follow the `$component-state-property-size` formula for
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ul :class="listClasses" 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>
|
||||
@ -44,6 +44,11 @@ export default {
|
||||
removableIfSet: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
displayPicked: {
|
||||
// display picked entities.
|
||||
type: Boolean,
|
||||
default: true,
|
||||
}
|
||||
},
|
||||
emits: ['addNewEntity', 'removeEntity'],
|
||||
|
@ -17,6 +17,11 @@ export default {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: false
|
||||
},
|
||||
hoursOnly: {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: false,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -10,6 +10,28 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if form.dateRanges is defined %}
|
||||
{% if form.dateRanges|length > 0 %}
|
||||
{% for dateRangeName, _o in form.dateRanges %}
|
||||
<div class="row gx-2">
|
||||
<div class="col-md-5">
|
||||
{{ form_label(form.dateRanges[dateRangeName])}}
|
||||
</div>
|
||||
<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 %}
|
||||
|
@ -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,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, ?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, ?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;
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ services:
|
||||
tags:
|
||||
- { name: 'doctrine.event_subscriber' }
|
||||
|
||||
|
||||
# workflow related
|
||||
Chill\MainBundle\Workflow\:
|
||||
resource: '../Workflow/'
|
||||
|
@ -0,0 +1,5 @@
|
||||
services:
|
||||
Chill\MainBundle\Service\ShortMessage\:
|
||||
resource: '../Service/ShortMessage'
|
||||
autowire: true
|
||||
autoconfigure: true
|
@ -46,7 +46,7 @@ class SocialAction
|
||||
private $desactivationDate;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity=Evaluation::class, inversedBy="socialActions")
|
||||
* @ORM\ManyToMany(targetEntity=Evaluation::class, mappedBy="socialActions")
|
||||
* @ORM\JoinTable(name="chill_person_social_work_evaluation_action")
|
||||
*/
|
||||
private Collection $evaluations;
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 8694ad7c4de306f9d5e35af966d24fb2e3e1c2ff
|
||||
Subproject commit 5b35e7ccd0735e5593835e28acbf82386c18e1b6
|
Loading…
x
Reference in New Issue
Block a user