diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 37c40986d..dd1aba738 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -3368,7 +3368,7 @@ 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/Ticket/IndexAction.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$eventId of method AppBundle\\\\Event\\\\Model\\\\Repository\\\\VoteRepository\\:\\:getVotesByEvent\\(\\) expects int, int\\|null given\\.$#', + 'message' => '#^Parameter \\#1 \\$eventId of method AppBundle\\\\Event\\\\Entity\\\\Repository\\\\VoteRepository\\:\\:getVotesByEvent\\(\\) expects int, int\\|null given\\.$#', 'identifier' => 'argument.type', 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/VotesListeAction.php', @@ -4147,12 +4147,6 @@ 'count' => 2, 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/Event/ShowAction.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'votes\' on mixed\\.$#', - 'identifier' => 'offsetAccess.nonOffsetAccessible', - 'count' => 2, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/Event/ShowAction.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot call method getPath\\(\\) on mixed\\.$#', 'identifier' => 'method.nonObject', @@ -4429,12 +4423,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/Ticket/TicketAction.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'asvg\' on mixed\\.$#', - 'identifier' => 'offsetAccess.nonOffsetAccessible', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/Vote/IndexAction.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'sessions\' on mixed\\.$#', 'identifier' => 'offsetAccess.nonOffsetAccessible', @@ -4448,38 +4436,32 @@ 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/Vote/IndexAction.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#3 \\$vote of method AppBundle\\\\Controller\\\\Event\\\\Vote\\\\VoteController\\:\\:createVoteForm\\(\\) expects AppBundle\\\\Event\\\\Model\\\\Vote, mixed given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/Vote/IndexAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method setSubmittedOn\\(\\) on mixed\\.$#', - 'identifier' => 'method.nonObject', + 'message' => '#^Cannot access property \\$submittedOn on mixed\\.$#', + 'identifier' => 'property.nonObject', 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/Vote/NewAction.php', ]; $ignoreErrors[] = [ - 'message' => '#^Cannot call method setTalk\\(\\) on mixed\\.$#', - 'identifier' => 'method.nonObject', + 'message' => '#^Cannot access property \\$talk on mixed\\.$#', + 'identifier' => 'property.nonObject', 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/Vote/NewAction.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$user of method AppBundle\\\\Event\\\\Model\\\\Vote\\:\\:setUser\\(\\) expects int, int\\|null given\\.$#', + 'message' => '#^Parameter \\#1 \\$vote of method AppBundle\\\\Event\\\\Entity\\\\Repository\\\\VoteRepository\\:\\:upsert\\(\\) expects AppBundle\\\\Event\\\\Entity\\\\Vote, mixed given\\.$#', 'identifier' => 'argument.type', 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/Vote/NewAction.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$vote of method AppBundle\\\\Event\\\\Model\\\\Repository\\\\VoteRepository\\:\\:upsert\\(\\) expects AppBundle\\\\Event\\\\Model\\\\Vote, mixed given\\.$#', + 'message' => '#^Parameter \\#1 \\$vote of method AppBundle\\\\Notifier\\\\SlackNotifier\\:\\:notifyVote\\(\\) expects AppBundle\\\\Event\\\\Entity\\\\Vote, mixed given\\.$#', 'identifier' => 'argument.type', 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/Vote/NewAction.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$vote of method AppBundle\\\\Notifier\\\\SlackNotifier\\:\\:notifyVote\\(\\) expects AppBundle\\\\Event\\\\Model\\\\Vote, mixed given\\.$#', - 'identifier' => 'argument.type', + 'message' => '#^Property AppBundle\\\\Event\\\\Entity\\\\Vote\\:\\:\\$userId \\(int\\) does not accept int\\|null\\.$#', + 'identifier' => 'assign.propertyType', 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/Vote/NewAction.php', ]; @@ -5041,6 +5023,18 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Email/Mailer/Mailer.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Cannot cast mixed to int\\.$#', + 'identifier' => 'cast.int', + 'count' => 7, + 'path' => __DIR__ . '/sources/AppBundle/Event/Entity/Repository/VoteRepository.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$datetime of class DateTimeImmutable constructor expects string, mixed given\\.$#', + 'identifier' => 'argument.type', + 'count' => 1, + 'path' => __DIR__ . '/sources/AppBundle/Event/Entity/Repository/VoteRepository.php', +]; $ignoreErrors[] = [ 'message' => '#^Class AppBundle\\\\Event\\\\Form\\\\EventCFPTextType extends generic class Symfony\\\\Component\\\\Form\\\\AbstractType but does not specify its types\\: TData$#', 'identifier' => 'missingType.generics', @@ -6487,12 +6481,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TalkRepository.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TalkRepository\\:\\:getTalksBySpeakerWithVotes\\(\\) return type with generic interface CCMBenchmark\\\\Ting\\\\Repository\\\\CollectionInterface does not specify its types\\: T$#', - 'identifier' => 'missingType.generics', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TalkRepository.php', -]; $ignoreErrors[] = [ 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TalkRepository\\:\\:initMetadata\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#', 'identifier' => 'missingType.iterableValue', @@ -6829,60 +6817,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TicketTypeRepository.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot cast mixed to int\\.$#', - 'identifier' => 'cast.int', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/VoteRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\VoteRepository\\:\\:getNumberOfVotesByEvent\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/VoteRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\VoteRepository\\:\\:getVotesByEvent\\(\\) return type with generic interface CCMBenchmark\\\\Ting\\\\Repository\\\\CollectionInterface does not specify its types\\: T$#', - 'identifier' => 'missingType.generics', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/VoteRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\VoteRepository\\:\\:getVotesByTalkWithUser\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/VoteRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\VoteRepository\\:\\:getVotesByTalkWithUser\\(\\) has parameter \\$talkId with no type specified\\.$#', - 'identifier' => 'missingType.parameter', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/VoteRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\VoteRepository\\:\\:initMetadata\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#', - 'identifier' => 'missingType.iterableValue', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/VoteRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\VoteRepository\\:\\:initMetadata\\(\\) should return M of CCMBenchmark\\\\Ting\\\\Repository\\\\Metadata but returns CCMBenchmark\\\\Ting\\\\Repository\\\\Metadata\\\\.$#', - 'identifier' => 'return.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/VoteRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$databaseName of method CCMBenchmark\\\\Ting\\\\Repository\\\\Metadata\\\\:\\:setDatabase\\(\\) expects string, mixed given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/VoteRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$submittedOn of method AppBundle\\\\Event\\\\Model\\\\Vote\\:\\:setSubmittedOn\\(\\) expects DateTime, DateTime\\|null given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/VoteRepository.php', -]; $ignoreErrors[] = [ 'message' => '#^Property AppBundle\\\\Event\\\\Model\\\\Room\\:\\:\\$name \\(string\\) does not accept mixed\\.$#', 'identifier' => 'assign.propertyType', @@ -6955,6 +6889,12 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Talk.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Property AppBundle\\\\Event\\\\Model\\\\Talk\\:\\:\\$votes has unknown class AppBundle\\\\Event\\\\Model\\\\Vote as its type\\.$#', + 'identifier' => 'class.notFound', + 'count' => 1, + 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Talk.php', +]; $ignoreErrors[] = [ 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Ticket\\:\\:getTags\\(\\) return type has no value type specified in iterable type array\\.$#', 'identifier' => 'missingType.iterableValue', @@ -6973,12 +6913,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Event/Model/TicketType.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Vote\\:\\:getId\\(\\) should return int but returns int\\|null\\.$#', - 'identifier' => 'return.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Vote.php', -]; $ignoreErrors[] = [ 'message' => '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#', 'identifier' => 'foreach.nonIterable', @@ -8671,12 +8605,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Slack/MessageFactory.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getTitle\\(\\) on AppBundle\\\\Event\\\\Model\\\\Talk\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 2, - 'path' => __DIR__ . '/sources/AppBundle/Slack/MessageFactory.php', -]; $ignoreErrors[] = [ 'message' => '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#', 'identifier' => 'foreach.nonIterable', diff --git a/sources/AppBundle/Controller/Admin/Event/VotesListeAction.php b/sources/AppBundle/Controller/Admin/Event/VotesListeAction.php index 33bf5327a..e1ca856ae 100644 --- a/sources/AppBundle/Controller/Admin/Event/VotesListeAction.php +++ b/sources/AppBundle/Controller/Admin/Event/VotesListeAction.php @@ -5,7 +5,7 @@ namespace AppBundle\Controller\Admin\Event; use AppBundle\Event\AdminEventSelection; -use AppBundle\Event\Model\Repository\VoteRepository; +use AppBundle\Event\Entity\Repository\VoteRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; diff --git a/sources/AppBundle/Controller/Event/CFP/EditAction.php b/sources/AppBundle/Controller/Event/CFP/EditAction.php index c7cd5df2c..faafeb338 100644 --- a/sources/AppBundle/Controller/Event/CFP/EditAction.php +++ b/sources/AppBundle/Controller/Event/CFP/EditAction.php @@ -12,7 +12,7 @@ use AppBundle\Event\Model\Repository\SpeakerRepository; use AppBundle\Event\Model\Repository\TalkInvitationRepository; use AppBundle\Event\Model\Repository\TalkRepository; -use AppBundle\Event\Model\Repository\VoteRepository; +use AppBundle\Event\Entity\Repository\VoteRepository; use AppBundle\Event\Model\Talk; use AppBundle\Event\Model\TalkInvitation; use AppBundle\Event\Talk\InvitationFormHandler; @@ -98,7 +98,7 @@ public function __invoke(Request $request): RedirectResponse|Response 'invitations' => $this->talkInvitationRepository->getPendingInvitationsByTalkId($talk->getId()), 'speakers' => $this->speakerRepository->getSpeakersByTalk($talk), 'invitationForm' => $invitationForm->createView(), - 'votes' => $this->voteRepository->getVotesByTalkWithUser($talk->getId()), + 'votes' => $this->voteRepository->getVotesByTalkWithUser((int) $talk->getId()), 'sidebar' => $this->sidebarRenderer->render($event), ]); } diff --git a/sources/AppBundle/Controller/Event/CFP/IndexAction.php b/sources/AppBundle/Controller/Event/CFP/IndexAction.php index 583299ff9..8800fc851 100644 --- a/sources/AppBundle/Controller/Event/CFP/IndexAction.php +++ b/sources/AppBundle/Controller/Event/CFP/IndexAction.php @@ -8,6 +8,7 @@ use AppBundle\CFP\SpeakerFactory; use AppBundle\CFP\ViewModel\EventTalkList; use AppBundle\Controller\Event\EventActionHelper; +use AppBundle\Event\Entity\Repository\VoteRepository; use AppBundle\Event\Model\Event; use AppBundle\Event\Model\Repository\EventRepository; use AppBundle\Event\Model\Repository\TalkRepository; @@ -28,6 +29,7 @@ public function __construct( private readonly SpeakerFactory $speakerFactory, private readonly PhotoStorage $photoStorage, private readonly SidebarRenderer $sidebarRenderer, + private readonly VoteRepository $voteRepository, ) {} public function __invoke(Request $request): Response @@ -39,7 +41,7 @@ public function __invoke(Request $request): Response } $speaker = $this->speakerFactory->getSpeaker($event); $eventTalkList = new EventTalkList($event); - foreach ($this->talkRepository->getTalksBySpeakerWithVotes($event, $speaker) as $talk) { + foreach ($this->talkRepository->getTalksBySpeaker($event, $speaker) as $talk) { $eventTalkList->addTalk($talk); } /** @var EventTalkList[] $previousEventTalkLists */ @@ -64,6 +66,7 @@ public function __invoke(Request $request): Response 'speaker' => $speaker, 'speakerPhoto' => $this->photoStorage->getUrl($speaker), 'sidebar' => $this->sidebarRenderer->render($event), + 'talksVotes' => $this->voteRepository->getVotesByEventGroupedBySession((int) $event->getId()), ]); } } diff --git a/sources/AppBundle/Controller/Event/Event/ShowAction.php b/sources/AppBundle/Controller/Event/Event/ShowAction.php index 35efb44a5..7bf3aa644 100644 --- a/sources/AppBundle/Controller/Event/Event/ShowAction.php +++ b/sources/AppBundle/Controller/Event/Event/ShowAction.php @@ -6,7 +6,7 @@ use AppBundle\Controller\Event\EventActionHelper; use AppBundle\Event\Model\Repository\TalkRepository; -use AppBundle\Event\Model\Repository\VoteRepository; +use AppBundle\Event\Entity\Repository\VoteRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; diff --git a/sources/AppBundle/Controller/Event/Vote/IndexAction.php b/sources/AppBundle/Controller/Event/Vote/IndexAction.php index 63fbb0d7c..73b52be7c 100644 --- a/sources/AppBundle/Controller/Event/Vote/IndexAction.php +++ b/sources/AppBundle/Controller/Event/Vote/IndexAction.php @@ -5,9 +5,10 @@ namespace AppBundle\Controller\Event\Vote; use AppBundle\Controller\Event\EventActionHelper; +use AppBundle\Event\Entity\Repository\VoteRepository; +use AppBundle\Event\Entity\Vote; use AppBundle\Event\Model\Repository\TalkRepository; use AppBundle\Event\Model\Talk; -use AppBundle\Event\Model\Vote; use AppBundle\Security\Authentication; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; @@ -18,6 +19,7 @@ public function __construct( private readonly RequestStack $requestStack, private readonly EventActionHelper $eventActionHelper, private readonly TalkRepository $talkRepository, + private readonly VoteRepository $voteRepository, private readonly Authentication $authentication, ) {} @@ -32,18 +34,21 @@ public function __invoke(string $eventSlug, int $page = 1, bool $all = false): R } // Get a random list of unrated talks + $githubUser = $this->authentication->getGithubUser(); if ($all === false) { - $talks = $this->talkRepository->getNewTalksToRate($event, $this->authentication->getGithubUser(), crc32($this->requestStack->getSession()->getId()), $page); + $talks = $this->talkRepository->getNewTalksToRate($event, $githubUser, crc32($this->requestStack->getSession()->getId()), $page); + $myUserVotes = []; } else { - $talks = $this->talkRepository->getAllTalksAndRatingsForUser($event, $this->authentication->getGithubUser(), crc32($this->requestStack->getSession()->getId()), $page); + $talks = $this->talkRepository->getAllTalksAndRatingsForUser($event, $githubUser, crc32($this->requestStack->getSession()->getId()), $page); + $myUserVotes = $this->voteRepository->getUserVotesByEvent((int) $githubUser->getId(), (int) $event->getId()); } $vote = new Vote(); - $forms = function () use ($talks, $vote, $eventSlug) { + $forms = function () use ($talks, $vote, $myUserVotes, $eventSlug) { foreach ($talks as $session) { /** @var Talk $talk */ $talk = $session['sessions']; - $myVote = $session['asvg'] ?? clone $vote; + $myVote = $myUserVotes[(int) $talk->getId()] ?? clone $vote; /* * By using a yield here, there will be only one iteration over the talks for the entire page */ diff --git a/sources/AppBundle/Controller/Event/Vote/NewAction.php b/sources/AppBundle/Controller/Event/Vote/NewAction.php index 8e68926c8..4b6d5f2b1 100644 --- a/sources/AppBundle/Controller/Event/Vote/NewAction.php +++ b/sources/AppBundle/Controller/Event/Vote/NewAction.php @@ -5,14 +5,16 @@ namespace AppBundle\Controller\Event\Vote; use AppBundle\Controller\Event\EventActionHelper; +use AppBundle\Event\Entity\Vote; +use AppBundle\Event\Entity\Repository\VoteRepository; use AppBundle\Event\Model\GithubUser; use AppBundle\Event\Model\Repository\TalkRepository; -use AppBundle\Event\Model\Repository\VoteRepository; use AppBundle\Event\Model\Talk; -use AppBundle\Event\Model\Vote; use AppBundle\Notifier\SlackNotifier; use AppBundle\Security\Authentication; -use CCMBenchmark\Ting\Exception; +use DateTime; +use Doctrine\DBAL\Exception as DbalException; +use Doctrine\ORM\Exception\ORMException; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -40,7 +42,7 @@ public function __invoke(Request $request, string $eventSlug, int $talkId): Json $vote = new Vote(); $user = $this->authentication->getGithubUserOrNull(); if ($user instanceof GithubUser) { - $vote->setUser($user->getId()); + $vote->userId = $user->getId(); } $form = $this->createVoteForm($eventSlug, $talkId, $vote); @@ -65,15 +67,15 @@ public function __invoke(Request $request, string $eventSlug, int $talkId): Json } $vote = $form->getData(); - $vote->setSubmittedOn(new \DateTime()); + $vote->submittedOn = new DateTime(); try { - $vote->setTalk($talk); + $vote->talk = $talk; $this->eventDispatcher->addListener(KernelEvents::TERMINATE, function () use ($vote): void { $this->slackNotifier->notifyVote($vote); }); $this->voteRepository->upsert($vote); - } catch (Exception $e) { + } catch (DbalException|ORMException $e) { return new JsonResponse(['errors' => [$e->getMessage()]], Response::HTTP_INTERNAL_SERVER_ERROR); } diff --git a/sources/AppBundle/Controller/Event/Vote/VoteController.php b/sources/AppBundle/Controller/Event/Vote/VoteController.php index f128cff68..972b4e278 100644 --- a/sources/AppBundle/Controller/Event/Vote/VoteController.php +++ b/sources/AppBundle/Controller/Event/Vote/VoteController.php @@ -4,8 +4,8 @@ namespace AppBundle\Controller\Event\Vote; +use AppBundle\Event\Entity\Vote; use AppBundle\Event\Form\VoteType; -use AppBundle\Event\Model\Vote; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; @@ -14,7 +14,7 @@ abstract class VoteController extends AbstractController { protected function createVoteForm(string $eventSlug, int $talkId, Vote $vote): FormInterface { - $vote->setSessionId($talkId); + $vote->sessionId = $talkId; return $this ->createFormBuilder()->create( diff --git a/sources/AppBundle/Event/Entity/Repository/VoteRepository.php b/sources/AppBundle/Event/Entity/Repository/VoteRepository.php new file mode 100644 index 000000000..eb65e6fe8 --- /dev/null +++ b/sources/AppBundle/Event/Entity/Repository/VoteRepository.php @@ -0,0 +1,208 @@ + + */ +final class VoteRepository extends EntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Vote::class); + } + + /** + * @return array{votes: int} + */ + public function getNumberOfVotesByEvent(Event $event): array + { + $row = $this->getEntityManager()->getConnection()->fetchAssociative( + 'SELECT COUNT(vote.id) AS votes + FROM afup_sessions_vote_github AS vote + LEFT JOIN afup_sessions AS session ON session.session_id = vote.session_id + WHERE session.id_forum = :eventId', + ['eventId' => $event->getId()], + ); + + return ['votes' => (int) ($row['votes'] ?? 0)]; + } + + /** + * Votes d'un évènement avec le titre du talk et le login du votant places + * sous forme de tableaux imbriques consommables par le template Twig. + * + * @return list> + */ + public function getVotesByEvent(int $eventId): array + { + $rows = $this->getEntityManager()->getConnection()->fetchAllAssociative( + 'SELECT vote.id, vote.session_id, vote.user, vote.comment, vote.vote, + session.titre AS talk_titre, + github_user.login AS github_user_login + FROM afup_sessions_vote_github AS vote + LEFT JOIN afup_sessions AS session ON session.session_id = vote.session_id + LEFT JOIN afup_user_github AS github_user ON github_user.id = vote.user + WHERE session.id_forum = :eventId + ORDER BY vote.session_id, vote.submitted_on', + ['eventId' => $eventId], + ); + + $votes = []; + foreach ($rows as $row) { + $votes[] = [ + 'id' => (int) $row['id'], + 'sessionId' => (int) $row['session_id'], + 'userId' => (int) $row['user'], + 'comment' => $row['comment'], + 'vote' => (int) $row['vote'], + 'talk' => ['title' => $row['talk_titre']], + 'githubUser' => ['login' => $row['github_user_login']], + ]; + } + + return $votes; + } + + /** + * Votes d'une proposition avec l'information "membre du staff" du votant. + * + * @return list> + */ + public function getVotesByTalkWithUser(int $talkId): array + { + $rows = $this->getEntityManager()->getConnection()->fetchAllAssociative( + 'SELECT vote.id, vote.submitted_on, vote.comment, vote.vote, + github_user.afup_crew + FROM afup_sessions_vote_github AS vote + LEFT JOIN afup_user_github AS github_user ON github_user.id = vote.user + WHERE vote.session_id = :talkId + ORDER BY vote.submitted_on DESC', + ['talkId' => $talkId], + ); + + $votes = []; + foreach ($rows as $row) { + $votes[] = [ + 'id' => (int) $row['id'], + 'submittedOn' => new \DateTimeImmutable($row['submitted_on']), + 'comment' => $row['comment'], + 'vote' => (int) $row['vote'], + 'githubUser' => ['afupCrew' => (bool) $row['afup_crew']], + ]; + } + + return $votes; + } + + /** + * Votes d'un évènement avec l'information "membre du staff" du votant, + * groupés par identifiant de proposition, identiques à la sortie de + * getVotesByTalkWithUser() mais en une seule requête. + * + * @return array>> + */ + public function getVotesByEventGroupedBySession(int $eventId): array + { + $rows = $this->getEntityManager()->getConnection()->fetchAllAssociative( + 'SELECT vote.id, vote.session_id, vote.submitted_on, vote.comment, vote.vote, + github_user.afup_crew + FROM afup_sessions_vote_github AS vote + LEFT JOIN afup_sessions AS session ON session.session_id = vote.session_id + LEFT JOIN afup_user_github AS github_user ON github_user.id = vote.user + WHERE session.id_forum = :eventId + ORDER BY vote.session_id, vote.submitted_on DESC', + ['eventId' => $eventId], + ); + + $votes = []; + foreach ($rows as $row) { + $id = $row['id']; + $sessionId = $row['session_id']; + $submittedOn = $row['submitted_on']; + $comment = $row['comment']; + $voteNote = $row['vote']; + $afupCrew = $row['afup_crew']; + + if (!is_int($id) || !is_int($sessionId) || !is_string($submittedOn) || !is_int($voteNote) + || ($comment !== null && !is_string($comment)) + || ($afupCrew !== null && !is_bool($afupCrew) && !is_int($afupCrew))) { + throw new \RuntimeException('Données inattendues dans la table afup_sessions_vote_github.'); + } + + $votes[$sessionId][] = [ + 'id' => $id, + 'submittedOn' => new \DateTimeImmutable($submittedOn), + 'comment' => $comment, + 'vote' => $voteNote, + 'githubUser' => ['afupCrew' => (bool) ($afupCrew ?? false)], + ]; + } + + return $votes; + } + + /** + * Votes déposés par un utilisateur sur tout un évènement, indexés par + * l'identifiant de la proposition. + * + * @return array + */ + public function getUserVotesByEvent(int $userId, int $eventId): array + { + $rows = $this->getEntityManager()->getConnection()->fetchAllAssociative( + 'SELECT vote.* + FROM afup_sessions_vote_github AS vote + JOIN afup_sessions AS session ON session.session_id = vote.session_id + WHERE vote.user = :userId AND session.id_forum = :eventId', + ['userId' => $userId, 'eventId' => $eventId], + ); + + $votes = []; + foreach ($rows as $row) { + $id = $row['id']; + $sessionId = $row['session_id']; + $rawUser = $row['user']; + $comment = $row['comment']; + $voteNote = $row['vote']; + $submittedOn = $row['submitted_on']; + + if (!is_int($id) || !is_int($sessionId) || !is_int($rawUser) || !is_int($voteNote) + || ($comment !== null && !is_string($comment)) + || ($submittedOn !== null && !is_string($submittedOn))) { + throw new \RuntimeException('Données inattendues dans la table afup_sessions_vote_github.'); + } + + $vote = new Vote(); + $vote->id = $id; + $vote->sessionId = $sessionId; + $vote->userId = $rawUser; + $vote->comment = $comment; + $vote->vote = $voteNote; + $vote->submittedOn = $submittedOn !== null ? new \DateTime($submittedOn) : null; + $votes[$sessionId] = $vote; + } + + return $votes; + } + + public function upsert(Vote $vote): void + { + /** @var Vote|null $previousVote */ + $previousVote = $this->findOneBy(['userId' => $vote->userId, 'sessionId' => $vote->sessionId]); + if ($previousVote !== null) { + $previousVote->comment = $vote->comment; + $previousVote->submittedOn = $vote->submittedOn; + $previousVote->vote = $vote->vote; + $vote = $previousVote; + } + $this->save($vote); + } +} diff --git a/sources/AppBundle/Event/Entity/Vote.php b/sources/AppBundle/Event/Entity/Vote.php new file mode 100644 index 000000000..4ff312930 --- /dev/null +++ b/sources/AppBundle/Event/Entity/Vote.php @@ -0,0 +1,44 @@ +query($this->getCollection(new HydratorSingleObject())); } - /** - * @return CollectionInterface&iterable - */ - public function getTalksBySpeakerWithVotes(Event $event, Speaker $speaker) - { - $query = $this->getPreparedQuery( - 'SELECT sessions.session_id, titre, abstract, id_forum, sessions.plannifie, skill, sessions.genre, votes.* - FROM afup_sessions sessions - LEFT JOIN afup_conferenciers_sessions cs ON cs.session_id = sessions.session_id - LEFT JOIN afup_sessions_vote_github votes ON votes.session_id = sessions.session_id - LEFT JOIN afup_user_github aug ON aug.id = votes.user - INNER JOIN ( - SELECT afup_conferenciers_sessions.session_id - FROM afup_conferenciers_sessions - INNER JOIN afup_sessions ON afup_conferenciers_sessions.session_id = afup_sessions.session_id - WHERE afup_sessions.id_forum = :event - AND afup_conferenciers_sessions.conferencier_id = :speaker - LIMIT 0, 20 - ) as s2 ON s2.session_id = sessions.session_id - WHERE cs.conferencier_id = :speaker - ORDER BY titre - ', - )->setParams(['event' => $event->getId(), 'speaker' => $speaker->getId()]); - $hydrator = new HydratorRelational(); - $hydrator->addRelation(new RelationMany(new AggregateFrom('github_user'), new AggregateTo('votes'), 'setGithubUser')); - $hydrator->addRelation(new RelationMany(new AggregateFrom('votes'), new AggregateTo('sessions'), 'setVotes')); - $hydrator->callableFinalizeAggregate(fn(array $row) => $row['sessions']); - return $query->query($this->getCollection($hydrator)); - } - /** * @return CollectionInterface */ @@ -142,9 +108,8 @@ public function getPreviousTalksBySpeaker(Event $event, Speaker $speaker) public function getAllTalksAndRatingsForUser(Event $event, GithubUser $user, $randomSeed, $page = 1, $limit = 10) { $query = $this->getPreparedQuery( - 'SELECT sessions.session_id, titre, abstract, skill, sessions.genre, id_forum, asvg.id, asvg.comment, asvg.vote + 'SELECT sessions.session_id, titre, abstract, skill, sessions.genre, id_forum FROM afup_sessions sessions - LEFT JOIN afup_sessions_vote_github asvg ON (asvg.session_id = sessions.session_id AND asvg.user = :user) WHERE plannifie = 0 AND id_forum = :event ORDER BY RAND(:randomSeed) LIMIT ' . ((int) $page - 1) * $limit . ', ' . ((int) $limit + 1), diff --git a/sources/AppBundle/Event/Model/Repository/VoteRepository.php b/sources/AppBundle/Event/Model/Repository/VoteRepository.php deleted file mode 100644 index 64fb91cba..000000000 --- a/sources/AppBundle/Event/Model/Repository/VoteRepository.php +++ /dev/null @@ -1,143 +0,0 @@ - - */ -class VoteRepository extends Repository implements MetadataInitializer -{ - public function getNumberOfVotesByEvent(Event $event) - { - $query = $this->getQuery('SELECT COUNT(id) AS votes -FROM afup_sessions_vote_github asvg -LEFT JOIN afup_sessions s ON s.session_id = asvg.session_id -WHERE s.id_forum = :event'); - $query->setParams(['event' => $event->getId()]); - return $query->query($this->getCollection(new HydratorArray()))->first(); - } - - /** - * @param int $eventId - * @return CollectionInterface - */ - public function getVotesByEvent($eventId) - { - $query = $this - ->getPreparedQuery(' - SELECT asvg.id, asvg.session_id, submitted_on, asvg.comment, asvg.user, vote, - sessions.titre, sessions.abstract, aug.login, aug.avatar_url - FROM afup_sessions_vote_github asvg - LEFT JOIN afup_sessions sessions ON sessions.session_id = asvg.session_id - LEFT JOIN afup_user_github aug ON aug.id = asvg.user - WHERE sessions.id_forum = :eventId - ORDER BY asvg.session_id, asvg.submitted_on - '); - $query->setParams(['eventId' => (int) $eventId]); - - $hydrator = new HydratorSingleObject(); - $hydrator - ->mapObjectTo('sessions', 'asvg', 'setTalk') - ->mapObjectTo('aug', 'asvg', 'setGithubUser') - ; - return $query->query($this->getCollection($hydrator)); - } - - public function getVotesByTalkWithUser($talkId) - { - $query = $this->getPreparedQuery(' - SELECT asvg.id, asvg.session_id, asvg.submitted_on, asvg.comment, asvg.user, asvg.vote, aug.afup_crew - FROM afup_sessions_vote_github asvg - LEFT JOIN afup_user_github aug ON aug.id = asvg.user - WHERE asvg.session_id = :talkId - ORDER BY asvg.submitted_on DESC - '); - - $query->setParams(['talkId' => (int) $talkId]); - - $hydrator = new HydratorSingleObject(); - $hydrator - ->mapObjectTo('aug', 'asvg', 'setGithubUser') - ; - return $query->query($this->getCollection($hydrator)); - } - - /** - * @inheritDoc - */ - public static function initMetadata(SerializerFactoryInterface $serializerFactory, array $options = []) - { - $metadata = new Metadata($serializerFactory); - - $metadata->setEntity(Vote::class); - $metadata->setConnectionName('main'); - $metadata->setDatabase($options['database']); - $metadata->setTable('afup_sessions_vote_github'); - - $metadata - ->addField([ - 'columnName' => 'id', - 'fieldName' => 'id', - 'primary' => true, - 'autoincrement' => true, - 'type' => 'int', - ]) - ->addField([ - 'columnName' => 'session_id', - 'fieldName' => 'sessionId', - 'type' => 'int', - ]) - ->addField([ - 'columnName' => 'submitted_on', - 'fieldName' => 'submittedOn', - 'type' => 'datetime', - ]) - ->addField([ - 'columnName' => 'comment', - 'fieldName' => 'comment', - 'type' => 'string', - ]) - ->addField([ - 'columnName' => 'user', - 'fieldName' => 'user', - 'type' => 'int', - ]) - ->addField([ - 'columnName' => 'vote', - 'fieldName' => 'vote', - 'type' => 'int', - ]) - ; - - return $metadata; - } - - public function upsert(Vote $vote): void - { - /** - * @var Vote|null $previousVote - */ - $previousVote = $this->getOneBy(['user' => $vote->getUser(), 'sessionId' => $vote->getSessionId()]); - if ($previousVote !== null) { - $previousVote - ->setComment($vote->getComment()) - ->setSubmittedOn($vote->getSubmittedOn()) - ->setVote($vote->getVote()) - ; - $vote = $previousVote; - } - $this->save($vote); - } -} diff --git a/sources/AppBundle/Event/Model/Vote.php b/sources/AppBundle/Event/Model/Vote.php deleted file mode 100644 index bc75adcaa..000000000 --- a/sources/AppBundle/Event/Model/Vote.php +++ /dev/null @@ -1,130 +0,0 @@ -id; - } - - public function setId(int $id): self - { - $this->propertyChanged('id', $this->id, $id); - $this->id = $id; - return $this; - } - - public function getSessionId(): int - { - return $this->sessionId; - } - - public function setSessionId(int $sessionId): self - { - $this->propertyChanged('sessionId', $this->sessionId, $sessionId); - $this->sessionId = $sessionId; - return $this; - } - - public function getUser(): int - { - return $this->user; - } - - public function setUser(int $user): self - { - $this->propertyChanged('user', $this->user, $user); - $this->user = $user; - return $this; - } - - public function getComment(): ?string - { - return $this->comment; - } - - public function setComment(?string $comment): self - { - $this->propertyChanged('comment', $this->comment, $comment); - $this->comment = $comment; - return $this; - } - - public function getVote(): int - { - return $this->vote; - } - - public function setVote(int $vote): self - { - $this->propertyChanged('vote', $this->vote, $vote); - $this->vote = $vote; - return $this; - } - - public function getSubmittedOn(): ?\DateTime - { - return $this->submittedOn; - } - - public function setSubmittedOn(\DateTime $submittedOn): self - { - $this->propertyChanged('submittedOn', $this->submittedOn, $submittedOn); - $this->submittedOn = $submittedOn; - return $this; - } - - public function getGithubUser(): ?GithubUser - { - return $this->githubUser; - } - - public function setGithubUser(GithubUser $githubUser): self - { - $this->githubUser = $githubUser; - return $this; - } - - public function getTalk(): ?Talk - { - return $this->talk; - } - - public function setTalk(Talk $talk): self - { - $this->talk = $talk; - return $this; - } -} diff --git a/sources/AppBundle/Notifier/SlackNotifier.php b/sources/AppBundle/Notifier/SlackNotifier.php index 838495285..f516e2b11 100644 --- a/sources/AppBundle/Notifier/SlackNotifier.php +++ b/sources/AppBundle/Notifier/SlackNotifier.php @@ -4,9 +4,9 @@ namespace AppBundle\Notifier; +use AppBundle\Event\Entity\Vote; use AppBundle\Event\Model\Event; use AppBundle\Event\Model\Talk; -use AppBundle\Event\Model\Vote; use AppBundle\Slack\Message; use AppBundle\Slack\MessageFactory; use JMS\Serializer\SerializerInterface; diff --git a/sources/AppBundle/Slack/MessageFactory.php b/sources/AppBundle/Slack/MessageFactory.php index 531221123..94a1f8d40 100644 --- a/sources/AppBundle/Slack/MessageFactory.php +++ b/sources/AppBundle/Slack/MessageFactory.php @@ -6,13 +6,13 @@ use AppBundle\Association\Model\Repository\UserRepository; use AppBundle\Event\Entity\Repository\TalkToSpeakerRepository; +use AppBundle\Event\Entity\Vote; use AppBundle\Event\Model\Event; use AppBundle\Event\Model\EventStats\SalesPilotage; use AppBundle\Event\Model\Repository\EventStatsRepository; use AppBundle\Event\Model\Repository\TalkRepository; use AppBundle\Event\Model\Repository\TicketTypeRepository; use AppBundle\Event\Model\Talk; -use AppBundle\Event\Model\Vote; use AppBundle\AssembleeGenerale\Entity\Repository\PresenceRepository; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Contracts\Translation\TranslatorInterface; @@ -36,9 +36,9 @@ public function createMessageForVote(Vote $vote): Message ->setTitleLink('https://afup.org/pages/administration/index.php?page=forum_vote_github') ->setFallback(sprintf( 'Nouveau vote sur la conférence "%s". Note: %s. Commentaire: %s', - $vote->getTalk()->getTitle(), - $vote->getVote(), - $vote->getComment(), + $vote->talk?->getTitle() ?? '', + $vote->vote, + $vote->comment, ), ) ->setColor('good') @@ -47,18 +47,18 @@ public function createMessageForVote(Vote $vote): Message $attachment ->addField( - new Field()->setShort(false)->setTitle('Talk')->setValue($vote->getTalk()->getTitle()), + new Field()->setShort(false)->setTitle('Talk')->setValue($vote->talk?->getTitle() ?? ''), ) ->addField( new Field()->setShort(false)->setTitle('Nouveau vote')->setValue( - str_repeat(':star:', $vote->getVote()), + str_repeat(':star:', $vote->vote), ), ) ; - if ($vote->getComment() !== null) { + if ($vote->comment !== null) { $attachment ->addField( - new Field()->setShort(false)->setTitle('Commentaire')->setValue($vote->getComment()), + new Field()->setShort(false)->setTitle('Commentaire')->setValue($vote->comment), ) ; } diff --git a/templates/event/cfp/home.html.twig b/templates/event/cfp/home.html.twig index 2c2ad8fb6..7e37c3256 100644 --- a/templates/event/cfp/home.html.twig +++ b/templates/event/cfp/home.html.twig @@ -41,7 +41,7 @@ {% endif %} - {{ include('event/cfp/list_votes.html.twig', {votes: talk.votes, cfp_open: event.cfpOpen}, with_context: false) }} + {{ include('event/cfp/list_votes.html.twig', {votes: talksVotes[talk.id] ?? [], cfp_open: event.cfpOpen}, with_context: false) }} {% endfor %} {% else %}

{{ 'Aucune proposition enregistrée pour le moment.'|trans }}