wip: synchro of calendar range

This commit is contained in:
Julien Fastré 2022-05-11 12:17:29 +02:00
parent ba8a2327be
commit e895da31d7
17 changed files with 492 additions and 4 deletions

View File

@ -37,6 +37,7 @@
"symfony/http-foundation": "^4.4", "symfony/http-foundation": "^4.4",
"symfony/intl": "^4.4", "symfony/intl": "^4.4",
"symfony/mailer": "^5.4", "symfony/mailer": "^5.4",
"symfony/messenger": "^5.4",
"symfony/mime": "^5.4", "symfony/mime": "^5.4",
"symfony/monolog-bundle": "^3.5", "symfony/monolog-bundle": "^3.5",
"symfony/security-bundle": "^4.4", "symfony/security-bundle": "^4.4",

View File

@ -12,6 +12,10 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Entity; namespace Chill\CalendarBundle\Entity;
use Chill\CalendarBundle\Repository\CalendarRangeRepository; use Chill\CalendarBundle\Repository\CalendarRangeRepository;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
@ -20,11 +24,17 @@ use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\Groups;
/** /**
* @ORM\Table(name="chill_calendar.calendar_range") * @ORM\Table(name="chill_calendar.calendar_range", indexes={@ORM\Index(name="idx_calendar_range_remote", columns={"remoteId"})})
* @ORM\Entity(repositoryClass=CalendarRangeRepository::class) * @ORM\Entity(repositoryClass=CalendarRangeRepository::class)
*/ */
class CalendarRange class CalendarRange implements TrackCreationInterface, TrackUpdateInterface
{ {
use RemoteCalendarTrait;
use TrackCreationTrait;
use TrackUpdateTrait;
/** /**
* @ORM\OneToMany(targetEntity=Calendar::class, * @ORM\OneToMany(targetEntity=Calendar::class,
* mappedBy="calendarRange") * mappedBy="calendarRange")

View File

@ -0,0 +1,56 @@
<?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\Entity;
use Doctrine\ORM\Mapping as ORM;
trait RemoteCalendarTrait
{
/**
* @ORM\Column(type="json", options={"default": "[]"}, nullable=false)
*/
private array $remoteAttributes = [];
/**
* @ORM\Column(type="text", options={"default": ""}, nullable=false)
*/
private string $remoteId = '';
public function addRemoteAttributes(array $remoteAttributes): self
{
$this->remoteAttributes = array_merge($this->remoteAttributes, $remoteAttributes);
return $this;
}
public function getRemoteAttributes(): array
{
return $this->remoteAttributes;
}
public function getRemoteId(): string
{
return $this->remoteId;
}
public function hasRemoteId(): bool
{
return '' !== $this->remoteId;
}
public function setRemoteId(string $remoteId): self
{
$this->remoteId = $remoteId;
return $this;
}
}

View File

@ -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\Messenger\Doctrine;
use Chill\CalendarBundle\Entity\Calendar;
use Doctrine\ORM\Event\LifecycleEventArgs;
class CalendarEntityListener
{
public function postPersistCalendar(Calendar $calendar, LifecycleEventArgs $args): void
{
}
}

View File

@ -0,0 +1,53 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Messenger\Doctrine;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\CalendarBundle\Messenger\Message\CalendarRangeMessage;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Security\Core\Security;
class CalendarRangeEntityListener
{
private MessageBusInterface $messageBus;
private Security $security;
public function __construct(MessageBusInterface $messageBus, Security $security)
{
$this->messageBus = $messageBus;
$this->security = $security;
}
public function postPersist(CalendarRange $calendarRange, LifecycleEventArgs $eventArgs): void
{
$this->messageBus->dispatch(
new CalendarRangeMessage(
$calendarRange,
CalendarRangeMessage::CALENDAR_RANGE_PERSIST,
$this->security->getUser()
)
);
}
public function postUpdate(CalendarRange $calendarRange, LifecycleEventArgs $eventArgs): void
{
$this->messageBus->dispatch(
new CalendarRangeMessage(
$calendarRange,
CalendarRangeMessage::CALENDAR_RANGE_UPDATE,
$this->security->getUser()
)
);
}
}

View File

@ -0,0 +1,50 @@
<?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\CalendarRangeMessage;
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
use Chill\CalendarBundle\Repository\CalendarRangeRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
/**
* @AsMessageHandler
*/
class CalendarRangeToRemoteHandler implements MessageHandlerInterface
{
private CalendarRangeRepository $calendarRangeRepository;
private EntityManagerInterface $entityManager;
private RemoteCalendarConnectorInterface $remoteCalendarConnector;
public function __construct(
CalendarRangeRepository $calendarRangeRepository,
RemoteCalendarConnectorInterface $remoteCalendarConnector,
EntityManagerInterface $entityManager
) {
$this->calendarRangeRepository = $calendarRangeRepository;
$this->remoteCalendarConnector = $remoteCalendarConnector;
$this->entityManager = $entityManager;
}
public function __invoke(CalendarRangeMessage $calendarRangeMessage): void
{
$range = $this->calendarRangeRepository->find($calendarRangeMessage->getCalendarRangeId());
$this->remoteCalendarConnector->syncCalendarRange($range);
$this->entityManager->flush();
}
}

View File

@ -0,0 +1,32 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Messenger\Handler;
use Chill\CalendarBundle\Messenger\Message\CalendarMessage;
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
/**
* @AsMessageHandler
*/
class CalendarToRemoteHandler implements MessageHandlerInterface
{
private RemoteCalendarConnectorInterface $calendarConnector;
private EntityManagerInterface $em;
public function __invoke(CalendarMessage $calendarMessage)
{
}
}

View File

@ -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\Messenger\Message;
use Chill\CalendarBundle\Entity\Calendar;
class CalendarMessage
{
public const CALENDAR_PERSIST = 'CHILL_CALENDAR_CALENDAR_PERSIST';
public const CALENDAR_UPDATE = 'CHILL_CALENDAR_CALENDAR_UPDATE';
private string $action;
private int $calendarId;
public function __construct(Calendar $calendar, string $action)
{
$this->calendarId = $calendar->getId();
$this->action = $action;
}
public function getAction(): string
{
return $this->action;
}
public function getCalendarId(): ?int
{
return $this->calendarId;
}
}

View File

@ -0,0 +1,53 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Messenger\Message;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\MainBundle\Entity\User;
class CalendarRangeMessage
{
public const CALENDAR_RANGE_PERSIST = 'CHILL_CALENDAR_CALENDAR_RANGE_PERSIST';
public const CALENDAR_RANGE_UPDATE = 'CHILL_CALENDAR_CALENDAR_RANGE_UPDATE';
private string $action;
private ?int $byUserId = null;
private int $calendarRangeId;
public function __construct(CalendarRange $calendarRange, string $action, ?User $byUser)
{
$this->action = $action;
$this->calendarRangeId = $calendarRange->getId();
if (null !== $byUser) {
$this->byUserId = $byUser->getId();
}
}
public function getAction(): string
{
return $this->action;
}
public function getByUserId(): ?int
{
return $this->byUserId;
}
public function getCalendarRangeId(): ?int
{
return $this->calendarRangeId;
}
}

View File

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph; namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\CalendarBundle\RemoteCalendar\Model\RemoteEvent; use Chill\CalendarBundle\RemoteCalendar\Model\RemoteEvent;
use DateTimeImmutable; use DateTimeImmutable;
use DateTimeZone; use DateTimeZone;
@ -22,6 +23,8 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/ */
class RemoteEventConverter class RemoteEventConverter
{ {
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_DATE_FORMAT = 'Y-m-d\TH:i:s.u0';
private DateTimeZone $defaultDateTimeZone; private DateTimeZone $defaultDateTimeZone;
@ -37,6 +40,37 @@ class RemoteEventConverter
$this->remoteDateTimeZone = self::getRemoteTimeZone(); $this->remoteDateTimeZone = self::getRemoteTimeZone();
} }
/**
* Transform a CalendarRange into a representation suitable for storing into MSGraph.
*
* @return array an array representation for event in MS Graph
*/
public function calendarRangeToEvent(CalendarRange $calendarRange): array
{
return [
'subject' => $this->translator->trans('remote_calendar.calendar_range_title'),
'start' => [
'dateTime' => $calendarRange->getStartDate()->setTimezone($this->remoteDateTimeZone)
->format(self::REMOTE_DATE_FORMAT),
'timeZone' => 'UTC',
],
'end' => [
'dateTime' => $calendarRange->getEndDate()->setTimezone($this->remoteDateTimeZone)
->format(self::REMOTE_DATE_FORMAT),
'timeZone' => 'UTC',
],
'attendees' => [
[
'emailAddress' => [
'address' => $calendarRange->getUser()->getEmailCanonical(),
'name' => $calendarRange->getUser()->getLabel(),
],
],
],
'isReminderOn' => false,
];
}
public function convertAvailabilityToRemoteEvent(array $event): RemoteEvent public function convertAvailabilityToRemoteEvent(array $event): RemoteEvent
{ {
$startDate = $startDate =

View File

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\RemoteCalendar\Connector; namespace Chill\CalendarBundle\RemoteCalendar\Connector;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineHttpClient; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineHttpClient;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MapCalendarToUser; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MapCalendarToUser;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\OnBehalfOfUserHttpClient; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\OnBehalfOfUserHttpClient;
@ -18,6 +19,7 @@ use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\OnBehalfOfUserTokenSto
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteEventConverter; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteEventConverter;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use DateTimeImmutable; use DateTimeImmutable;
use Exception;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@ -105,6 +107,56 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
} }
} }
public function syncCalendarRange(CalendarRange $calendarRange): void
{
if ($calendarRange->hasRemoteId()) {
throw new Exception('update existing not implemented');
}
$this->createRemoteCalendarRange($calendarRange);
}
private function createRemoteCalendarRange(CalendarRange $calendarRange): void
{
$userId = $this->mapCalendarToUser->getUserId($calendarRange->getUser());
$calendarId = $this->mapCalendarToUser->getCalendarId($calendarRange->getUser());
if (null === $userId || null === $calendarId) {
$this->logger->warning('user does not have userId nor calendarId', [
'user_id' => $calendarRange->getUser()->getId(),
'calendar_range_id' => $calendarRange->getId(),
]);
return;
}
$eventData = $this->remoteEventConverter->calendarRangeToEvent($calendarRange);
try {
$event = $this->machineHttpClient->request(
'POST',
'users/' . $userId . '/calendar/events',
[
'json' => $eventData,
]
)->toArray();
} catch (ClientExceptionInterface $e) {
$this->logger->warning('could not save calendar range to remote', [
'exception' => $e->getTraceAsString(),
'calendarRangeId' => $calendarRange->getId(),
]);
throw $e;
}
dump($event);
$calendarRange->setRemoteId($event['id'])
->addRemoteAttributes([
'lastModifiedDateTime' => (DateTimeImmutable::createFromFormat(RemoteEventConverter::REMOTE_DATETIMEZONE_FORMAT, $event['lastModifiedDateTime']))->getTimestamp(),
'changeKey' => $event['changeKey'],
]);
}
private function getScheduleTimesForUser(User $user, DateTimeImmutable $startDate, DateTimeImmutable $endDate): array private function getScheduleTimesForUser(User $user, DateTimeImmutable $startDate, DateTimeImmutable $endDate): array
{ {
$userId = $this->mapCalendarToUser->getUserId($user); $userId = $this->mapCalendarToUser->getUserId($user);

View File

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\RemoteCalendar\Connector; namespace Chill\CalendarBundle\RemoteCalendar\Connector;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use DateTimeImmutable; use DateTimeImmutable;
use LogicException; use LogicException;
@ -32,4 +33,8 @@ class NullRemoteCalendarConnector implements RemoteCalendarConnectorInterface
{ {
return []; return [];
} }
public function syncCalendarRange(CalendarRange $calendarRange): void
{
}
} }

View File

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\RemoteCalendar\Connector; namespace Chill\CalendarBundle\RemoteCalendar\Connector;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\CalendarBundle\RemoteCalendar\Model\RemoteEvent; use Chill\CalendarBundle\RemoteCalendar\Model\RemoteEvent;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use DateTimeImmutable; use DateTimeImmutable;
@ -35,4 +36,6 @@ interface RemoteCalendarConnectorInterface
* @return array|RemoteEvent[] * @return array|RemoteEvent[]
*/ */
public function listEventsForUser(User $user, DateTimeImmutable $startDate, DateTimeImmutable $endDate): array; public function listEventsForUser(User $user, DateTimeImmutable $startDate, DateTimeImmutable $endDate): array;
public function syncCalendarRange(CalendarRange $calendarRange): void;
} }

View File

@ -18,6 +18,11 @@ services:
autoconfigure: true autoconfigure: true
resource: '../../Command/' resource: '../../Command/'
Chill\CalendarBundle\Messenger\:
autowire: true
autoconfigure: true
resource: '../../Messenger/'
Chill\CalendarBundle\Command\AzureGrantAdminConsentAndAcquireToken: Chill\CalendarBundle\Command\AzureGrantAdminConsentAndAcquireToken:
autoconfigure: true autoconfigure: true
autowire: true autowire: true

View File

@ -7,4 +7,17 @@ services:
name: 'doctrine.orm.entity_listener' name: 'doctrine.orm.entity_listener'
event: 'postPersist' event: 'postPersist'
entity: 'Chill\ActivityBundle\Entity\Activity' entity: 'Chill\ActivityBundle\Entity\Activity'
Chill\CalendarBundle\Messenger\Doctrine\CalendarRangeEntityListener:
autowire: true
autoconfigure: true
tags:
-
name: 'doctrine.orm.entity_listener'
event: 'postPersist'
entity: 'Chill\CalendarBundle\Entity\CalendarRange'
-
name: 'doctrine.orm.entity_listener'
event: 'postUpdate'
entity: 'Chill\CalendarBundle\Entity\CalendarRange'

View File

@ -0,0 +1,55 @@
<?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 Version20220510155609 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_calendar.calendar_range DROP CONSTRAINT FK_38D57D0565FF1AEC');
$this->addSql('ALTER TABLE chill_calendar.calendar_range DROP CONSTRAINT FK_38D57D053174800F');
$this->addSql('DROP INDEX chill_calendar.IDX_38D57D0565FF1AEC');
$this->addSql('DROP INDEX chill_calendar.IDX_38D57D053174800F');
$this->addSql('DROP INDEX chill_calendar.idx_calendar_range_remote');
$this->addSql('ALTER TABLE chill_calendar.calendar_range DROP remoteId');
$this->addSql('ALTER TABLE chill_calendar.calendar_range DROP remoteAttributes');
$this->addSql('ALTER TABLE chill_calendar.calendar_range DROP updatedAt');
$this->addSql('ALTER TABLE chill_calendar.calendar_range DROP createdAt');
$this->addSql('ALTER TABLE chill_calendar.calendar_range DROP updatedBy_id');
$this->addSql('ALTER TABLE chill_calendar.calendar_range DROP createdBy_id');
}
public function getDescription(): string
{
return 'Add columns on calendar range to handle remote';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_calendar.calendar_range ADD remoteId TEXT DEFAULT \'\' NOT NULL');
$this->addSql('ALTER TABLE chill_calendar.calendar_range ADD remoteAttributes JSON DEFAULT \'[]\' NOT NULL');
$this->addSql('ALTER TABLE chill_calendar.calendar_range ADD updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
$this->addSql('ALTER TABLE chill_calendar.calendar_range ADD createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
$this->addSql('ALTER TABLE chill_calendar.calendar_range ADD updatedBy_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE chill_calendar.calendar_range ADD createdBy_id INT DEFAULT NULL');
$this->addSql('COMMENT ON COLUMN chill_calendar.calendar_range.updatedAt IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN chill_calendar.calendar_range.createdAt IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE chill_calendar.calendar_range ADD CONSTRAINT FK_38D57D0565FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_calendar.calendar_range ADD CONSTRAINT FK_38D57D053174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX IDX_38D57D0565FF1AEC ON chill_calendar.calendar_range (updatedBy_id)');
$this->addSql('CREATE INDEX IDX_38D57D053174800F ON chill_calendar.calendar_range (createdBy_id)');
$this->addSql('CREATE INDEX idx_calendar_range_remote ON chill_calendar.calendar_range (remoteId)');
}
}

View File

@ -14,7 +14,7 @@ start date: début du rendez-vous
end date: fin du rendez-vous end date: fin du rendez-vous
cancel reason: motif d'annulation cancel reason: motif d'annulation
status: Statut du rendez-vous status: Statut du rendez-vous
calendar location: Localistion du rendez-vous calendar location: Localisation du rendez-vous
calendar comment: Remarque sur le rendez-vous calendar comment: Remarque sur le rendez-vous
sendSMS: Envoi d'un SMS sendSMS: Envoi d'un SMS
Send s m s: Envoi d'un SMS ? Send s m s: Envoi d'un SMS ?
@ -35,3 +35,6 @@ remote_ms_graph:
oof: En dehors du bureau oof: En dehors du bureau
workingElsewhere: Travaille à l'extérieur workingElsewhere: Travaille à l'extérieur
unknown: Inconnu unknown: Inconnu
remote_calendar:
calendar_range_title: Plage de disponibilité Chill