diff --git a/composer.lock b/composer.lock index d2ee646..68cc9c3 100644 --- a/composer.lock +++ b/composer.lock @@ -420,16 +420,16 @@ }, { "name": "doctrine/dbal", - "version": "3.10.0", + "version": "3.10.1", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "1cf840d696373ea0d58ad0a8875c0fadcfc67214" + "reference": "3626601014388095d3af9de7e9e958623b7ef005" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/1cf840d696373ea0d58ad0a8875c0fadcfc67214", - "reference": "1cf840d696373ea0d58ad0a8875c0fadcfc67214", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/3626601014388095d3af9de7e9e958623b7ef005", + "reference": "3626601014388095d3af9de7e9e958623b7ef005", "shasum": "" }, "require": { @@ -514,7 +514,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.10.0" + "source": "https://github.com/doctrine/dbal/tree/3.10.1" }, "funding": [ { @@ -530,7 +530,7 @@ "type": "tidelift" } ], - "time": "2025-07-10T21:11:04+00:00" + "time": "2025-08-05T12:18:06+00:00" }, { "name": "doctrine/deprecations", @@ -1221,16 +1221,16 @@ }, { "name": "doctrine/orm", - "version": "3.5.0", + "version": "3.5.1", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "6deec3655ba3e8f15280aac11e264225854d2369" + "reference": "64444dcfd511089d526cd2c7f74b9d7ed583bdfc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/6deec3655ba3e8f15280aac11e264225854d2369", - "reference": "6deec3655ba3e8f15280aac11e264225854d2369", + "url": "https://api.github.com/repos/doctrine/orm/zipball/64444dcfd511089d526cd2c7f74b9d7ed583bdfc", + "reference": "64444dcfd511089d526cd2c7f74b9d7ed583bdfc", "shasum": "" }, "require": { @@ -1305,9 +1305,9 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/3.5.0" + "source": "https://github.com/doctrine/orm/tree/3.5.1" }, - "time": "2025-07-01T17:40:53+00:00" + "time": "2025-08-05T06:05:51+00:00" }, { "name": "doctrine/persistence", @@ -10086,16 +10086,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.21", + "version": "2.1.22", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "1ccf445757458c06a04eb3f803603cb118fe5fa6" + "reference": "41600c8379eb5aee63e9413fe9e97273e25d57e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1ccf445757458c06a04eb3f803603cb118fe5fa6", - "reference": "1ccf445757458c06a04eb3f803603cb118fe5fa6", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/41600c8379eb5aee63e9413fe9e97273e25d57e4", + "reference": "41600c8379eb5aee63e9413fe9e97273e25d57e4", "shasum": "" }, "require": { @@ -10140,7 +10140,7 @@ "type": "github" } ], - "time": "2025-07-28T19:35:08+00:00" + "time": "2025-08-04T19:17:37+00:00" }, { "name": "phpstan/phpstan-doctrine", diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index 4706dec..978d4db 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -56,12 +56,12 @@ class ProjectController extends AbstractController $em->flush(); $this->fileService->init('project', $project->getId()); - return $this->redirectToRoute($isAdmin ? 'app_admin_project' : 'app_user_update', ['id' => $project->getId()]); + return $this->redirectToRoute($isAdmin ? 'app_admin_project_update' : 'app_user_project_update', ['id' => $project->getId()]); } return $this->render('project/edit.html.twig', [ 'usemenu' => true, - 'usesidebar' => true, + 'usesidebar' => $isAdmin, 'title' => 'Création Projet', 'routecancel' => $isAdmin ? 'app_admin_project' : 'app_home', 'routedelete' => $isAdmin ? 'app_admin_project_delete' : 'app_user_project_delete', @@ -87,7 +87,11 @@ class ProjectController extends AbstractController if ($form->isSubmitted() && $form->isValid()) { $em->flush(); - return $this->redirectToRoute($isAdmin ? 'app_admin_project' : 'app_home'); + if ($isAdmin) { + return $this->redirectToRoute('app_admin_project'); + } else { + return $this->redirectToRoute('app_user_project_view', ['id' => $project->getId()]); + } } return $this->render('project/edit.html.twig', [ @@ -95,7 +99,10 @@ class ProjectController extends AbstractController 'usesidebar' => $isAdmin, 'title' => 'Modification Projet = '.$project->getTitle(), 'routecancel' => $isAdmin ? 'app_admin_project' : 'app_home', + 'routemove' => $isAdmin ? 'app_admin_project_move' : 'app_user_project_move', 'routedelete' => $isAdmin ? 'app_admin_project_delete' : 'app_user_project_delete', + 'routesubmitoption' => $isAdmin ? 'app_admin_option_submit' : 'app_user_option_submit', + 'routeupdateoption' => $isAdmin ? 'app_admin_option_update' : 'app_user_option_update', 'mode' => 'update', 'form' => $form, 'project' => $project, @@ -119,7 +126,7 @@ class ProjectController extends AbstractController $isAdmin = str_starts_with($request->attributes->get('_route'), 'app_admin'); - return $this->redirectToRoute($isAdmin ? 'app_admin_project' : 'app_home'); + return $this->redirectToRoute($isAdmin ? 'app_admin_project' : 'app_user_project_view', ['id' => $project->getId()]); } #[Route('/admin/project/delete/{id}', name: 'app_admin_project_delete')] @@ -141,8 +148,6 @@ class ProjectController extends AbstractController $em->flush(); } catch (\Exception $e) { $this->addflash('error', $e->getMessage()); - - return $this->redirectToRoute($isAdmin ? 'app_admin_project' : 'app_user_update', ['id' => $project->getId()]); } return $this->redirectToRoute($isAdmin ? 'app_admin_project' : 'app_home'); @@ -158,11 +163,16 @@ class ProjectController extends AbstractController $this->denyAccessUnlessGranted(ProjectVoter::VIEW, $project); + $isAdmin = str_starts_with($request->attributes->get('_route'), 'app_admin'); + return $this->render('project/view.html.twig', [ 'usemenu' => true, 'usesidebar' => false, 'title' => 'Projet = '.$project->getTitle(), - 'routecancel' => 'app_home', + 'routeupdate' => $isAdmin ? 'app_admin_project_update' : 'app_user_project_update', + 'routecancel' => $isAdmin ? 'app_admin_project' : 'app_home', + 'routedelete' => $isAdmin ? 'app_admin_project_delete' : 'app_user_project_delete', + 'routemove' => $isAdmin ? 'app_admin_project_move' : 'app_user_project_move', 'project' => $project, ]); } diff --git a/src/Controller/ProjectOptionController.php b/src/Controller/ProjectOptionController.php new file mode 100644 index 0000000..b674b2a --- /dev/null +++ b/src/Controller/ProjectOptionController.php @@ -0,0 +1,111 @@ +find($idproject); + if (!$project) { + throw new NotFoundHttpException('La ressource demandée est introuvable.'); + } + $this->denyAccessUnlessGranted(ProjectVoter::UPDATE, $project); + + $option = new ProjectOption(); + $option->setProject($project); + + $isAdmin = str_starts_with($request->attributes->get('_route'), 'app_admin'); + + $form = $this->createForm(OptionType::class, $option, ['mode' => 'submit']); + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + $em->persist($option); + $em->flush(); + + return $this->redirectToRoute($isAdmin ? 'app_admin_project_update' : 'app_user_project_update', ['id' => $idproject]); + } + + return $this->render('option/edit.html.twig', [ + 'usemenu' => true, + 'usesidebar' => $isAdmin, + 'title' => 'Création Option', + 'routecancel' => $isAdmin ? 'app_admin_project_update' : 'app_admin_project_update', + 'routedelete' => $isAdmin ? 'app_admin_option_delete' : 'app_user_option_delete', + 'mode' => 'submit', + 'idproject' => $idproject, + 'form' => $form, + ]); + } + + #[Route('/admin/option/update/{idproject}/{id}', name: 'app_admin_option_update')] + #[Route('/user/option/update/{idproject}/{id}', name: 'app_user_option_update')] + public function update(int $idproject, int $id, Request $request, ProjectOptionRepository $projectOptionResitory, EntityManagerInterface $em): Response + { + $option = $projectOptionResitory->find($id); + if (!$option) { + throw new NotFoundHttpException('La ressource demandée est introuvable.'); + } + + $this->denyAccessUnlessGranted(ProjectVoter::UPDATE, $option->getProject()); + + $isAdmin = str_starts_with($request->attributes->get('_route'), 'app_admin'); + $form = $this->createForm(OptionType::class, $option, ['mode' => 'update']); + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + $em->flush(); + + return $this->redirectToRoute($isAdmin ? 'app_admin_project_update' : 'app_user_project_update', ['id' => $idproject]); + } + + return $this->render('option/edit.html.twig', [ + 'usemenu' => true, + 'usesidebar' => $isAdmin, + 'title' => 'Modification Option = '.$option->getTitle(), + 'routecancel' => $isAdmin ? 'app_admin_project_update' : 'app_user_project_update', + 'routedelete' => $isAdmin ? 'app_admin_option_delete' : 'app_user_option_delete', + 'mode' => 'update', + 'idproject' => $idproject, + 'option' => $option, + 'form' => $form, + ]); + } + + #[Route('/admin/option/delete/{idproject}/{id}', name: 'app_admin_option_delete')] + #[Route('/user/option/delete/{idproject}/{id}', name: 'app_user_option_delete')] + public function delete(int $idproject, int $id, Request $request, ProjectOptionRepository $projectOptionResitory, EntityManagerInterface $em): Response + { + $option = $projectOptionResitory->find($id); + if (!$option) { + throw new NotFoundHttpException('La ressource demandée est introuvable.'); + } + + $this->denyAccessUnlessGranted(ProjectVoter::UPDATE, $option->getProject()); + + $isAdmin = str_starts_with($request->attributes->get('_route'), 'app_admin'); + + // Tentative de suppression + try { + $em->remove($option); + $em->flush(); + } catch (\Exception $e) { + $this->addflash('error', $e->getMessage()); + } + + return $this->redirectToRoute($isAdmin ? 'app_admin_project_update' : 'app_user_project_update', ['id' => $idproject]); + } +} diff --git a/src/Entity/Project.php b/src/Entity/Project.php index 91a55d7..fd2369f 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -55,10 +55,18 @@ class Project #[ORM\OrderBy(['createdAt' => 'DESC'])] private Collection $timelines; + /** + * @var Collection + */ + #[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectOption::class, cascade: ['remove'])] + #[ORM\OrderBy(['title' => 'ASC'])] + private Collection $options; + public function __construct() { $this->users = new ArrayCollection(); $this->timelines = new ArrayCollection(); + $this->options = new ArrayCollection(); } public function getId(): ?int @@ -169,4 +177,9 @@ class Project { return $this->timelines; } + + public function getOptions(): Collection + { + return $this->options; + } } diff --git a/src/Entity/ProjectOption.php b/src/Entity/ProjectOption.php new file mode 100644 index 0000000..71f7f01 --- /dev/null +++ b/src/Entity/ProjectOption.php @@ -0,0 +1,81 @@ +id; + } + + public function getProject(): Project + { + return $this->project; + } + + public function setProject(Project $project): self + { + $this->project = $project; + + return $this; + } + + public function getTitle(): string + { + return $this->title; + } + + public function setTitle(string $title): self + { + $this->title = $title; + + return $this; + } + + public function getWhyYes(): ?string + { + return $this->whyYes; + } + + public function setWhyYes(?string $whyYes): self + { + $this->whyYes = $whyYes; + + return $this; + } + + public function getWhyNot(): ?string + { + return $this->whyNot; + } + + public function setWhyNot(?string $whyNot): self + { + $this->whyNot = $whyNot; + + return $this; + } +} diff --git a/src/Form/OptionType.php b/src/Form/OptionType.php new file mode 100644 index 0000000..b8c0612 --- /dev/null +++ b/src/Form/OptionType.php @@ -0,0 +1,45 @@ +add('submit', SubmitType::class, [ + 'label' => 'Valider', + 'attr' => ['class' => 'btn btn-success no-print me-1'], + ]) + + ->add('title', TextType::class, [ + 'label' => 'Titre', + ]) + + ->add('whyYes', MarkdownType::class, [ + 'label' => 'Pourquoi Voter Pour', + 'markdown_height' => 200, + ]) + + ->add('whyNot', MarkdownType::class, [ + 'label' => 'Pourquoi Voter Contre', + 'markdown_height' => 200, + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => ProjectOption::class, + 'mode' => 'submit', + ]); + } +} diff --git a/src/Form/ProjectType.php b/src/Form/ProjectType.php index 9b5abf3..dbdf558 100644 --- a/src/Form/ProjectType.php +++ b/src/Form/ProjectType.php @@ -21,7 +21,7 @@ class ProjectType extends AbstractType $builder ->add('submit', SubmitType::class, [ 'label' => 'Valider', - 'attr' => ['class' => 'btn btn-success no-print'], + 'attr' => ['class' => 'btn btn-success no-print me-1'], ]) ->add('title', TextType::class, [ diff --git a/src/Form/UserType.php b/src/Form/UserType.php index 4959c39..feaa4ed 100644 --- a/src/Form/UserType.php +++ b/src/Form/UserType.php @@ -24,7 +24,7 @@ class UserType extends AbstractType $builder ->add('submit', SubmitType::class, [ 'label' => 'Valider', - 'attr' => ['class' => 'btn btn-success no-print'], + 'attr' => ['class' => 'btn btn-success no-print me-1'], ]) ->add('username', TextType::class, [ diff --git a/src/Repository/ProjectOptionRepository.php b/src/Repository/ProjectOptionRepository.php new file mode 100644 index 0000000..6bb9c84 --- /dev/null +++ b/src/Repository/ProjectOptionRepository.php @@ -0,0 +1,18 @@ + + */ +class ProjectOptionRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, ProjectOption::class); + } +} diff --git a/src/Security/ProjectVoter.php b/src/Security/ProjectVoter.php index 6662982..cf3e207 100644 --- a/src/Security/ProjectVoter.php +++ b/src/Security/ProjectVoter.php @@ -16,10 +16,12 @@ class ProjectVoter extends Voter public const DELETE = 'DELETE'; public const MOVEDRAFT = 'MOVEDRAFT'; public const MOVETOVOTE = 'MOVETOVOTE'; + public const MOVEVOTED = 'MOVEVOTED'; + public const MOVEARCHIVED = 'MOVEARCHIVED'; protected function supports(string $attribute, $subject): bool { - $attributes = [self::VIEW, self::SUBMIT, self::UPDATE, self::DELETE, self::MOVEDRAFT, self::MOVETOVOTE]; + $attributes = [self::VIEW, self::SUBMIT, self::UPDATE, self::DELETE, self::MOVEDRAFT, self::MOVETOVOTE, self::MOVEVOTED, self::MOVEARCHIVED]; return in_array($attribute, $attributes) && $subject instanceof Project; } @@ -36,22 +38,58 @@ class ProjectVoter extends Voter private function canUpdate(Project $project, User $user): bool { - return $user->hasRole('ROLE_ADMIN') || (Project::DRAFT === $project->getStatus() && $project->getUsers()->contains($user)); + $hasMaster = $user->hasRole('ROLE_ADMIN') || $user->hasRole('ROLE_MASTER'); + $hasUser = $project->getUsers()->contains($user); + $hasStatus = Project::DRAFT === $project->getStatus(); + + return $hasMaster || ($hasUser && $hasStatus); } private function canDelete(Project $project, User $user): bool { - return $user->hasRole('ROLE_ADMIN') || (Project::DRAFT === $project->getStatus() && $project->getUsers()->contains($user)); + $hasMaster = $user->hasRole('ROLE_ADMIN') || $user->hasRole('ROLE_MASTER'); + $hasUser = $project->getUsers()->contains($user); + $hasStatus = Project::DRAFT === $project->getStatus(); + + return $hasMaster || ($hasUser && $hasStatus); } private function canMoveDraft(Project $project, User $user): bool { - return $user->hasRole('ROLE_ADMIN') || (Project::TOVOTE === $project->getStatus() && $project->getUsers()->contains($user)); + $hasUser = $user->hasRole('ROLE_ADMIN') || $user->hasRole('ROLE_MASTER') || $project->getUsers()->contains($user); + $hasStatus = Project::TOVOTE === $project->getStatus(); + + return $hasUser && $hasStatus; } private function canMoveToVote(Project $project, User $user): bool { - return $user->hasRole('ROLE_ADMIN') || (Project::DRAFT === $project->getStatus() && $project->getUsers()->contains($user)); + $hasMaster = $user->hasRole('ROLE_ADMIN') || $user->hasRole('ROLE_MASTER'); + $hasUser = $project->getUsers()->contains($user); + if (Project::VOTED === $project->getStatus()) { + return $hasMaster; + } elseif (Project::DRAFT === $project->getStatus()) { + return $hasMaster || $hasUser; + } else { + return false; + } + } + + private function canMoveVoted(Project $project, User $user): bool + { + $hasUser = $user->hasRole('ROLE_ADMIN') || $user->hasRole('ROLE_MASTER'); + $hasStatus = Project::TOVOTE === $project->getStatus() || Project::ARCHIVED === $project->getStatus(); + dump($hasStatus); + + return $hasUser && $hasStatus; + } + + private function canMoveArchived(Project $project, User $user): bool + { + $hasUser = $user->hasRole('ROLE_ADMIN') || $user->hasRole('ROLE_MASTER'); + $hasStatus = Project::VOTED === $project->getStatus(); + + return $hasUser && $hasStatus; } protected function voteOnAttribute(string $attribute, $project, TokenInterface $token): bool @@ -60,6 +98,7 @@ class ProjectVoter extends Voter if (!$user instanceof User) { return false; } + dump($attribute); return match ($attribute) { self::VIEW => $this->canView($project, $user), @@ -68,6 +107,8 @@ class ProjectVoter extends Voter self::DELETE => $this->canDelete($project, $user), self::MOVEDRAFT => $this->canMoveDraft($project, $user), self::MOVETOVOTE => $this->canMoveToVote($project, $user), + self::MOVEVOTED => $this->canMoveVoted($project, $user), + self::MOVEARCHIVED => $this->canMoveArchived($project, $user), default => false, }; } diff --git a/templates/home/home.html.twig b/templates/home/home.html.twig index ed94fe4..1a7859e 100644 --- a/templates/home/home.html.twig +++ b/templates/home/home.html.twig @@ -4,30 +4,28 @@ {%block body%}

Projets

+Ajouter +
{% for project in projects %} {% if is_granted('VIEW', project) %}
- {{project.status}}
- {{project.title}} - -
- {% if is_granted('UPDATE', project) %} - - {% endif %} +
{{project.title}}
+
- {{project.summary|raw}} - -
+ {{project.summary|nl2br}} +
+ diff --git a/templates/option/edit.html.twig b/templates/option/edit.html.twig new file mode 100644 index 0000000..04ad283 --- /dev/null +++ b/templates/option/edit.html.twig @@ -0,0 +1,58 @@ +{% extends 'base.html.twig' %} + +{% block title %} = {{title}}{% endblock %} + +{% block body %} +

{{title}}

+ + + {{ form_start(form) }} + {{ form_widget(form.submit) }} + Annuler + {% if mode=="update" %} + Supprimer + {% endif %} + + {% include('include/error.html.twig') %} + +
+
+
+
Information
+
+ {{ form_row(form.title) }} +
+
+
+
+ +
+
+
+
+
Information
+
+ {{ form_row(form.whyYes) }} +
+
+
+
+
+
Information
+
+ {{ form_row(form.whyNot) }} +
+
+
+
+ + {{ form_end(form) }} +{% endblock %} + +{% block localscript %} + +{% endblock %} diff --git a/templates/project/edit.html.twig b/templates/project/edit.html.twig index 7b420e0..0412953 100644 --- a/templates/project/edit.html.twig +++ b/templates/project/edit.html.twig @@ -8,18 +8,10 @@ {{ form_start(form) }} {{ form_widget(form.submit) }} - Annuler - {%if mode=="update" %} - {% if is_granted('MOVEDRAFT', project) %} - Mettre en Brouillon - {% endif %} - {% if is_granted('MOVETOVOTE', project) %} - Mettre au Vote - {% endif %} - {% if is_granted('DELETE', project) %} - Supprimer - {% endif %} - {%endif%} + Annuler + {% if mode=="update" and is_granted('DELETE', project) %} + Supprimer + {% endif %} {% include('include/error.html.twig') %} @@ -49,6 +41,34 @@ {% if mode=="update" %}
+
+
Options de Vote
+
+ Ajouter + +
+ + + + + + + + + {% for option in project.options %} + + + + + {% endfor %} + +
ActionTitle
+ + {{option.title}}
+
+
+
+
Description du Projet
diff --git a/templates/project/status.html.twig b/templates/project/status.html.twig new file mode 100644 index 0000000..2fbb683 --- /dev/null +++ b/templates/project/status.html.twig @@ -0,0 +1,12 @@ + {% 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 6c87042..e6fe5bf 100644 --- a/templates/project/view.html.twig +++ b/templates/project/view.html.twig @@ -3,29 +3,44 @@ {% block title %} = {{title}}{% endblock %} {% block body %} -

{{title}}

+

+
{{title}}
+
{{project.status}}
+

+ {% if is_granted('UPDATE', project) %} + Modifier + {% endif %} - Annuler + Retour {% if is_granted('MOVEDRAFT', project) %} - Mettre en Brouillon + Mettre en Brouillon {% endif %} {% if is_granted('MOVETOVOTE', project) %} - Mettre au Vote + Mettre au Vote + {% endif %} + {% if is_granted('MOVEVOTED', project) %} + Voter {% endif %} {% if is_granted('DELETE', project) %} Supprimer {% endif %} - +
Information
- {{ project.title }} - {{ project.nature }} - {{ project.summary }} + Titre = {{ project.title }}
+ Nature = {{ project.nature }}

+
+
+ +
+
Résumé
+
+ {{ project.summary|nl2br }}
@@ -43,7 +58,7 @@
Description du Projet
- {{ project.description }} + {{ project.description ? project.description|markdown_to_html : "" }}