diff --git a/.env b/.env index 09302d5..f3f7f42 100644 --- a/.env +++ b/.env @@ -18,3 +18,6 @@ CAS_MAIL=mail CAS_LASTNAME=lastname CAS_FIRSTNAME=firstname +ETHERPAD_URL=http://localhost:9001 +ETHERPAD_INTERNALURL=http://etherpad:9001 +ETHERPAD_APIKEY=changeme # identique que data/etherpad/apikey/APIKEY.txt \ No newline at end of file diff --git a/.env.dev b/.env.dev deleted file mode 100644 index b5e81da..0000000 --- a/.env.dev +++ /dev/null @@ -1,4 +0,0 @@ - -###> symfony/framework-bundle ### -APP_SECRET=628790eb09e2cf96d93a21f4f43433d8 -###< symfony/framework-bundle ### diff --git a/.gitignore b/.gitignore index 7373367..f233dc1 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ phpstan.neon /public/bundles/ /var/ /vendor/ +/data/ ###< symfony/framework-bundle ### ###> friendsofphp/php-cs-fixer ### diff --git a/compose.yaml b/compose.yaml index 4fa64e4..153b217 100644 --- a/compose.yaml +++ b/compose.yaml @@ -34,6 +34,22 @@ services: - ./vendor:/app/vendor:delegated - ./public/bundles:/app/public/bundles:delegated + etherpad: + image: etherpad/etherpad:latest + container_name: etherpad + restart: unless-stopped + environment: + - ADMIN_PASSWORD=changeme + - TITLE=Mon Etherpad + - DEFAULT_PAD_TEXT=Bienvenue dans Etherpad! + - API_KEY=changeme + - AUTHENTICATION_METHOD=apikey + ports: + - "9001:9001" + volumes: + - ./volume/etherpad/data:/opt/etherpad-lite/var + - ./volume/etherpad/settings:/opt/etherpad-lite/settings + - ./volume/etherpad/apikey/APIKEY.txt:/opt/etherpad-lite/APIKEY.txt adminer: image: adminer diff --git a/config/services.yaml b/config/services.yaml index 60c2f94..fa81e22 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -12,7 +12,9 @@ parameters: casMail: "%env(resolve:CAS_MAIL)%" casLastname: "%env(resolve:CAS_LASTNAME)%" casFirstname: "%env(resolve:CAS_FIRSTNAME)%" - + etherpadUrl: "%env(resolve:ETHERPAD_URL)%" + etherpadInternalUrl: "%env(resolve:ETHERPAD_INTERNALURL)%" + etherpadApiKey: "%env(resolve:ETHERPAD_APIKEY)%" services: _defaults: diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php index 70e579e..b17038d 100644 --- a/src/Controller/HomeController.php +++ b/src/Controller/HomeController.php @@ -2,23 +2,18 @@ namespace App\Controller; -use App\Entity\User; +use App\Repository\ProjectRepository; +use App\Service\EtherpadService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; class HomeController extends AbstractController { #[Route('/', name: 'app_home')] - public function home(Request $request): Response + public function home(ProjectRepository $projectRepository): Response { - $user = $this->getUser(); - if (!$user instanceof User) { - throw new AccessDeniedException('Vous n\'avez pas accès à cette ressource.'); - } - $projects = $user->getProjects(); + $projects = $projectRepository->findAll(); return $this->render('home/home.html.twig', [ 'usemenu' => true, @@ -35,4 +30,19 @@ class HomeController extends AbstractController 'usesidebar' => true, ]); } + + #[Route('/user/etherpad/{id}', name: 'app_ehterpad')] + public function etherpad(string $id, EtherpadService $etherpadService): Response + { + $padAccess = $etherpadService->preparePadAccess($this->getUser(), $id); + dump(vars: $padAccess); + + $response = $this->render('etherpad/show.html.twig', [ + 'iframeUrl' => $padAccess['iframeUrl'], + ]); + + $response->headers->setCookie($padAccess['cookie']); + + return $response; + } } diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index 978d4db..f0aa9cb 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -121,6 +121,9 @@ class ProjectController extends AbstractController $attribute = constant(ProjectVoter::class.'::MOVE'.$status); $this->denyAccessUnlessGranted($attribute, $project); + if (Project::VOTED == $status) { + } + $project->setStatus(constant(Project::class.'::'.$status)); $em->flush(); diff --git a/src/Entity/Project.php b/src/Entity/Project.php index fd2369f..c18e43e 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -42,6 +42,15 @@ class Project #[ORM\Column(type: 'datetime', nullable: true)] private ?\DateTimeInterface $dueDate = null; + #[ORM\Column(type: 'integer')] + private int $nbVoteWhite = 0; + + #[ORM\Column(type: 'integer')] + private int $nbVoteNull = 0; + + #[ORM\Column(type: 'string', nullable: true)] + private ?string $resultVote = null; + /** * @var Collection */ @@ -51,14 +60,14 @@ class Project /** * @var Collection */ - #[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectTimeline::class, cascade: ['remove'])] + #[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectTimeline::class, cascade: ['remove'], orphanRemoval: true)] #[ORM\OrderBy(['createdAt' => 'DESC'])] private Collection $timelines; /** - * @var Collection + * @var Collection */ - #[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectOption::class, cascade: ['remove'])] + #[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectOption::class, cascade: ['remove'], orphanRemoval: true)] #[ORM\OrderBy(['title' => 'ASC'])] private Collection $options; @@ -146,6 +155,42 @@ class Project return $this; } + public function getNbVoteWhite(): int + { + return $this->nbVoteWhite; + } + + public function setNbVoteWhite(int $nbVoteWhite): self + { + $this->nbVoteWhite = $nbVoteWhite; + + return $this; + } + + public function getNbVoteNull(): int + { + return $this->nbVoteNull; + } + + public function setNbVoteNull(int $nbVoteNull): self + { + $this->nbVoteNull = $nbVoteNull; + + return $this; + } + + public function getResultVote(): ?string + { + return $this->resultVote; + } + + public function setResultVote(?string $resultVote): self + { + $this->resultVote = $resultVote; + + return $this; + } + /** * @return Collection */ @@ -182,4 +227,21 @@ class Project { return $this->options; } + + public function addOption(ProjectOption $option): self + { + if (!$this->options->contains($option)) { + $this->options->add($option); + $option->setProject($this); + } + + return $this; + } + + public function removeOption(ProjectOption $option): self + { + $this->options->removeElement($option); + + return $this; + } } diff --git a/src/Entity/ProjectOption.php b/src/Entity/ProjectOption.php index 71f7f01..791c77e 100644 --- a/src/Entity/ProjectOption.php +++ b/src/Entity/ProjectOption.php @@ -26,6 +26,9 @@ class ProjectOption #[ORM\Column(type: 'text', nullable: true)] private ?string $whyNot = null; + #[ORM\Column(type: 'integer')] + private int $nbVote = 0; + public function getId(): ?int { return $this->id; @@ -78,4 +81,16 @@ class ProjectOption return $this; } + + public function getNbVote(): int + { + return $this->nbVote; + } + + public function setNbVote(int $nbVote): self + { + $this->nbVote = $nbVote; + + return $this; + } } diff --git a/src/Form/ProjectType.php b/src/Form/ProjectType.php index dbdf558..ec7097f 100644 --- a/src/Form/ProjectType.php +++ b/src/Form/ProjectType.php @@ -8,6 +8,7 @@ use Bnine\MdEditorBundle\Form\Type\MarkdownType; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -28,6 +29,12 @@ class ProjectType extends AbstractType 'label' => 'Titre', ]) + ->add('dueDate', DateType::class, [ + 'label' => 'A Voter pour le', + 'required' => false, + 'html5' => true, + ]) + ->add('summary', TextareaType::class, [ 'label' => 'Résumé', ]) diff --git a/src/Form/ProjectVotedType.php b/src/Form/ProjectVotedType.php new file mode 100644 index 0000000..51443a1 --- /dev/null +++ b/src/Form/ProjectVotedType.php @@ -0,0 +1,40 @@ +add('submit', SubmitType::class, [ + 'label' => 'Valider', + 'attr' => ['class' => 'btn btn-success no-print me-1'], + ]) + + ->add('options', CollectionType::class, [ + 'entry_type' => ProjectOptionType::class, + 'entry_options' => ['label' => false], + 'label' => false, + 'allow_add' => false, + 'allow_delete' => false, + 'by_reference' => false, + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Project::class, + 'mode' => 'submit', + ]); + } +} diff --git a/src/Form/Type/ProjectOptionType.php b/src/Form/Type/ProjectOptionType.php new file mode 100644 index 0000000..13ef84a --- /dev/null +++ b/src/Form/Type/ProjectOptionType.php @@ -0,0 +1,31 @@ +add('title', null, [ + 'disabled' => true, + 'label' => 'Titre', + ]) + ->add('nbVote', IntegerType::class, [ + 'label' => 'Nombre de votes', + ]); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => ProjectOption::class, + ]); + } +} diff --git a/src/Security/ProjectVoter.php b/src/Security/ProjectVoter.php index cf3e207..cc6e6c2 100644 --- a/src/Security/ProjectVoter.php +++ b/src/Security/ProjectVoter.php @@ -18,10 +18,11 @@ class ProjectVoter extends Voter public const MOVETOVOTE = 'MOVETOVOTE'; public const MOVEVOTED = 'MOVEVOTED'; public const MOVEARCHIVED = 'MOVEARCHIVED'; + public const TOME = 'TOME'; protected function supports(string $attribute, $subject): bool { - $attributes = [self::VIEW, self::SUBMIT, self::UPDATE, self::DELETE, self::MOVEDRAFT, self::MOVETOVOTE, self::MOVEVOTED, self::MOVEARCHIVED]; + $attributes = [self::VIEW, self::SUBMIT, self::UPDATE, self::DELETE, self::MOVEDRAFT, self::MOVETOVOTE, self::MOVEVOTED, self::MOVEARCHIVED, self::TOME]; return in_array($attribute, $attributes) && $subject instanceof Project; } @@ -79,7 +80,6 @@ class ProjectVoter extends Voter { $hasUser = $user->hasRole('ROLE_ADMIN') || $user->hasRole('ROLE_MASTER'); $hasStatus = Project::TOVOTE === $project->getStatus() || Project::ARCHIVED === $project->getStatus(); - dump($hasStatus); return $hasUser && $hasStatus; } @@ -92,13 +92,17 @@ class ProjectVoter extends Voter return $hasUser && $hasStatus; } + private function toMe(Project $project, User $user): bool + { + return $project->getUsers()->contains($user); + } + protected function voteOnAttribute(string $attribute, $project, TokenInterface $token): bool { $user = $token->getUser(); if (!$user instanceof User) { return false; } - dump($attribute); return match ($attribute) { self::VIEW => $this->canView($project, $user), @@ -109,6 +113,7 @@ class ProjectVoter extends Voter self::MOVETOVOTE => $this->canMoveToVote($project, $user), self::MOVEVOTED => $this->canMoveVoted($project, $user), self::MOVEARCHIVED => $this->canMoveArchived($project, $user), + self::TOME => $this->toMe($project, $user), default => false, }; } diff --git a/src/Service/EtherpadService.php b/src/Service/EtherpadService.php new file mode 100644 index 0000000..a561434 --- /dev/null +++ b/src/Service/EtherpadService.php @@ -0,0 +1,117 @@ +client = $client; + $this->etherpadUrl = $parameter->get('etherpadUrl'); + $this->etherpadInternalUrl = $parameter->get('etherpadInternalUrl'); + $this->etherpadApiKey = $parameter->get('etherpadApiKey'); + } + + public function preparePadAccess(User $user, string $padName): array + { + // 1. Vérifier/créer l'auteur + $authorId = $this->createAuthorIfNeeded($user); + + // 2. Vérifier/créer le groupe lié à ce pad + $groupId = $this->createGroupIfNeeded($padName); + + // 3. Créer le pad si pas encore créé + $this->createGroupPadIfNeeded($groupId, $padName); + + // 4. Créer une session temporaire (ex: 1h) + $validUntil = time() + 3600; + $sessionId = $this->createSession($groupId, $authorId, $validUntil); + + // 5. Construire le cookie à ajouter à la réponse + $cookie = Cookie::create('sessionID', $sessionId) + ->withDomain(parse_url($this->etherpadInternalUrl, PHP_URL_HOST)) + ->withPath('/') + ->withSecure(true) + ->withHttpOnly(false) + ->withExpires($validUntil); + + // 6. Construire l’URL iframe + $iframeUrl = sprintf( + '%s/p/%s?showControls=true&showChat=false&userName=%s', + rtrim($this->etherpadUrl, '/'), + $padName, + urlencode($user->getUsername()) + ); + + return [ + 'iframeUrl' => $iframeUrl, + 'cookie' => $cookie, + ]; + } + + private function createAuthorIfNeeded(User $user): string + { + // Idéalement, stocker $authorId en base pour éviter de le recréer + $response = $this->client->request('GET', $this->etherpadInternalUrl.'/api/1.2.15/createAuthor', [ + 'query' => [ + 'apikey' => $this->etherpadApiKey, + 'name' => $user->getUsername(), + ], + ]); + + $data = $response->toArray(false); + + return $data['data']['authorID'] ?? throw new \RuntimeException('Impossible de créer l\'author'); + } + + private function createGroupIfNeeded(string $padName): string + { + // Ici tu peux lier un groupID en base à ton padName + $response = $this->client->request('GET', $this->etherpadInternalUrl.'/api/1.2.15/createGroup', [ + 'query' => [ + 'apikey' => $this->etherpadApiKey, + ], + ]); + + $data = $response->toArray(false); + + return $data['data']['groupID'] ?? throw new \RuntimeException('Impossible de créer le group'); + } + + private function createGroupPadIfNeeded(string $groupId, string $padName): void + { + $this->client->request('GET', $this->etherpadInternalUrl.'/api/1.2.15/createGroupPad', [ + 'query' => [ + 'apikey' => $this->etherpadApiKey, + 'groupID' => $groupId, + 'padName' => $padName, + ], + ]); + } + + private function createSession(string $groupId, string $authorId, int $validUntil): string + { + $response = $this->client->request('GET', $this->etherpadInternalUrl.'/api/1.2.15/createSession', [ + 'query' => [ + 'apikey' => $this->etherpadApiKey, + 'groupID' => $groupId, + 'authorID' => $authorId, + 'validUntil' => $validUntil, + ], + ]); + + $data = $response->toArray(false); + + return $data['data']['sessionID'] ?? throw new \RuntimeException('Impossible de créer la session'); + } +} diff --git a/templates/etherpad/show.html.twig b/templates/etherpad/show.html.twig new file mode 100644 index 0000000..32087d7 --- /dev/null +++ b/templates/etherpad/show.html.twig @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/home/home.html.twig b/templates/home/home.html.twig index 1a7859e..3d9256d 100644 --- a/templates/home/home.html.twig +++ b/templates/home/home.html.twig @@ -4,12 +4,44 @@ {%block body%}

Projets

-Ajouter + -
+
+

Brouillon

+
+
+

A Voter

+
+
+

Voté

+
+
+

Archivé

+
+ +
{% for project in projects %} {% if is_granted('VIEW', project) %} -
+
{{project.title}}
@@ -20,9 +52,12 @@
{{project.summary|nl2br}}
- +{%endblock%} + +{% block localscript %} + +{% endblock %} -{%endblock%} \ No newline at end of file diff --git a/templates/option/edit.html.twig b/templates/option/edit.html.twig index 04ad283..36b9a60 100644 --- a/templates/option/edit.html.twig +++ b/templates/option/edit.html.twig @@ -18,9 +18,9 @@
-
Information
+
Titre
- {{ form_row(form.title) }} + {{ form_widget(form.title) }}
@@ -30,17 +30,17 @@
-
Information
+
Pourquoi Voter Pour
- {{ form_row(form.whyYes) }} + {{ form_widget(form.whyYes) }}
-
Information
+
Pourquoi Voter Contre
- {{ form_row(form.whyNot) }} + {{ form_widget(form.whyNot) }}
diff --git a/templates/project/edit.html.twig b/templates/project/edit.html.twig index 0412953..6a4c17c 100644 --- a/templates/project/edit.html.twig +++ b/templates/project/edit.html.twig @@ -22,6 +22,7 @@
{{ form_row(form.title) }} {{ form_row(form.nature) }} + {{ form_row(form.dueDate) }} {{ form_row(form.users) }} {{ form_row(form.summary) }}
@@ -51,7 +52,9 @@ Action - Title + Titre + Pour + Contre @@ -61,6 +64,8 @@ {{option.title}} + {{option.whyYes?option.whyYes|markdown_to_html:""}} + {{option.whyNot?option.whyNot|markdown_to_html:""}} {% endfor %} @@ -85,6 +90,13 @@ {% endblock %} diff --git a/templates/project/status.html.twig b/templates/project/status.html.twig index 2fbb683..8b13789 100644 --- a/templates/project/status.html.twig +++ b/templates/project/status.html.twig @@ -1,12 +1 @@ - {% if is_granted('MOVEDRAFT', project) %} - Mettre en Brouillon - {% endif %} - {% if is_granted('MOVETOVOTE', project) %} - Mettre au Vote - {% endif %} - {% if is_granted('MOVEVOTED', project) %} - Voter - {% endif %} - {% if is_granted('DELETE', project) %} - Supprimer - {% endif %} + diff --git a/templates/project/view.html.twig b/templates/project/view.html.twig index e6fe5bf..8c2744f 100644 --- a/templates/project/view.html.twig +++ b/templates/project/view.html.twig @@ -8,32 +8,59 @@
{{project.status}}
- {% if is_granted('UPDATE', project) %} - Modifier - {% endif %} +
+ {% if is_granted('UPDATE', project) %} + Modifier + {% endif %} - Retour + Retour + + {% if is_granted('MOVETOVOTE', project) and project.status=="Brouillon"%} + + Statut = à Voter + + {% endif %} + {% if is_granted('MOVEVOTED', project) and project.status=="A Voter" %} + + Statut = Voté + + {% endif %} + {% if is_granted('MOVEARCHIVED', project) and project.status=="Voté" %} + + Statut = Archivé + + {% endif %} + + {% if is_granted('DELETE', project) %} + + Supprimer + + {% endif %} + {% if is_granted('MOVEDRAFT', project) and project.status=="A Voter"%} + + Statut = Brouillon + + {% endif %} + {% if is_granted('MOVETOVOTE', project) and project.status=="Voté"%} + + Statut = à Voter + + {% endif %} + {% if is_granted('MOVEVOTED', project) and project.status=="Archivé"%} + + Statut = Voté + + {% endif %} +
- {% if is_granted('MOVEDRAFT', project) %} - Mettre en Brouillon - {% endif %} - {% if is_granted('MOVETOVOTE', project) %} - Mettre au Vote - {% endif %} - {% if is_granted('MOVEVOTED', project) %} - Voter - {% endif %} - {% if is_granted('DELETE', project) %} - Supprimer - {% endif %} -
-
+
Information
Titre = {{ project.title }}
- Nature = {{ project.nature }}

+ Nature = {{ project.nature }}
+ A Voter pour le = {{ project.dueDate ? project.dueDate|date("d/m/Y"):"" }}
@@ -55,6 +82,23 @@
+
+ {% for option in project.options %} +
+

{{ option.title }}

+
+
+
+ {{option.whyYes?option.whyYes|markdown_to_html:""}} +
+
+
+ {{option.whyNot?option.whyNot|markdown_to_html:""}} +
+
+
+ {% endfor %} +
Description du Projet