From 52ce4a87157b7dd09f547905796620d52b447e81 Mon Sep 17 00:00:00 2001 From: Dimitri Date: Fri, 18 Oct 2024 17:53:26 +0200 Subject: [PATCH] [VotingPlatform] add announcement email notification (#10890) --- features/api/adherents.feature | 2 +- .../Designation/DesignationAdmin.php | 11 +- .../VotingPlatform/ConfigureCommand.php | 2 +- src/Command/VotingPlatform/NotifyCommand.php | 26 ++- .../Designation/Designation.php | 31 +++- .../Alert/Provider/ElectionAlertProvider.php | 2 +- .../AbstractAnnouncementMessage.php | 47 ++++++ .../ConsultationAnnouncementMessage.php | 7 + .../VoteAnnouncementMessage.php | 7 + src/Normalizer/DesignationDenormalizer.php | 10 +- src/Repository/AdherentRepository.php | 52 +++++- .../VotingPlatform/ElectionRepository.php | 20 +++ src/Twig/VotingPlatformExtension.php | 1 + src/Twig/VotingPlatformRuntime.php | 1 + .../Designation/DesignationTypeEnum.php | 4 +- .../Notifier/ElectionNotifier.php | 159 ++++++++++-------- .../candidate_list_binomes_column.html.twig | 2 +- .../election_list_pools_column.html.twig | 6 - .../vote_list_action_column.html.twig | 2 +- .../_layout_consultation.html.twig | 24 +-- .../voting_platform/_layout_vote.html.twig | 116 +++++++++++++ .../_confirmation_block.html.twig | 2 +- templates/voting_platform/index.html.twig | 6 +- templates/voting_platform/vote.html.twig | 2 +- translations/messages+intl-icu.fr.yml | 1 + 25 files changed, 402 insertions(+), 141 deletions(-) create mode 100644 src/Mailer/Message/Renaissance/VotingPlatform/AbstractAnnouncementMessage.php create mode 100644 src/Mailer/Message/Renaissance/VotingPlatform/ConsultationAnnouncementMessage.php create mode 100644 src/Mailer/Message/Renaissance/VotingPlatform/VoteAnnouncementMessage.php create mode 100644 templates/voting_platform/_layout_vote.html.twig diff --git a/features/api/adherents.feature b/features/api/adherents.feature index 5479451b1d3..d9ffc81bdef 100644 --- a/features/api/adherents.feature +++ b/features/api/adherents.feature @@ -1263,7 +1263,7 @@ Feature: """ { "adherent": 6, - "sympathizer": 1 + "sympathizer": 2 } """ When I send a "POST" request to "/api/v3/adherents/count?scope=president_departmental_assembly" with body: diff --git a/src/Admin/VotingPlatform/Designation/DesignationAdmin.php b/src/Admin/VotingPlatform/Designation/DesignationAdmin.php index 8343ed696d5..c8b6474ebb2 100644 --- a/src/Admin/VotingPlatform/Designation/DesignationAdmin.php +++ b/src/Admin/VotingPlatform/Designation/DesignationAdmin.php @@ -2,7 +2,6 @@ namespace App\Admin\VotingPlatform\Designation; -use App\Adherent\Tag\TagEnum; use App\Admin\AbstractAdmin; use App\Entity\Geo\Zone; use App\Entity\VotingPlatform\Designation\CandidacyPool\CandidacyPool; @@ -86,10 +85,10 @@ protected function configureFormFields(FormMapper $form): void 'help' => 'Obligatoire pour les élections départementales', 'btn_add' => false, ]) - ->add('target', ChoiceType::class, [ - 'label' => 'Personnes concernées', - 'choices' => array_combine($tags = TagEnum::getAdherentTags(true), $tags), - 'multiple' => true, + ->add('targetYear', ChoiceType::class, [ + 'required' => false, + 'label' => 'Collège électoral : Adhérent à jour à partir de :', + 'choices' => array_combine($years = range(2022, date('Y')), $years), ]) ->end() ->with('Candidature 🎎', ['class' => 'col-md-6', 'box_class' => 'box box-solid box-default']) @@ -139,7 +138,7 @@ protected function configureFormFields(FormMapper $form): void ->end() ->tab('Notifications 📯') ->with('Envoi d\'email') - ->add('notifications', DesignationNotificationType::class, ['required' => false]) + ->add('notifications', DesignationNotificationType::class, ['label' => false, 'required' => false]) ->end() ->end() ->tab('Questionnaire ❓') diff --git a/src/Command/VotingPlatform/ConfigureCommand.php b/src/Command/VotingPlatform/ConfigureCommand.php index 8d07071d047..3f6dd9c6bfe 100644 --- a/src/Command/VotingPlatform/ConfigureCommand.php +++ b/src/Command/VotingPlatform/ConfigureCommand.php @@ -91,7 +91,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->configureLocalElection($designation); } elseif ($designation->isLocalPollType()) { $this->configureLocalPoll($designation); - } elseif ($designation->isConsultationType()) { + } elseif ($designation->isConsultationType() || $designation->isVoteType()) { $this->configureConsultation($designation); } elseif ($designation->isTerritorialAssemblyType()) { $this->configureTerritorialAssembly($designation); diff --git a/src/Command/VotingPlatform/NotifyCommand.php b/src/Command/VotingPlatform/NotifyCommand.php index cab357b87ae..a5ef5faa200 100644 --- a/src/Command/VotingPlatform/NotifyCommand.php +++ b/src/Command/VotingPlatform/NotifyCommand.php @@ -15,7 +15,6 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; #[AsCommand( @@ -24,8 +23,6 @@ )] class NotifyCommand extends Command { - private SymfonyStyle $io; - public function __construct( private readonly EntityManagerInterface $entityManager, private readonly DesignationRepository $designationRepository, @@ -38,16 +35,12 @@ public function __construct( parent::__construct(); } - protected function initialize(InputInterface $input, OutputInterface $output): void - { - $this->io = new SymfonyStyle($input, $output); - } - protected function execute(InputInterface $input, OutputInterface $output): int { $date = new \DateTime(); $this->notifyForEndForCandidacy($date); + $this->notifyBeforeVote($date); $this->notifyWithReminderToVote($date, Designation::NOTIFICATION_VOTE_REMINDER_1D); $this->notifyWithReminderToVote($date, Designation::NOTIFICATION_VOTE_REMINDER_1H); $this->notifyForForElectionResults($date); @@ -59,15 +52,11 @@ private function notifyForEndForCandidacy(\DateTimeInterface $date): void { $designations = $this->designationRepository->getWithFinishCandidacyPeriod($date, [DesignationTypeEnum::COMMITTEE_ADHERENT]); - $this->io->progressStart(); - foreach ($designations as $designation) { if (DesignationTypeEnum::COMMITTEE_ADHERENT === $designation->getType()) { $this->notifyCommitteeElections($designation); } } - - $this->io->progressFinish(); } public function notifyCommitteeElections(Designation $designation): void @@ -87,8 +76,6 @@ public function notifyCommitteeElections(Designation $designation): void $committeeElection->setAdherentNotified(true); $this->entityManager->flush(); - - $this->io->progressAdvance(); } $this->entityManager->clear(); @@ -100,7 +87,16 @@ private function notifyWithReminderToVote(\DateTimeInterface $date, int $notific $elections = $this->electionRepository->getElectionsToClose($date, $notification); foreach ($elections as $election) { - $this->electionNotifier->notifyVotingPlatformVoteReminder($election, $notification); + $this->electionNotifier->notifyVoteReminder($election, $notification); + } + } + + private function notifyBeforeVote(\DateTimeInterface $date): void + { + $elections = $this->electionRepository->findIncomingElections($date); + + foreach ($elections as $election) { + $this->electionNotifier->notifyVoteAnnouncement($election); } } diff --git a/src/Entity/VotingPlatform/Designation/Designation.php b/src/Entity/VotingPlatform/Designation/Designation.php index 707fb3d3cf2..aa6b1f8744e 100644 --- a/src/Entity/VotingPlatform/Designation/Designation.php +++ b/src/Entity/VotingPlatform/Designation/Designation.php @@ -86,8 +86,10 @@ class Designation implements EntityAdministratorBlameableInterface, EntityAdhere 'Ouverture du vote' => self::NOTIFICATION_VOTE_OPENED, 'Fermeture du vote' => self::NOTIFICATION_VOTE_CLOSED, 'Résultats disponible' => self::NOTIFICATION_RESULT_READY, - 'Rappel de vote' => self::NOTIFICATION_VOTE_REMINDER_1D, + 'Rappel de vote J-1' => self::NOTIFICATION_VOTE_REMINDER_1D, + 'Rappel de vote H-1' => self::NOTIFICATION_VOTE_REMINDER_1H, 'Ouverture du tour bis' => self::NOTIFICATION_SECOND_ROUND, + 'Annonce du vote J-2' => self::NOTIFICATION_VOTE_ANNOUNCEMENT, ]; public const NOTIFICATION_VOTE_OPENED = 1; @@ -96,6 +98,7 @@ class Designation implements EntityAdministratorBlameableInterface, EntityAdhere public const NOTIFICATION_SECOND_ROUND = 8; public const NOTIFICATION_RESULT_READY = 16; public const NOTIFICATION_VOTE_REMINDER_1H = 32; + public const NOTIFICATION_VOTE_ANNOUNCEMENT = 64; /** * @var string|null @@ -223,7 +226,12 @@ class Designation implements EntityAdministratorBlameableInterface, EntityAdhere private ?string $description = null; #[ORM\Column(type: 'integer', nullable: true)] - private $notifications = self::NOTIFICATION_VOTE_OPENED + self::NOTIFICATION_RESULT_READY + self::NOTIFICATION_VOTE_REMINDER_1D + self::NOTIFICATION_VOTE_REMINDER_1H; + private ?int $notifications = + self::NOTIFICATION_VOTE_OPENED + + self::NOTIFICATION_RESULT_READY + + self::NOTIFICATION_VOTE_REMINDER_1D + + self::NOTIFICATION_VOTE_REMINDER_1H + + self::NOTIFICATION_VOTE_ANNOUNCEMENT; #[ORM\Column(type: 'boolean')] private bool $isBlankVoteEnabled = true; @@ -487,6 +495,7 @@ public function hasValidZone(): bool DesignationTypeEnum::POLL, DesignationTypeEnum::LOCAL_POLL, DesignationTypeEnum::CONSULTATION, + DesignationTypeEnum::VOTE, DesignationTypeEnum::TERRITORIAL_ASSEMBLY, ], true)) { return true; @@ -595,6 +604,7 @@ public function isLocalElectionTypes(): bool DesignationTypeEnum::LOCAL_POLL, DesignationTypeEnum::LOCAL_ELECTION, DesignationTypeEnum::CONSULTATION, + DesignationTypeEnum::VOTE, ]); } @@ -608,6 +618,11 @@ public function isConsultationType(): bool return DesignationTypeEnum::CONSULTATION === $this->type; } + public function isVoteType(): bool + { + return DesignationTypeEnum::VOTE === $this->type; + } + public function isTerritorialAssemblyType(): bool { return DesignationTypeEnum::TERRITORIAL_ASSEMBLY === $this->type; @@ -840,6 +855,18 @@ public function isLimitedResultsView(): bool DesignationTypeEnum::TERRITORIAL_ASSEMBLY, DesignationTypeEnum::COMMITTEE_SUPERVISOR, DesignationTypeEnum::CONSULTATION, + DesignationTypeEnum::VOTE, ], true); } + + public function initCreationDate(): void + { + if ($this->getVoteStartDate()) { + $electionCreationDate = (clone $this->getVoteStartDate())->modify(\sprintf('-%d days', $this->isCommitteeSupervisorType() ? 15 : 2)); + + if ($this->electionCreationDate !== $electionCreationDate) { + $this->electionCreationDate = $electionCreationDate; + } + } + } } diff --git a/src/JeMengage/Alert/Provider/ElectionAlertProvider.php b/src/JeMengage/Alert/Provider/ElectionAlertProvider.php index 6b3664b0e2f..ce374dda152 100644 --- a/src/JeMengage/Alert/Provider/ElectionAlertProvider.php +++ b/src/JeMengage/Alert/Provider/ElectionAlertProvider.php @@ -20,7 +20,7 @@ public function getAlert(Adherent $adherent): ?Alert { $designations = $this->electionManager->findActiveDesignations( $adherent, - [DesignationTypeEnum::LOCAL_POLL, DesignationTypeEnum::CONSULTATION], + [DesignationTypeEnum::LOCAL_POLL, DesignationTypeEnum::CONSULTATION, DesignationTypeEnum::VOTE], ); if (!$designations) { diff --git a/src/Mailer/Message/Renaissance/VotingPlatform/AbstractAnnouncementMessage.php b/src/Mailer/Message/Renaissance/VotingPlatform/AbstractAnnouncementMessage.php new file mode 100644 index 00000000000..dfaa1607637 --- /dev/null +++ b/src/Mailer/Message/Renaissance/VotingPlatform/AbstractAnnouncementMessage.php @@ -0,0 +1,47 @@ +getEmailAddress(), + $first->getFullName(), + $election->getTitle(), + [ + 'vote_title' => $election->getTitle(), + 'vote_start_date' => static::formatDate($election->getVoteStartDate(), 'd MMMM y'), + 'vote_start_hour' => static::formatDate($election->getVoteStartDate(), 'HH\'h\'mm'), + 'vote_end_date' => static::formatDate($election->getVoteEndDate(), 'd MMMM y'), + 'vote_end_hour' => static::formatDate($election->getVoteEndDate(), 'HH\'h\'mm'), + 'year' => $election->getDesignation()->targetYear, + 'description' => nl2br($election->getDesignation()->getDescription() ?? ''), + 'primary_link' => $url, + ], + [ + 'first_name' => $first->getFirstName(), + ] + ); + + foreach ($adherents as $adherent) { + $message->addRecipient($adherent->getEmailAddress(), $adherent->getFullName(), [ + 'first_name' => $adherent->getFirstName(), + ]); + } + + return $message; + } +} diff --git a/src/Mailer/Message/Renaissance/VotingPlatform/ConsultationAnnouncementMessage.php b/src/Mailer/Message/Renaissance/VotingPlatform/ConsultationAnnouncementMessage.php new file mode 100644 index 00000000000..b361fab0bd0 --- /dev/null +++ b/src/Mailer/Message/Renaissance/VotingPlatform/ConsultationAnnouncementMessage.php @@ -0,0 +1,7 @@ +getCandidacyEndDate() !== $voteDate = $designation->getVoteStartDate()) { $designation->setCandidacyEndDate($voteDate); } - - if ($designation->getVoteStartDate()) { - $electionCreationDate = (clone $designation->getVoteStartDate())->modify('-15 days')->setTime(0, 0, 0); - - if ($designation->electionCreationDate !== $electionCreationDate) { - $designation->electionCreationDate = $electionCreationDate; - } - } } elseif ($designation->isConsultationType()) { $designation->alertTitle = $designation->alertTitle ?: $designation->getTitle(); $designation->alertCtaLabel = $designation->alertCtaLabel ?: 'Voir'; $designation->alertDescription = $designation->alertDescription ?: (new UnicodeString($designation->getDescription() ?? ''))->truncate(200, '…', false); } + + $designation->initCreationDate(); } return $designation; diff --git a/src/Repository/AdherentRepository.php b/src/Repository/AdherentRepository.php index 3e218f724d7..77631a01852 100644 --- a/src/Repository/AdherentRepository.php +++ b/src/Repository/AdherentRepository.php @@ -26,6 +26,10 @@ use App\Entity\Phoning\Campaign; use App\Entity\Phoning\CampaignHistory; use App\Entity\VotingPlatform\Designation\Designation; +use App\Entity\VotingPlatform\Election; +use App\Entity\VotingPlatform\ElectionRound; +use App\Entity\VotingPlatform\Vote; +use App\Entity\VotingPlatform\Voter; use App\Entity\VotingPlatform\VotersList; use App\Membership\MembershipSourceEnum; use App\Pap\CampaignHistoryStatusEnum as PapCampaignHistoryStatusEnum; @@ -937,15 +941,57 @@ public function countInZones(array $zones, bool $adherentRenaissance, bool $symp ; } - public function getAllInZones(array $zones, bool $adherentRenaissance, bool $sympathizerRenaissance): array + public function getAllInZones(array $zones, bool $adherentRenaissance, bool $sympathizerRenaissance, ?int $offset = null, ?int $limit = null): array { if (!$zones) { return []; } - return $this + $qb = $this ->createQueryBuilderForZones($zones, $adherentRenaissance, $sympathizerRenaissance) ->select('PARTIAL adherent.{id, uuid, emailAddress, source, firstName, lastName, lastMembershipDonation}') + ; + + if (null !== $offset && null !== $limit) { + $qb + ->setMaxResults($limit) + ->setFirstResult($offset) + ; + } + + return $qb + ->getQuery() + ->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true) + ->getResult() + ; + } + + public function getAllInZonesAndNotVoted(Election $election, array $zones, ?int $offset = null, ?int $limit = null): array + { + if (!$zones) { + return []; + } + + $qb = $this + ->createQueryBuilderForZones($zones, true, false) + ->select('PARTIAL adherent.{id, uuid, emailAddress, source, firstName, lastName, lastMembershipDonation}') + ->leftJoin(Voter::class, 'voter', Join::WITH, 'voter.adherent = adherent') + ->leftJoin('voter.votersLists', 'voters_lists', Join::WITH, 'voters_lists.election = :election') + ->leftJoin(ElectionRound::class, 'election_round', Join::WITH, 'election_round.election = :election') + ->leftJoin(Vote::class, 'vote', Join::WITH, 'vote.voter = voter AND vote.electionRound = election_round') + ->andWhere('vote.id IS NULL') + ->groupBy('adherent.id') + ->setParameter('election', $election) + ; + + if (null !== $offset && null !== $limit) { + $qb + ->setMaxResults($limit) + ->setFirstResult($offset) + ; + } + + return $qb ->getQuery() ->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true) ->getResult() @@ -977,10 +1023,8 @@ private function createQueryBuilderForZones( bool $sympathizerRenaissance, ): QueryBuilder { $qb = $this->createQueryBuilder('adherent') - ->where('adherent.source = :source') ->andWhere('adherent.status = :status') ->setParameters([ - 'source' => MembershipSourceEnum::RENAISSANCE, 'status' => Adherent::ENABLED, ]) ; diff --git a/src/Repository/VotingPlatform/ElectionRepository.php b/src/Repository/VotingPlatform/ElectionRepository.php index 6d9122384d2..4bb6ec2f809 100644 --- a/src/Repository/VotingPlatform/ElectionRepository.php +++ b/src/Repository/VotingPlatform/ElectionRepository.php @@ -205,4 +205,24 @@ public function getElectionsToCloseOrWithoutResults(\DateTime $date, ?int $limit return $qb->getResult(); } + + public function findIncomingElections(\DateTimeInterface $date) + { + return $this->createQueryBuilder('election') + ->addSelect('designation') + ->innerJoin('election.designation', 'designation') + ->where('designation.voteStartDate BETWEEN :start_date AND :end_date') + ->andWhere('election.status = :open') + ->andWhere('designation.isCanceled = false') + ->andWhere('BIT_AND(designation.notifications, :notification) > 0 AND BIT_AND(election.notificationsSent, :notification) = 0') + ->setParameters([ + 'start_date' => $date, + 'end_date' => (clone $date)->modify('+2 days'), + 'open' => ElectionStatusEnum::OPEN, + 'notification' => Designation::NOTIFICATION_VOTE_ANNOUNCEMENT, + ]) + ->getQuery() + ->getResult() + ; + } } diff --git a/src/Twig/VotingPlatformExtension.php b/src/Twig/VotingPlatformExtension.php index 9821e1addf7..42243c6276e 100644 --- a/src/Twig/VotingPlatformExtension.php +++ b/src/Twig/VotingPlatformExtension.php @@ -36,6 +36,7 @@ public function getElectionPoolTitleKey(ElectionPool $pool): string break; case DesignationTypeEnum::LOCAL_POLL: case DesignationTypeEnum::CONSULTATION: + case DesignationTypeEnum::VOTE: case DesignationTypeEnum::TERRITORIAL_ASSEMBLY: $key = ''; break; diff --git a/src/Twig/VotingPlatformRuntime.php b/src/Twig/VotingPlatformRuntime.php index 4bc29c34439..3fc7bd8b04a 100644 --- a/src/Twig/VotingPlatformRuntime.php +++ b/src/Twig/VotingPlatformRuntime.php @@ -39,6 +39,7 @@ public function findActiveDesignations(Adherent $adherent, ?array $types = null, DesignationTypeEnum::POLL, DesignationTypeEnum::COMMITTEE_SUPERVISOR, DesignationTypeEnum::CONSULTATION, + DesignationTypeEnum::VOTE, ]; if (\count($adherent->findActifLocalMandates())) { diff --git a/src/VotingPlatform/Designation/DesignationTypeEnum.php b/src/VotingPlatform/Designation/DesignationTypeEnum.php index 0be81cadd68..c4c49e6896c 100644 --- a/src/VotingPlatform/Designation/DesignationTypeEnum.php +++ b/src/VotingPlatform/Designation/DesignationTypeEnum.php @@ -21,9 +21,10 @@ final class DesignationTypeEnum extends Enum public const POLL = 'poll'; public const LOCAL_POLL = 'local_poll'; public const LOCAL_ELECTION = 'local_election'; + public const TERRITORIAL_ASSEMBLY = 'territorial_assembly'; public const CONSULTATION = 'consultation'; - public const TERRITORIAL_ASSEMBLY = 'territorial_assembly'; + public const VOTE = 'vote'; public const TITLES = [ self::COMMITTEE_SUPERVISOR => 'Élection du binôme paritaire d’Animateurs locaux', @@ -64,5 +65,6 @@ final class DesignationTypeEnum extends Enum public const API_AVAILABLE_TYPES = [ self::COMMITTEE_SUPERVISOR, self::CONSULTATION, + self::VOTE, ]; } diff --git a/src/VotingPlatform/Notifier/ElectionNotifier.php b/src/VotingPlatform/Notifier/ElectionNotifier.php index 2de3aad3f16..a3d778d2672 100644 --- a/src/VotingPlatform/Notifier/ElectionNotifier.php +++ b/src/VotingPlatform/Notifier/ElectionNotifier.php @@ -9,13 +9,16 @@ use App\Entity\VotingPlatform\Voter; use App\Mailer\MailerService; use App\Mailer\Message\CommitteeElectionCandidacyPeriodIsOverMessage; +use App\Mailer\Message\Renaissance\VotingPlatform\ConsultationAnnouncementMessage; use App\Mailer\Message\Renaissance\VotingPlatform\ConsultationIsOpenMessage; use App\Mailer\Message\Renaissance\VotingPlatform\ResultsReadyMessage; +use App\Mailer\Message\Renaissance\VotingPlatform\VoteAnnouncementMessage; use App\Mailer\Message\Renaissance\VotingPlatform\VoteConfirmationMessage; use App\Mailer\Message\Renaissance\VotingPlatform\VoteIsOpenMessage; use App\Mailer\Message\Renaissance\VotingPlatform\VoteReminder1DMessage; use App\Mailer\Message\Renaissance\VotingPlatform\VoteReminder1HMessage; use App\Mailer\Message\VotingPlatformElectionSecondRoundNotificationMessage; +use App\Repository\AdherentRepository; use App\Repository\VotingPlatform\VoterRepository; use App\VotingPlatform\Designation\DesignationTypeEnum; use Doctrine\ORM\EntityManagerInterface; @@ -28,23 +31,18 @@ public function __construct( private readonly UrlGeneratorInterface $urlGenerator, private readonly EntityManagerInterface $entityManager, private readonly VoterRepository $voterRepository, + private readonly AdherentRepository $adherentRepository, ) { } public function notifyElectionVoteIsOpen(Election $election): void { - if (!$this->isValid($election, Designation::NOTIFICATION_VOTE_OPENED)) { - return; - } - $electionType = $election->getDesignationType(); - $getRecipientsCallback = function (int $offset, int $limit) use ($election): array { - return $this->getAdherentForElection($election, $offset, $limit); - }; $url = $this->getUrl($election); - $this->batchSendEmail( - $getRecipientsCallback, + $this->sendNotification( + Designation::NOTIFICATION_VOTE_OPENED, + $election, function (array $recipients) use ($election, $electionType, $url) { if (DesignationTypeEnum::CONSULTATION === $electionType) { return ConsultationIsOpenMessage::create($election, $recipients, $url); @@ -53,57 +51,53 @@ function (array $recipients) use ($election, $electionType, $url) { return VoteIsOpenMessage::create($election, $recipients, $url); } ); + } - $election->markSentNotification(Designation::NOTIFICATION_VOTE_OPENED); + public function notifyVoteAnnouncement(Election $election): void + { + $electionType = $election->getDesignationType(); + $url = $this->getUrl($election); - $this->entityManager->flush(); - } + $this->sendNotification( + Designation::NOTIFICATION_VOTE_ANNOUNCEMENT, + $election, + function (array $recipients) use ($election, $electionType, $url) { + if (DesignationTypeEnum::CONSULTATION === $electionType) { + return ConsultationAnnouncementMessage::create($election, $recipients, $url); + } - public function notifyCommitteeElectionCandidacyPeriodIsOver( - Adherent $adherent, - Designation $designation, - Committee $committee, - ): void { - $this->transactionalMailer->sendMessage(CommitteeElectionCandidacyPeriodIsOverMessage::create( - $adherent, - $committee, - $designation, - $this->urlGenerator->generate('app_committee_show', ['slug' => $committee->getSlug()], UrlGeneratorInterface::ABSOLUTE_URL) - )); + return VoteAnnouncementMessage::create($election, $recipients, $url); + } + ); } - public function notifyVotingPlatformVoteReminder(Election $election, int $notification): void + public function notifyVoteReminder(Election $election, int $notification): void { - if (!$this->isValid($election, $notification)) { + if (!\in_array($notification, [Designation::NOTIFICATION_VOTE_REMINDER_1D, Designation::NOTIFICATION_VOTE_REMINDER_1H], true)) { return; } - $getRecipientsCallback = function (int $offset, int $limit) use ($election): array { - return $this->getAdherentForElection($election, $offset, $limit); - }; + $zones = $election->getDesignation()->getZones()->toArray(); $url = $this->getUrl($election); - $this->batchSendEmail( - $getRecipientsCallback, - function (array $recipients) use ($notification, $election, $url) { + $this->sendNotification( + $notification, + $election, + function (array $recipients) use ($election, $url, $notification) { if (Designation::NOTIFICATION_VOTE_REMINDER_1H === $notification) { return VoteReminder1HMessage::create($election, $recipients, $url); } return VoteReminder1DMessage::create($election, $recipients, $url); - } + }, + $zones ? function (int $offset, int $limit) use ($election, $zones): array { + return $this->adherentRepository->getAllInZonesAndNotVoted($election, $zones, $offset, $limit); + } : null ); - - $election->markSentNotification($notification); - $this->entityManager->flush(); } public function notifyElectionVoteIsOver(Election $election): void { - if (!$this->isValid($election, Designation::NOTIFICATION_VOTE_CLOSED)) { - return; - } - $designation = $election->getDesignation(); /** * If the election does not have a delay for displaying the results, we don't send the email for availability of the results here. @@ -113,44 +107,28 @@ public function notifyElectionVoteIsOver(Election $election): void return; } - $getRecipientsCallback = function (int $offset, int $limit) use ($election): array { - return $this->getAdherentForElection($election, $offset, $limit); - }; - $url = $this->getUrl($election); - $this->batchSendEmail( - $getRecipientsCallback, + $this->sendNotification( + Designation::NOTIFICATION_VOTE_CLOSED, + $election, function (array $recipients) use ($election, $url) { return ResultsReadyMessage::create($election, $recipients, $url); } ); - - $election->markSentNotification(Designation::NOTIFICATION_VOTE_CLOSED); - $this->entityManager->flush(); } public function notifyForForElectionResults(Election $election): void { - if (!$this->isValid($election, Designation::NOTIFICATION_RESULT_READY)) { - return; - } - - $getRecipientsCallback = function (int $offset, int $limit) use ($election): array { - return $this->getAdherentForElection($election, $offset, $limit); - }; - $url = $this->getUrl($election); - $this->batchSendEmail( - $getRecipientsCallback, + $this->sendNotification( + Designation::NOTIFICATION_RESULT_READY, + $election, function (array $recipients) use ($election, $url) { return ResultsReadyMessage::create($election, $recipients, $url); } ); - - $election->markSentNotification(Designation::NOTIFICATION_RESULT_READY); - $this->entityManager->flush(); } public function notifyElectionSecondRound(Election $election): void @@ -159,20 +137,15 @@ public function notifyElectionSecondRound(Election $election): void return; } - if (!$this->isValid($election, Designation::NOTIFICATION_SECOND_ROUND)) { - return; - } - - if ($adherents = $this->getAdherentForElection($election)) { - $this->transactionalMailer->sendMessage(VotingPlatformElectionSecondRoundNotificationMessage::create( - $election, - $adherents, - $this->getUrl($election) - )); + $url = $this->getUrl($election); - $election->markSentNotification(Designation::NOTIFICATION_SECOND_ROUND); - $this->entityManager->flush(); - } + $this->sendNotification( + Designation::NOTIFICATION_SECOND_ROUND, + $election, + function (array $recipients) use ($election, $url) { + return VotingPlatformElectionSecondRoundNotificationMessage::create($election, $recipients, $url); + } + ); } public function notifyVoteConfirmation(Election $election, Voter $voter, string $voterKey): void @@ -185,6 +158,44 @@ public function notifyVoteConfirmation(Election $election, Voter $voter, string )); } + public function notifyCommitteeElectionCandidacyPeriodIsOver( + Adherent $adherent, + Designation $designation, + Committee $committee, + ): void { + $this->transactionalMailer->sendMessage(CommitteeElectionCandidacyPeriodIsOverMessage::create( + $adherent, + $committee, + $designation, + $this->urlGenerator->generate('app_committee_show', ['slug' => $committee->getSlug()], UrlGeneratorInterface::ABSOLUTE_URL) + )); + } + + private function sendNotification(int $notification, Election $election, callable $createMessageCallback, ?callable $getRecipientsCallback = null): void + { + if (!$this->isValid($election, $notification)) { + return; + } + + if (!$getRecipientsCallback) { + $designation = $election->getDesignation(); + if ($zones = $designation->getZones()->toArray()) { + $getRecipientsCallback = function (int $offset, int $limit) use ($zones): array { + return $this->adherentRepository->getAllInZones($zones, true, false, $offset, $limit); + }; + } else { + $getRecipientsCallback = function (int $offset, int $limit) use ($election): array { + return $this->getAdherentForElection($election, $offset, $limit); + }; + } + } + + $this->batchSendEmail($getRecipientsCallback, $createMessageCallback); + + $election->markSentNotification(Designation::NOTIFICATION_VOTE_OPENED); + $this->entityManager->flush(); + } + /** * @return Adherent[] */ diff --git a/templates/admin/instances/candidate_list_binomes_column.html.twig b/templates/admin/instances/candidate_list_binomes_column.html.twig index 13dff4bd67a..1b183ac30dc 100644 --- a/templates/admin/instances/candidate_list_binomes_column.html.twig +++ b/templates/admin/instances/candidate_list_binomes_column.html.twig @@ -2,6 +2,6 @@ {% block field %} {% if object.binome_ids is defined and object.binome_ids|length %} - {{ object.binome_ids is defined ? object.binome_ids|join }} + {{ object.binome_ids is defined ? object.binome_ids|join }} {% endif %} {% endblock %} diff --git a/templates/admin/instances/election_list_pools_column.html.twig b/templates/admin/instances/election_list_pools_column.html.twig index 4147715343e..f6e6fa18d89 100644 --- a/templates/admin/instances/election_list_pools_column.html.twig +++ b/templates/admin/instances/election_list_pools_column.html.twig @@ -6,10 +6,4 @@ {% if object.designation.isCommitteeTypes() and object.electionEntity.committee %} {% set filters = filters|merge({committee: {value: object.electionEntity.committee.id}}) %} {% endif %} - - {% endblock %} diff --git a/templates/admin/instances/vote_list_action_column.html.twig b/templates/admin/instances/vote_list_action_column.html.twig index 189a88cbaf2..2c443717fbd 100644 --- a/templates/admin/instances/vote_list_action_column.html.twig +++ b/templates/admin/instances/vote_list_action_column.html.twig @@ -4,6 +4,6 @@ {% set filters = filters|merge({committee: {value: object.election.electionEntity.committee.id}}) %} {% endif %} - + diff --git a/templates/voting_platform/_layout_consultation.html.twig b/templates/voting_platform/_layout_consultation.html.twig index 3874da9965e..1fb903dfa80 100644 --- a/templates/voting_platform/_layout_consultation.html.twig +++ b/templates/voting_platform/_layout_consultation.html.twig @@ -52,7 +52,7 @@

{% if designation.targetYear %}

- À parti des adhérents à jour {{ designation.targetYear }} + Collège électoral : à parti des adhérents à jour {{ designation.targetYear }}

{% endif %} @@ -64,10 +64,10 @@
- - - - + + + +
Taux de participation{{ (voters == 0 ? 0 : participated / voters * 100)|number_format(2, ',') }} %
Taux de participation{{ (voters == 0 ? 0 : participated / voters * 100)|number_format(2, ',') }} %
@@ -77,10 +77,6 @@ {% block voting_platform_content %} {% for pool_result in election_round_result.getElectionPoolResults %} - {% if designation.isBlankVoteEnabled() %} - {% set blank = pool_result.getBlank() ?? 0 %} - {% endif %} -

{{ pool_result.getElectionPool().getCode() }}

@@ -103,15 +99,9 @@ {% set candidate = candidate_group.getCandidates|first %} {{ candidate.getFirstName() }} - {{ candidate_group_result.getRate() }} + {{ candidate_group_result.getRate() }} % {% endfor %} - {% if blank is defined %} - - Blancs - {{ blank }} - - {% endif %}
@@ -121,6 +111,6 @@

Les résultats sont disponibles en ligne aux adhérents jusqu’à J+{{ designation.getResultDisplayDelay() }} ({{ designation.getResultEndDate()|format_datetime('none', 'none', "d MMMM yyyy à HH:mm") }}).

-

Une question, un problème ? Contactez votre assemblée départementale.

+

Une question, un problème ? Contactez votre Assemblée départementale.

{% endblock %} diff --git a/templates/voting_platform/_layout_vote.html.twig b/templates/voting_platform/_layout_vote.html.twig new file mode 100644 index 00000000000..80718b23d5a --- /dev/null +++ b/templates/voting_platform/_layout_vote.html.twig @@ -0,0 +1,116 @@ +{% extends 'voting_platform/_layout_base.html.twig' %} + +{% set start_button_label = 'J\'accède au vote' %} +{% set vote_blanc_label = 'Ne se prononce pas' %} + +{% block banner %} +
+ + ← Retourner + +
+{% endblock %} + +{% block election_sub_title '' %} + +{% block vote_step_pool_title %} + {{ pool.code }} +{% endblock %} + +{% block voting_platform_subtitle %} + Le vote est ouvert du + {% if election.isSecondRoundVotePeriodActive %} + {{ election.voteEndDate|date('d/m/Y \à H:i') }} au {{ election.secondRoundEndDate|date('d/m/Y \à H:i') }} + {% else %} + {{ election.voteStartDate|date('d/m/Y \à H:i') }} au {{ election.voteEndDate|date('d/m/Y \à H:i') }} + {% endif %} +{% endblock %} + +{% block vote_finish_action_block %} +
+ + Retour + +
+{% endblock %} + +{% block header_actions %} + Revenir à l'accueil +{% endblock %} + +{% block voting_platform_result_tab_votes_list_title 'Détails des résultats' %} + +{% block voting_platform_result_tab_pool_title '' %} + +{% block voting_platform_result_header %} +
+

{{ designation.title }}

+

+ Du {{ designation.voteStartDate|format_datetime('none', 'none', "d MMMM yyyy à HH:mm") }} au + {{ designation.voteEndDate|format_datetime('none', 'none', "d MMMM yyyy à HH:mm") }} +

+ {% if designation.targetYear %} +

+ Collège électoral : à parti des adhérents à jour {{ designation.targetYear }} +

+ {% endif %} + + {% set pool_result = election_round_result.getElectionPoolResults|first %} + {% set voters = pool_result.participated ?? 0 %} + {% set participated = pool_result.getBulletinCount() ?? 0 %} + +
+
+ + + + + + + +
Taux de participation{{ (voters == 0 ? 0 : participated / voters * 100)|number_format(2, ',') }} %
+
+
+
+{% endblock %} + +{% block voting_platform_content %} + {% for pool_result in election_round_result.getElectionPoolResults %} +
+

{{ pool_result.getElectionPool().getCode() }}

+ +
+
+ + + + + + + + + {% for candidate_group_result in pool_result.getCandidateGroupResults() %} + {% set candidate_group = candidate_group_result.getCandidateGroup() %} + {% set candidate = candidate_group.getCandidates|first %} + + + + + {% endfor %} + +
+ Bulletin + + % de bulletins +
{{ candidate.getFirstName() }}{{ candidate_group_result.getRate() }} %
+
+
+
+ {% endfor %} + +
+

Les résultats sont disponibles en ligne aux adhérents jusqu’à J+{{ designation.getResultDisplayDelay() }} ({{ designation.getResultEndDate()|format_datetime('none', 'none', "d MMMM yyyy à HH:mm") }}).

+

Une question, un problème ? Contactez votre Assemblée départementale.

+
+{% endblock %} diff --git a/templates/voting_platform/confirmation/_confirmation_block.html.twig b/templates/voting_platform/confirmation/_confirmation_block.html.twig index 77ddfdecce1..4a9e9033608 100644 --- a/templates/voting_platform/confirmation/_confirmation_block.html.twig +++ b/templates/voting_platform/confirmation/_confirmation_block.html.twig @@ -6,7 +6,7 @@ {% include 'voting_platform/confirmation/candidate_box/burex_content.html.twig' %} {% elseif designation.isPollType() %} {% include 'voting_platform/confirmation/candidate_box/poll_content.html.twig' %} - {% elseif designation.isConsultationType() %} + {% elseif designation.isConsultationType() or designation.isVoteType() %} {% include 'voting_platform/confirmation/candidate_box/consultation_content.html.twig' %} {% elseif designation.isTerritorialAssemblyType() or designation.isLocalElectionTypes() or designation.isCommitteeSupervisorType() %} {% include 'voting_platform/confirmation/candidate_box/local_election_content.html.twig' %} diff --git a/templates/voting_platform/index.html.twig b/templates/voting_platform/index.html.twig index 9c07af3fb88..0120f6f8304 100644 --- a/templates/voting_platform/index.html.twig +++ b/templates/voting_platform/index.html.twig @@ -46,7 +46,11 @@

Pour les territoires avec un nombre de membres supplémentaires voté par le Conseil territorial pour respecter la règle de parité au sein du Comité politique :

A l’issue du vote des 5 binômes paritaires pour le Comité politique, seront élus individuellement (le(s) femme(s) ou le(es)’hommes(s)) figurant dans les binômes ayant obtenu le plus de voix en valeur absolue, tous types de binômes confondus, à la suite des binômes paritaires élus.

- {% elseif election.designationType in [constant('App\\VotingPlatform\\Designation\\DesignationTypeEnum::COMMITTEE_SUPERVISOR'), constant('App\\VotingPlatform\\Designation\\DesignationTypeEnum::CONSULTATION')] %} + {% elseif election.designationType in [ + constant('App\\VotingPlatform\\Designation\\DesignationTypeEnum::COMMITTEE_SUPERVISOR'), + constant('App\\VotingPlatform\\Designation\\DesignationTypeEnum::CONSULTATION'), + constant('App\\VotingPlatform\\Designation\\DesignationTypeEnum::VOTE'), + ] %}

{{ designation.getDescription()|nl2br }}

{% elseif election.designationType == constant('App\\VotingPlatform\\Designation\\DesignationTypeEnum::NATIONAL_COUNCIL') %}

Pour assurer une meilleure représentativité et poursuivre nos objectifs de démocratie interne et de parité dans nos instances, nous vous invitions à désigner un trio composé d'un élu local, d'un animateur local et d'un adhérent qui constituera un quatuor paritaire avec le référent territorial. Le trio désigné siégera au Conseil national pendant 2 ans.

diff --git a/templates/voting_platform/vote.html.twig b/templates/voting_platform/vote.html.twig index e46de265a3e..e27d20259a9 100644 --- a/templates/voting_platform/vote.html.twig +++ b/templates/voting_platform/vote.html.twig @@ -15,7 +15,7 @@
{% for choice in form.poolChoice %} - {% include 'voting_platform/vote_step/' ~ (designation.isMajorityType() ? '_majority_vote') ~ (designation.isPollType or designation.isConsultationType() ? '_poll') ~ '_candidate_box.html.twig' with { + {% include 'voting_platform/vote_step/' ~ (designation.isMajorityType() ? '_majority_vote') ~ (designation.isPollType or designation.isConsultationType() or designation.isVoteType() ? '_poll') ~ '_candidate_box.html.twig' with { candidate_group: candidate_groups|filter(group => (designation.isMajorityType ? choice.vars.name : choice.vars.value) == group.uuid.toString)|first, form: choice } %} diff --git a/translations/messages+intl-icu.fr.yml b/translations/messages+intl-icu.fr.yml index 3ca979763fa..70f78e19a35 100644 --- a/translations/messages+intl-icu.fr.yml +++ b/translations/messages+intl-icu.fr.yml @@ -960,6 +960,7 @@ voting_platform.designation.type_poll: Vote des statuts voting_platform.designation.type_local_election: Élection départementale voting_platform.designation.type_local_poll: Sondage local voting_platform.designation.type_consultation: Consultation +voting_platform.designation.type_vote: Vote voting_platform.designation.type_territorial_assembly: Élection du Bureau de l'Assemblée des territoires voting_platform.designation.zone_fr: France voting_platform.designation.zone_fde: FDE