From 3df06e1ebab8e64c73bc6c93f65a04d6aba1b0e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Sat, 2 Jul 2022 14:31:27 +0200 Subject: [PATCH] add api endpoint for listing calendars --- .../Controller/CalendarAPIController.php | 62 +++++++++++ .../ChillCalendarBundle/Entity/Calendar.php | 19 ++-- .../Repository/CalendarRepository.php | 100 ++++++++++++++++++ .../ChillCalendarBundle/chill.api.specs.yaml | 38 ++++++- 4 files changed, 211 insertions(+), 8 deletions(-) diff --git a/src/Bundle/ChillCalendarBundle/Controller/CalendarAPIController.php b/src/Bundle/ChillCalendarBundle/Controller/CalendarAPIController.php index 70cf34cd4..6589b30f1 100644 --- a/src/Bundle/ChillCalendarBundle/Controller/CalendarAPIController.php +++ b/src/Bundle/ChillCalendarBundle/Controller/CalendarAPIController.php @@ -11,11 +11,73 @@ declare(strict_types=1); namespace Chill\CalendarBundle\Controller; +use Chill\CalendarBundle\Repository\CalendarRepository; use Chill\MainBundle\CRUD\Controller\ApiController; +use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Serializer\Model\Collection; +use DateTimeImmutable; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\Routing\Annotation\Route; class CalendarAPIController extends ApiController { + private CalendarRepository $calendarRepository; + + public function __construct(CalendarRepository $calendarRepository) + { + $this->calendarRepository = $calendarRepository; + } + + /** + * @Route("/api/1.0/calendar/calendar/by-user/{id}.{_format}", + * name="chill_api_single_calendar_list_by-user", + * requirements={"_format": "json"} + * ) + */ + public function listByUser(User $user, Request $request, string $_format): JsonResponse + { + $this->denyAccessUnlessGranted('ROLE_USER'); + + if (!$request->query->has('dateFrom')) { + throw new BadRequestHttpException('You must provide a dateFrom parameter'); + } + + if (false === $dateFrom = DateTimeImmutable::createFromFormat( + DateTimeImmutable::ATOM, + $request->query->get('dateFrom') + )) { + throw new BadRequestHttpException('dateFrom not parsable'); + } + + if (!$request->query->has('dateTo')) { + throw new BadRequestHttpException('You must provide a dateTo parameter'); + } + + if (false === $dateTo = DateTimeImmutable::createFromFormat( + DateTimeImmutable::ATOM, + $request->query->get('dateTo') + )) { + throw new BadRequestHttpException('dateTo not parsable'); + } + + $total = $this->calendarRepository->countByUser($user, $dateFrom, $dateTo); + $paginator = $this->getPaginatorFactory()->create($total); + $ranges = $this->calendarRepository->findByUser( + $user, + $dateFrom, + $dateTo, + $paginator->getItemsPerPage(), + $paginator->getCurrentPageFirstItemNumber() + ); + + $collection = new Collection($ranges, $paginator); + + return $this->json($collection, Response::HTTP_OK, [], ['groups' => ['calendar:light']]); + } + protected function customizeQuery(string $action, Request $request, $qb): void { if ($request->query->has('main_user')) { diff --git a/src/Bundle/ChillCalendarBundle/Entity/Calendar.php b/src/Bundle/ChillCalendarBundle/Entity/Calendar.php index c0168ab74..1ed858de7 100644 --- a/src/Bundle/ChillCalendarBundle/Entity/Calendar.php +++ b/src/Bundle/ChillCalendarBundle/Entity/Calendar.php @@ -85,7 +85,6 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface /** * @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod", inversedBy="calendars") - * @Serializer\Groups({"read"}) */ private AccompanyingPeriod $accompanyingPeriod; @@ -113,7 +112,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface /** * @ORM\Column(type="datetime_immutable", nullable=false) - * @Serializer\Groups({"calendar:read", "read"}) + * @Serializer\Groups({"calendar:read", "read", "calendar:light"}) * @Assert\NotNull(message="calendar.An end date is required") */ private ?DateTimeImmutable $endDate = null; @@ -122,7 +121,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") - * @Serializer\Groups({"calendar:read", "read"}) + * @Serializer\Groups({"calendar:read", "read", "calendar:light"}) */ private ?int $id = null; @@ -147,7 +146,8 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface /** * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User") - * @Serializer\Groups({"calendar:read", "read"}) + * @Serializer\Groups({"calendar:read", "read", "calendar:light"}) + * @Serializer\Context(normalizationContext={"read"}, groups={"calendar:light"}) * @Assert\NotNull(message="calendar.A main user is mandatory") */ private ?User $mainUser = null; @@ -155,7 +155,8 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface /** * @ORM\ManyToMany(targetEntity="Chill\PersonBundle\Entity\Person", inversedBy="calendars") * @ORM\JoinTable(name="chill_calendar.calendar_to_persons") - * @Serializer\Groups({"calendar:read", "read"}) + * @Serializer\Groups({"calendar:read", "read", "calendar:light"}) + * @Serializer\Context(normalizationContext={"read"}, groups={"calendar:light"}) * @Assert\Count(min=1, minMessage="calendar.At least {{ limit }} person is required.") */ private Collection $persons; @@ -169,7 +170,8 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface /** * @ORM\ManyToMany(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty") * @ORM\JoinTable(name="chill_calendar.calendar_to_thirdparties") - * @Serializer\Groups({"calendar:read", "read"}) + * @Serializer\Groups({"calendar:read", "read", "calendar:light"}) + * @Serializer\Context(normalizationContext={"read"}, groups={"calendar:light"}) */ private Collection $professionals; @@ -185,13 +187,16 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface /** * @ORM\Column(type="datetime_immutable", nullable=false) - * @Serializer\Groups({"calendar:read", "read"}) + * @Serializer\Groups({"calendar:read", "read", "calendar:light"}) + * @Serializer\Context(normalizationContext={"read"}, groups={"calendar:light"}) * @Assert\NotNull(message="calendar.A start date is required") */ private ?DateTimeImmutable $startDate = null; /** * @ORM\Column(type="string", length=255, nullable=false, options={"default": "valid"}) + * @Serializer\Groups({"calendar:read", "read", "calendar:light"}) + * @Serializer\Context(normalizationContext={"read"}, groups={"calendar:light"}) */ private string $status = self::STATUS_VALID; diff --git a/src/Bundle/ChillCalendarBundle/Repository/CalendarRepository.php b/src/Bundle/ChillCalendarBundle/Repository/CalendarRepository.php index ef803a1b3..8af66cc34 100644 --- a/src/Bundle/ChillCalendarBundle/Repository/CalendarRepository.php +++ b/src/Bundle/ChillCalendarBundle/Repository/CalendarRepository.php @@ -12,12 +12,16 @@ declare(strict_types=1); namespace Chill\CalendarBundle\Repository; use Chill\CalendarBundle\Entity\Calendar; +use Chill\MainBundle\Entity\User; use Chill\PersonBundle\Entity\AccompanyingPeriod; use DateTimeImmutable; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Query\ResultSetMapping; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; +use function count; class CalendarRepository implements ObjectRepository { @@ -33,6 +37,14 @@ class CalendarRepository implements ObjectRepository return $this->repository->count(['accompanyingPeriod' => $period]); } + public function countByUser(User $user, DateTimeImmutable $from, DateTimeImmutable $to): int + { + return $this->buildQueryByUser($user, $from, $to) + ->select('COUNT(c)') + ->getQuery() + ->getSingleScalarResult(); + } + public function find($id): ?Calendar { return $this->repository->find($id); @@ -84,16 +96,104 @@ class CalendarRepository implements ObjectRepository return $qb->getQuery()->getResult(); } + /** + * @return array|Calendar[] + */ + public function findByUser(User $user, DateTimeImmutable $from, DateTimeImmutable $to, ?int $limit = null, ?int $offset = null): array + { + $qb = $this->buildQueryByUser($user, $from, $to)->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); } + /** + * Given a list of remote ids, return an array where + * keys are the remoteIds, and value is a boolean, true if the + * id is present in database. + * + * @param array|list $remoteIds + * + * @return array + */ + public function findRemoteIdsPresent(array $remoteIds): array + { + if (0 === count($remoteIds)) { + return []; + } + + $remoteIdsStr = implode( + ', ', + array_fill(0, count($remoteIds), '((?))') + ); + + $sql = "SELECT + sq.remoteId as remoteid, + EXISTS (SELECT 1 FROM chill_calendar.calendar c WHERE c.remoteId = sq.remoteId) AS present + FROM + ( + VALUES {$remoteIdsStr} + ) AS sq(remoteId); + "; + + $rsm = new ResultSetMapping(); + $rsm + ->addScalarResult('remoteid', 'remoteId', Types::STRING) + ->addScalarResult('present', 'present', Types::BOOLEAN); + + $rows = $this->em + ->createNativeQuery( + $sql, + $rsm + ) + ->setParameters(array_values($remoteIds)) + ->getResult(); + + $results = []; + + foreach ($rows as $r) { + $results[$r['remoteId']] = $r['present']; + } + + return $results; + } + public function getClassName() { return Calendar::class; } + private function buildQueryByUser(User $user, DateTimeImmutable $from, DateTimeImmutable $to): QueryBuilder + { + $qb = $this->repository->createQueryBuilder('c'); + + return $qb + ->where( + $qb->expr()->andX( + $qb->expr()->eq('c.mainUser', ':user'), + $qb->expr()->gte('c.startDate', ':startDate'), + $qb->expr()->lte('c.endDate', ':endDate'), + ) + ) + ->setParameters([ + 'user' => $user, + 'startDate' => $from, + 'endDate' => $to, + ]); + } + private function queryByNotificationAvailable(DateTimeImmutable $startDate, DateTimeImmutable $endDate): QueryBuilder { $qb = $this->repository->createQueryBuilder('c'); diff --git a/src/Bundle/ChillCalendarBundle/chill.api.specs.yaml b/src/Bundle/ChillCalendarBundle/chill.api.specs.yaml index 1c055a24e..8d8df2568 100644 --- a/src/Bundle/ChillCalendarBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillCalendarBundle/chill.api.specs.yaml @@ -95,7 +95,43 @@ paths: description: "ok" 404: description: "not found" - 401: + 403: + description: "Unauthorized" + + /1.0/calendar/calendar/by-user/{userId}.json: + get: + tags: + - calendar + summary: Return a list of calendars for a user + parameters: + - name: userId + in: path + required: true + description: The user id + schema: + type: integer + format: integer + minimum: 1 + - name: dateFrom + in: query + required: true + description: The date from, formatted as ISO8601 string + schema: + type: string + format: date-time + - name: dateTo + in: query + required: true + description: The date to, formatted as ISO8601 string + schema: + type: string + format: date-time + responses: + 200: + description: "ok" + 404: + description: "not found" + 403: description: "Unauthorized" /1.0/calendar/calendar-range.json: