Compare commits
6 Commits
21230d6ca5
...
symfo
Author | SHA1 | Date | |
---|---|---|---|
589a2eacb9 | |||
bcdf788be3 | |||
92976d8496 | |||
3ad0b8f15e | |||
91022d2037 | |||
b7b07e5abf |
@@ -7,8 +7,7 @@ node_modules/
|
||||
.env.*.local
|
||||
|
||||
# Cache et logs Symfony
|
||||
var/cache/
|
||||
var/log/
|
||||
var
|
||||
|
||||
# Build front-end
|
||||
public/build/
|
||||
|
@@ -40,6 +40,7 @@
|
||||
"symfony/property-info": "^7.2",
|
||||
"symfony/runtime": "^7.2",
|
||||
"symfony/security-bundle": "^7.2",
|
||||
"symfony/security-csrf": "^7.2",
|
||||
"symfony/serializer": "^7.2",
|
||||
"symfony/stimulus-bundle": "^2.21",
|
||||
"symfony/string": "^7.2",
|
||||
|
2
composer.lock
generated
2
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "67e324518270930150d990299cd0b613",
|
||||
"content-hash": "31c3ee9a06365c5a9df6f2ed45712eb3",
|
||||
"packages": [
|
||||
{
|
||||
"name": "apereo/phpcas",
|
||||
|
@@ -1,10 +1,14 @@
|
||||
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||
framework:
|
||||
secret: '%env(APP_SECRET)%'
|
||||
#csrf_protection: true
|
||||
csrf_protection: true
|
||||
|
||||
# Note that the session will be started ONLY if you read or write from it.
|
||||
session: true
|
||||
session:
|
||||
enabled: true
|
||||
handler_id: null
|
||||
cookie_secure: auto
|
||||
|
||||
|
||||
#esi: true
|
||||
#fragments: true
|
||||
|
@@ -22,7 +22,9 @@ security:
|
||||
form_login:
|
||||
login_path: app_login
|
||||
check_path: app_login
|
||||
enable_csrf: true
|
||||
enable_csrf: false
|
||||
csrf_token_id: authenticate
|
||||
csrf_parameter: _csrf_token
|
||||
default_target_path: /
|
||||
logout:
|
||||
path: app_logout
|
||||
|
@@ -11,9 +11,10 @@ COPY ./misc/docker/apache.conf /etc/apache2/conf.d/nine/site.conf
|
||||
WORKDIR /app
|
||||
|
||||
# Crée vendor à l’avance et donne les droits
|
||||
RUN mkdir -p /app/vendor && chown -R apache:apache /app
|
||||
RUN mkdir -p /app/vendor && mkdir -p /app/var && chown -R apache:apache /app
|
||||
|
||||
USER apache
|
||||
WORKDIR /app
|
||||
COPY --chown=apache:apache . .
|
||||
|
||||
RUN composer install --no-interaction
|
||||
|
@@ -7,6 +7,8 @@ cd ${DIR}
|
||||
cd ../..
|
||||
DIR=$(pwd)
|
||||
|
||||
bin/console cache:clear --env=dev
|
||||
bin/console cache:clear --env=prod
|
||||
bin/console d:s:u --force --complete
|
||||
bin/console app:init
|
||||
|
||||
|
201
src/Controller/GroupController.php
Normal file
201
src/Controller/GroupController.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Group;
|
||||
use App\Form\GroupType;
|
||||
use App\Repository\GroupRepository;
|
||||
use App\Security\GroupVoter;
|
||||
use Bnine\FilesBundle\Service\FileService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class GroupController extends AbstractController
|
||||
{
|
||||
private FileService $fileService;
|
||||
|
||||
public function __construct(FileService $fileService)
|
||||
{
|
||||
$this->fileService = $fileService;
|
||||
}
|
||||
|
||||
#[Route('/admin/group', name: 'app_admin_group')]
|
||||
public function list(GroupRepository $groupRepository): Response
|
||||
{
|
||||
$groups = $groupRepository->findAll();
|
||||
|
||||
return $this->render('group/list.html.twig', [
|
||||
'usemenu' => true,
|
||||
'usesidebar' => true,
|
||||
'title' => 'Liste des Groupes',
|
||||
'routesubmit' => 'app_admin_group_submit',
|
||||
'routeupdate' => 'app_admin_group_update',
|
||||
'groups' => $groups,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/group/submit', name: 'app_admin_group_submit')]
|
||||
#[Route('/user/group/submit', name: 'app_user_group_submit')]
|
||||
public function submit(Request $request, EntityManagerInterface $em): Response
|
||||
{
|
||||
$group = new Group();
|
||||
$group->addUser($this->getUser());
|
||||
$group->setStatus(Group::ACTIVE);
|
||||
$group->setOpen(true);
|
||||
|
||||
$this->denyAccessUnlessGranted(GroupVoter::SUBMIT, $group);
|
||||
|
||||
$isAdmin = str_starts_with($request->attributes->get('_route'), 'app_admin');
|
||||
$form = $this->createForm(GroupType::class, $group, ['mode' => 'submit']);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$em->persist($group);
|
||||
$em->flush();
|
||||
|
||||
$this->fileService->init('group', $group->getId());
|
||||
|
||||
return $this->redirectToRoute($isAdmin ? 'app_admin_group_update' : 'app_user_group_update', ['id' => $group->getId()]);
|
||||
}
|
||||
|
||||
return $this->render('group/edit.html.twig', [
|
||||
'usemenu' => true,
|
||||
'usesidebar' => $isAdmin,
|
||||
'title' => 'Création Groupe',
|
||||
'routecancel' => $isAdmin ? 'app_admin_group' : 'app_home',
|
||||
'routedelete' => $isAdmin ? 'app_admin_group_delete' : 'app_user_group_delete',
|
||||
'mode' => 'submit',
|
||||
'form' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/group/update/{id}', name: 'app_admin_group_update')]
|
||||
#[Route('/user/group/update/{id}', name: 'app_user_group_update')]
|
||||
public function update(int $id, Request $request, GroupRepository $groupRepository, EntityManagerInterface $em): Response
|
||||
{
|
||||
$group = $groupRepository->find($id);
|
||||
if (!$group) {
|
||||
throw new NotFoundHttpException('La ressource demandée est introuvable.');
|
||||
}
|
||||
|
||||
$this->denyAccessUnlessGranted(GroupVoter::UPDATE, $group);
|
||||
|
||||
$isAdmin = str_starts_with($request->attributes->get('_route'), 'app_admin');
|
||||
$form = $this->createForm(GroupType::class, $group, ['mode' => 'update']);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$em->flush();
|
||||
|
||||
if ($isAdmin) {
|
||||
return $this->redirectToRoute('app_admin_group');
|
||||
} else {
|
||||
return $this->redirectToRoute('app_user_group_view', ['id' => $group->getId()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('group/edit.html.twig', [
|
||||
'usemenu' => true,
|
||||
'usesidebar' => $isAdmin,
|
||||
'title' => 'Modification Groupe = '.$group->getTitle(),
|
||||
'routecancel' => $isAdmin ? 'app_admin_group' : 'app_home',
|
||||
'routemove' => $isAdmin ? 'app_admin_group_move' : 'app_user_group_move',
|
||||
'routedelete' => $isAdmin ? 'app_admin_group_delete' : 'app_user_group_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,
|
||||
'group' => $group,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/group/moveto/{id}/{status}', name: 'app_admin_group_move')]
|
||||
#[Route('/user/group/moveto/{id}/{status}', name: 'app_user_group_move')]
|
||||
public function move(int $id, string $status, Request $request, GroupRepository $groupRepository, EntityManagerInterface $em): Response
|
||||
{
|
||||
$group = $groupRepository->find($id);
|
||||
if (!$group) {
|
||||
throw new NotFoundHttpException('La ressource demandée est introuvable.');
|
||||
}
|
||||
|
||||
$attribute = constant(GroupVoter::class.'::MOVETO'.$status);
|
||||
$this->denyAccessUnlessGranted($attribute, $group);
|
||||
|
||||
$group->setStatus(constant(Group::class.'::'.$status));
|
||||
$em->flush();
|
||||
|
||||
$isAdmin = str_starts_with($request->attributes->get('_route'), 'app_admin');
|
||||
|
||||
return $this->redirectToRoute($isAdmin ? 'app_admin_group' : 'app_user_group_view', ['id' => $group->getId()]);
|
||||
}
|
||||
|
||||
#[Route('/admin/group/subscribe/{id}', name: 'app_admin_group_subscribe')]
|
||||
#[Route('/user/group/subscribe/{id}', name: 'app_user_group_subscribe')]
|
||||
public function subscribe(int $id, Request $request, GroupRepository $groupRepository, EntityManagerInterface $em): Response
|
||||
{
|
||||
$group = $groupRepository->find($id);
|
||||
if (!$group) {
|
||||
throw new NotFoundHttpException('La ressource demandée est introuvable.');
|
||||
}
|
||||
|
||||
$this->denyAccessUnlessGranted(GroupVoter::CANSUBSCRIBE, $group);
|
||||
|
||||
$group->addUser($this->getUser());
|
||||
$em->flush();
|
||||
|
||||
$isAdmin = str_starts_with($request->attributes->get('_route'), 'app_admin');
|
||||
|
||||
return $this->redirectToRoute($isAdmin ? 'app_admin_group' : 'app_user_group_view', ['id' => $group->getId()]);
|
||||
}
|
||||
|
||||
#[Route('/admin/group/delete/{id}', name: 'app_admin_group_delete')]
|
||||
#[Route('/user/group/delete/{id}', name: 'app_user_group_delete')]
|
||||
public function delete(int $id, Request $request, GroupRepository $groupRepository, EntityManagerInterface $em): Response
|
||||
{
|
||||
$group = $groupRepository->find($id);
|
||||
if (!$group) {
|
||||
throw new NotFoundHttpException('La ressource demandée est introuvable.');
|
||||
}
|
||||
|
||||
$this->denyAccessUnlessGranted(GroupVoter::DELETE, $group);
|
||||
|
||||
$isAdmin = str_starts_with($request->attributes->get('_route'), 'app_admin');
|
||||
|
||||
// Tentative de suppression
|
||||
try {
|
||||
$em->remove($group);
|
||||
$em->flush();
|
||||
} catch (\Exception $e) {
|
||||
$this->addflash('error', $e->getMessage());
|
||||
}
|
||||
|
||||
return $this->redirectToRoute($isAdmin ? 'app_admin_group' : 'app_home');
|
||||
}
|
||||
|
||||
#[Route('/user/group/view/{id}', name: 'app_user_group_view')]
|
||||
public function view(int $id, Request $request, GroupRepository $groupRepository, EntityManagerInterface $em): Response
|
||||
{
|
||||
$group = $groupRepository->find($id);
|
||||
if (!$group) {
|
||||
throw new NotFoundHttpException('La ressource demandée est introuvable.');
|
||||
}
|
||||
|
||||
$this->denyAccessUnlessGranted(GroupVoter::VIEW, $group);
|
||||
|
||||
$isAdmin = str_starts_with($request->attributes->get('_route'), 'app_admin');
|
||||
|
||||
return $this->render('group/view.html.twig', [
|
||||
'usemenu' => true,
|
||||
'usesidebar' => false,
|
||||
'title' => 'Group = '.$group->getTitle(),
|
||||
'routeupdate' => $isAdmin ? 'app_admin_group_update' : 'app_user_group_update',
|
||||
'routecancel' => $isAdmin ? 'app_admin_group' : 'app_home',
|
||||
'routedelete' => $isAdmin ? 'app_admin_group_delete' : 'app_user_group_delete',
|
||||
'routemove' => $isAdmin ? 'app_admin_group_move' : 'app_user_group_move',
|
||||
'routesubscribe' => $isAdmin ? 'app_admin_group_subscribe' : 'app_user_group_subscribe',
|
||||
'group' => $group,
|
||||
]);
|
||||
}
|
||||
}
|
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Repository\GroupRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Service\EtherpadService;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
@@ -11,14 +12,16 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||
class HomeController extends AbstractController
|
||||
{
|
||||
#[Route('/', name: 'app_home')]
|
||||
public function home(ProjectRepository $projectRepository): Response
|
||||
public function home(ProjectRepository $projectRepository, GroupRepository $groupRepository): Response
|
||||
{
|
||||
$projects = $projectRepository->findAll();
|
||||
$groups = $groupRepository->findAll();
|
||||
|
||||
return $this->render('home/home.html.twig', [
|
||||
'usemenu' => true,
|
||||
'usesidebar' => false,
|
||||
'projects' => $projects,
|
||||
'groups' => $groups,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -35,14 +38,7 @@ class HomeController extends AbstractController
|
||||
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;
|
||||
return $this->redirect($padAccess['iframeUrl']);
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\ProjectOption;
|
||||
use App\Form\ProjectType;
|
||||
use App\Form\ProjectVotedType;
|
||||
use App\Repository\ProjectRepository;
|
||||
@@ -46,6 +47,7 @@ class ProjectController extends AbstractController
|
||||
$project = new Project();
|
||||
$project->addUser($this->getUser());
|
||||
$project->setStatus(Project::DRAFT);
|
||||
$project->setOpen(true);
|
||||
|
||||
$this->denyAccessUnlessGranted(ProjectVoter::SUBMIT, $project);
|
||||
|
||||
@@ -55,6 +57,20 @@ class ProjectController extends AbstractController
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$em->persist($project);
|
||||
$em->flush();
|
||||
|
||||
// Création d'option par défaut
|
||||
$option = new ProjectOption();
|
||||
$option->setProject($project);
|
||||
$option->setTitle('Pour');
|
||||
$em->persist($option);
|
||||
$em->flush();
|
||||
|
||||
$option = new ProjectOption();
|
||||
$option->setProject($project);
|
||||
$option->setTitle('Contre');
|
||||
$em->persist($option);
|
||||
$em->flush();
|
||||
|
||||
$this->fileService->init('project', $project->getId());
|
||||
|
||||
return $this->redirectToRoute($isAdmin ? 'app_admin_project_update' : 'app_user_project_update', ['id' => $project->getId()]);
|
||||
@@ -146,6 +162,25 @@ class ProjectController extends AbstractController
|
||||
return $this->redirectToRoute($isAdmin ? 'app_admin_project' : 'app_user_project_view', ['id' => $project->getId()]);
|
||||
}
|
||||
|
||||
#[Route('/admin/project/subscribe/{id}', name: 'app_admin_project_subscribe')]
|
||||
#[Route('/user/project/subscribe/{id}', name: 'app_user_project_subscribe')]
|
||||
public function subscribe(int $id, Request $request, ProjectRepository $projectRepository, EntityManagerInterface $em): Response
|
||||
{
|
||||
$project = $projectRepository->find($id);
|
||||
if (!$project) {
|
||||
throw new NotFoundHttpException('La ressource demandée est introuvable.');
|
||||
}
|
||||
|
||||
$this->denyAccessUnlessGranted(ProjectVoter::CANSUBSCRIBE, $project);
|
||||
|
||||
$project->addUser($this->getUser());
|
||||
$em->flush();
|
||||
|
||||
$isAdmin = str_starts_with($request->attributes->get('_route'), 'app_admin');
|
||||
|
||||
return $this->redirectToRoute($isAdmin ? 'app_admin_project' : 'app_user_project_view', ['id' => $project->getId()]);
|
||||
}
|
||||
|
||||
#[Route('/admin/project/delete/{id}', name: 'app_admin_project_delete')]
|
||||
#[Route('/user/project/delete/{id}', name: 'app_user_project_delete')]
|
||||
public function delete(int $id, Request $request, ProjectRepository $projectRepository, EntityManagerInterface $em): Response
|
||||
@@ -190,6 +225,7 @@ class ProjectController extends AbstractController
|
||||
'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',
|
||||
'routesubscribe' => $isAdmin ? 'app_admin_project_subscribe' : 'app_user_project_subscribe',
|
||||
'project' => $project,
|
||||
]);
|
||||
}
|
||||
|
@@ -2,14 +2,12 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use App\Form\UserType;
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
@@ -162,30 +160,4 @@ class UserController extends AbstractController
|
||||
'form' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/user/selectproject', name: 'app_user_selectproject')]
|
||||
public function selectproject(Request $request, EntityManagerInterface $em): JsonResponse
|
||||
{
|
||||
$id = $request->get('id');
|
||||
|
||||
$project = $em->getRepository(Project::class)->find($id);
|
||||
if (!$project) {
|
||||
return new JsonResponse(['status' => 'KO', 'message' => 'ID non fourni'], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$user = $this->getUser();
|
||||
if (!$user instanceof User) {
|
||||
throw new \LogicException('L\'utilisateur actuel n\'est pas une instance de App\Entity\User.');
|
||||
}
|
||||
|
||||
$projects = $user->getProjects();
|
||||
if (!$projects->contains($project)) {
|
||||
return new JsonResponse(['status' => 'KO', 'message' => 'Projet non autorisée'], Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
|
||||
$user->setProject($project);
|
||||
$em->flush();
|
||||
|
||||
return new JsonResponse(['status' => 'OK', 'message' => 'Projet selectionnée'], Response::HTTP_OK);
|
||||
}
|
||||
}
|
||||
|
165
src/Entity/Group.php
Normal file
165
src/Entity/Group.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\GroupRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: GroupRepository::class)]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ORM\Table(name: 'groupe')]
|
||||
class Group
|
||||
{
|
||||
public const ACTIVE = 'Actif';
|
||||
public const INACTIVE = 'Inactif';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $title = null;
|
||||
|
||||
#[ORM\Column(type: 'text')]
|
||||
private string $summary;
|
||||
|
||||
#[ORM\Column(type: 'text', nullable: true)]
|
||||
private ?string $description = null;
|
||||
|
||||
#[ORM\Column(type: 'boolean', nullable: false)]
|
||||
private bool $open;
|
||||
|
||||
#[ORM\Column]
|
||||
private string $status;
|
||||
|
||||
#[ORM\Column(type: 'datetime', nullable: true)]
|
||||
private ?\DateTimeInterface $dueDate = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, User>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: User::class, mappedBy: 'groups')]
|
||||
private Collection $users;
|
||||
|
||||
/**
|
||||
* @var Collection<int, GroupTimeline>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'group', targetEntity: GroupTimeline::class, cascade: ['remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['createdAt' => 'DESC'])]
|
||||
private Collection $timelines;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->users = new ArrayCollection();
|
||||
$this->timelines = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle(string $title): static
|
||||
{
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSummary(): string
|
||||
{
|
||||
return $this->summary;
|
||||
}
|
||||
|
||||
public function setSummary(string $summary): static
|
||||
{
|
||||
$this->summary = $summary;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setDescription(?string $description): static
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isOpen(): bool
|
||||
{
|
||||
return $this->open;
|
||||
}
|
||||
|
||||
public function setOpen(bool $open): static
|
||||
{
|
||||
$this->open = $open;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatus(): string
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(string $status): static
|
||||
{
|
||||
$this->status = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDueDate(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->dueDate;
|
||||
}
|
||||
|
||||
public function setDueDate(?\DateTimeInterface $dueDate): static
|
||||
{
|
||||
$this->dueDate = $dueDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUsers(): Collection
|
||||
{
|
||||
return $this->users;
|
||||
}
|
||||
|
||||
public function addUser(User $user): static
|
||||
{
|
||||
if (!$this->users->contains($user)) {
|
||||
$this->users->add($user);
|
||||
$user->addGroup($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeUser(User $user): static
|
||||
{
|
||||
if ($this->users->removeElement($user)) {
|
||||
$user->removeGroup($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTimelines(): Collection
|
||||
{
|
||||
return $this->timelines;
|
||||
}
|
||||
}
|
86
src/Entity/GroupTimeline.php
Normal file
86
src/Entity/GroupTimeline.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\GroupTimelineRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: GroupTimelineRepository::class)]
|
||||
class GroupTimeline
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Group::class, inversedBy: 'timelines')]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
private Group $group;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
private User $user;
|
||||
|
||||
#[ORM\Column(type: 'datetime')]
|
||||
private \DateTimeInterface $createdAt;
|
||||
|
||||
#[ORM\Column(type: 'array')]
|
||||
private array $description;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getGroup(): Group
|
||||
{
|
||||
return $this->group;
|
||||
}
|
||||
|
||||
public function setGroup(Group $group): static
|
||||
{
|
||||
$this->group = $group;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(User $user): static
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): \DateTimeInterface
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function setCreatedAt(\DateTimeInterface $createAt): static
|
||||
{
|
||||
$this->createdAt = $createAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDescription(): array
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setDescription(array $description): static
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@@ -36,6 +36,9 @@ class Project
|
||||
#[ORM\Column(type: 'string', length: 20)]
|
||||
private string $nature;
|
||||
|
||||
#[ORM\Column(type: 'boolean', nullable: false)]
|
||||
private bool $open;
|
||||
|
||||
#[ORM\Column]
|
||||
private string $status;
|
||||
|
||||
@@ -131,6 +134,18 @@ class Project
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isOpen(): bool
|
||||
{
|
||||
return $this->open;
|
||||
}
|
||||
|
||||
public function setOpen(bool $open): static
|
||||
{
|
||||
$this->open = $open;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatus(): string
|
||||
{
|
||||
return $this->status;
|
||||
|
@@ -45,13 +45,13 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
#[ORM\ManyToMany(targetEntity: Project::class, inversedBy: 'users')]
|
||||
private ?Collection $projects;
|
||||
|
||||
#[ORM\ManyToOne()]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
private ?Project $project = null;
|
||||
#[ORM\ManyToMany(targetEntity: Group::class, inversedBy: 'users')]
|
||||
private ?Collection $groups;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->projects = new ArrayCollection();
|
||||
$this->groups = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -71,21 +71,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A visual identifier that represents this user.
|
||||
*
|
||||
* @see UserInterface
|
||||
*/
|
||||
public function getUserIdentifier(): string
|
||||
{
|
||||
return (string) $this->username;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see UserInterface
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getRoles(): array
|
||||
{
|
||||
$roles = $this->roles;
|
||||
@@ -95,9 +85,6 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
return array_unique($roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $roles
|
||||
*/
|
||||
public function setRoles(array $roles): static
|
||||
{
|
||||
$this->roles = $roles;
|
||||
@@ -125,9 +112,6 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see UserInterface
|
||||
*/
|
||||
public function eraseCredentials(): void
|
||||
{
|
||||
// If you store any temporary, sensitive data on the user, clear it here
|
||||
@@ -194,18 +178,26 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProject(): ?Project
|
||||
/**
|
||||
* @return Collection<int, Group>
|
||||
*/
|
||||
public function getGroups(): ?Collection
|
||||
{
|
||||
if (!$this->projects) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->project;
|
||||
return $this->groups;
|
||||
}
|
||||
|
||||
public function setProject(?Project $project): static
|
||||
public function addGroup(Group $group): static
|
||||
{
|
||||
$this->project = $project;
|
||||
if (!$this->groups->contains($group)) {
|
||||
$this->groups->add($group);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeGroup(Group $group): static
|
||||
{
|
||||
$this->groups->removeElement($group);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
155
src/EventListener/GroupListener.php
Normal file
155
src/EventListener/GroupListener.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use App\Entity\Group;
|
||||
use App\Entity\GroupTimeline;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
#[AsDoctrineListener(event: 'onFlush')]
|
||||
class GroupListener
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
) {
|
||||
}
|
||||
|
||||
public function onFlush(OnFlushEventArgs $args): void
|
||||
{
|
||||
$em = $args->getObjectManager();
|
||||
if (!$em instanceof EntityManagerInterface) {
|
||||
return;
|
||||
}
|
||||
$uow = $em->getUnitOfWork();
|
||||
|
||||
$user = $this->security->getUser();
|
||||
if (!$user) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||
if (!$entity instanceof Group) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$timeline = new GroupTimeline();
|
||||
$timeline->setGroup($entity);
|
||||
$timeline->setUser($user);
|
||||
$timeline->setCreatedAt(new \DateTime());
|
||||
$timeline->setDescription(['created' => true]);
|
||||
|
||||
$em->persist($timeline);
|
||||
$meta = $em->getClassMetadata(GroupTimeline::class);
|
||||
$uow->computeChangeSet($meta, $timeline);
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||
if (!$entity instanceof Group) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$changeSet = $uow->getEntityChangeSet($entity);
|
||||
$changes = [];
|
||||
|
||||
foreach ($changeSet as $field => [$old, $new]) {
|
||||
$oldStr = $this->stringify($old);
|
||||
$newStr = $this->stringify($new);
|
||||
|
||||
if ($oldStr !== $newStr) {
|
||||
$changes[$field] = [
|
||||
'old' => $oldStr,
|
||||
'new' => $newStr,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($changes)) {
|
||||
$timeline = new GroupTimeline();
|
||||
$timeline->setGroup($entity);
|
||||
$timeline->setUser($user);
|
||||
$timeline->setCreatedAt(new \DateTime());
|
||||
$timeline->setDescription($changes);
|
||||
|
||||
$em->persist($timeline);
|
||||
$meta = $em->getClassMetadata(GroupTimeline::class);
|
||||
$uow->computeChangeSet($meta, $timeline);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledCollectionUpdates() as $col) {
|
||||
$owner = $col->getOwner();
|
||||
|
||||
if (!$owner instanceof Group) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mapping = $col->getMapping();
|
||||
$fieldName = $mapping['fieldName'];
|
||||
|
||||
$added = $col->getInsertDiff();
|
||||
$removed = $col->getDeleteDiff();
|
||||
|
||||
$changes = [];
|
||||
|
||||
foreach ($added as $addedEntity) {
|
||||
$changes[$fieldName.'_added'][] = $this->stringify($addedEntity);
|
||||
}
|
||||
|
||||
foreach ($removed as $removedEntity) {
|
||||
$changes[$fieldName.'_removed'][] = $this->stringify($removedEntity);
|
||||
}
|
||||
|
||||
if (!empty($changes)) {
|
||||
$timeline = new GroupTimeline();
|
||||
$timeline->setGroup($owner);
|
||||
$timeline->setUser($user);
|
||||
$timeline->setCreatedAt(new \DateTime());
|
||||
$timeline->setDescription($changes);
|
||||
|
||||
$em->persist($timeline);
|
||||
$meta = $em->getClassMetadata(GroupTimeline::class);
|
||||
$uow->computeChangeSet($meta, $timeline);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function stringify(mixed $value): string
|
||||
{
|
||||
if (is_null($value)) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
if (is_scalar($value)) {
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
if ($value instanceof Collection || is_array($value)) {
|
||||
$elements = [];
|
||||
foreach ($value as $item) {
|
||||
$elements[] = $this->stringify($item);
|
||||
}
|
||||
|
||||
return '['.implode(', ', $elements).']';
|
||||
}
|
||||
|
||||
if (is_object($value)) {
|
||||
if (method_exists($value, '__toString')) {
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
foreach (['getUsername', 'getName', 'getTitle', 'getEmail', 'getId'] as $method) {
|
||||
if (method_exists($value, $method)) {
|
||||
return (string) $value->{$method}();
|
||||
}
|
||||
}
|
||||
|
||||
return get_class($value);
|
||||
}
|
||||
|
||||
return json_encode($value);
|
||||
}
|
||||
}
|
76
src/Form/GroupType.php
Normal file
76
src/Form/GroupType.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Group;
|
||||
use App\Entity\User;
|
||||
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\CheckboxType;
|
||||
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;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class GroupType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('submit', SubmitType::class, [
|
||||
'label' => 'Valider',
|
||||
'attr' => ['class' => 'btn btn-success no-print me-1'],
|
||||
])
|
||||
|
||||
->add('title', TextType::class, [
|
||||
'label' => 'Titre',
|
||||
])
|
||||
|
||||
->add('dueDate', DateType::class, [
|
||||
'label' => 'A Voter pour le',
|
||||
'required' => false,
|
||||
'html5' => true,
|
||||
])
|
||||
|
||||
->add('summary', TextareaType::class, [
|
||||
'label' => 'Résumé',
|
||||
])
|
||||
|
||||
->add('open', CheckboxType::class, [
|
||||
'label' => 'Groupe Ouvert',
|
||||
'required' => false,
|
||||
])
|
||||
|
||||
->add('users', EntityType::class, [
|
||||
'label' => 'Paticipants',
|
||||
'class' => User::class,
|
||||
'choice_label' => 'username',
|
||||
'multiple' => true,
|
||||
'attr' => ['class' => 'select2'],
|
||||
'required' => false,
|
||||
'by_reference' => false,
|
||||
]);
|
||||
|
||||
if ('update' == $options['mode']) {
|
||||
$builder
|
||||
->add('description', MarkdownType::class, [
|
||||
'label' => 'Description du Projet',
|
||||
'markdown_height' => 900,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Group::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => static::class,
|
||||
'mode' => 'submit',
|
||||
]);
|
||||
}
|
||||
}
|
@@ -39,6 +39,9 @@ class OptionType extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ProjectOption::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => static::class,
|
||||
'mode' => 'submit',
|
||||
]);
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ use App\Entity\User;
|
||||
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\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
@@ -39,6 +40,11 @@ class ProjectType extends AbstractType
|
||||
'label' => 'Résumé',
|
||||
])
|
||||
|
||||
->add('open', CheckboxType::class, [
|
||||
'label' => 'Projet Ouvert',
|
||||
'required' => false,
|
||||
])
|
||||
|
||||
->add('nature', ChoiceType::class, [
|
||||
'label' => 'Nature',
|
||||
'choices' => [
|
||||
@@ -49,7 +55,7 @@ class ProjectType extends AbstractType
|
||||
])
|
||||
|
||||
->add('users', EntityType::class, [
|
||||
'label' => 'Propriétaires',
|
||||
'label' => 'Paticipants',
|
||||
'class' => User::class,
|
||||
'choice_label' => 'username',
|
||||
'multiple' => true,
|
||||
@@ -71,6 +77,9 @@ class ProjectType extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Project::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => static::class,
|
||||
'mode' => 'submit',
|
||||
]);
|
||||
}
|
||||
|
@@ -54,6 +54,9 @@ class ProjectVotedType extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Project::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => static::class,
|
||||
'mode' => 'submit',
|
||||
]);
|
||||
}
|
||||
|
@@ -31,11 +31,6 @@ class UserType extends AbstractType
|
||||
'label' => 'Login',
|
||||
])
|
||||
|
||||
->add('apikey', TextType::class, [
|
||||
'label' => 'apikey',
|
||||
'required' => false,
|
||||
])
|
||||
|
||||
->add('avatar', HiddenType::class)
|
||||
|
||||
->add('email', EmailType::class, [
|
||||
@@ -56,6 +51,7 @@ class UserType extends AbstractType
|
||||
'choice_label' => 'title',
|
||||
'multiple' => true,
|
||||
'attr' => ['class' => 'select2'],
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -81,6 +77,9 @@ class UserType extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => User::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => static::class,
|
||||
'mode' => 'submit',
|
||||
'modeAuth' => 'SQL',
|
||||
]);
|
||||
|
18
src/Repository/GroupRepository.php
Normal file
18
src/Repository/GroupRepository.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Group;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Group>
|
||||
*/
|
||||
class GroupRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Group::class);
|
||||
}
|
||||
}
|
18
src/Repository/GroupTimelineRepository.php
Normal file
18
src/Repository/GroupTimelineRepository.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\GroupTimeline;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<GroupTimeline>
|
||||
*/
|
||||
class GroupTimelineRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, GroupTimeline::class);
|
||||
}
|
||||
}
|
@@ -104,11 +104,11 @@ class DynamicAuthenticator extends AbstractAuthenticator
|
||||
|
||||
// \phpCAS::setDebug('/tmp/logcas.log');
|
||||
\phpCAS::client(
|
||||
CAS_VERSION_2_0,
|
||||
$this->parameterBag->get('casHost'),
|
||||
(int) $this->parameterBag->get('casPort'),
|
||||
$this->parameterBag->get('casPath'),
|
||||
$url,
|
||||
CAS_VERSION_2_0,
|
||||
$this->parameterBag->get('casHost'),
|
||||
(int) $this->parameterBag->get('casPort'),
|
||||
$this->parameterBag->get('casPath'),
|
||||
$url,
|
||||
false);
|
||||
|
||||
\phpCAS::setNoCasServerValidation();
|
||||
|
@@ -3,6 +3,7 @@
|
||||
namespace App\Security;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\GroupRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use Bnine\FilesBundle\Security\AbstractFileVoter;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
@@ -10,8 +11,9 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
class FileVoter extends AbstractFileVoter
|
||||
{
|
||||
private ProjectRepository $projectRepository;
|
||||
private GroupRepository $groupRepository;
|
||||
|
||||
public function __construct(ProjectRepository $projectRepository)
|
||||
public function __construct(ProjectRepository $projectRepository, GroupRepository $groupRepository)
|
||||
{
|
||||
$this->projectRepository = $projectRepository;
|
||||
}
|
||||
@@ -43,6 +45,12 @@ class FileVoter extends AbstractFileVoter
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 'group':
|
||||
$group = $this->groupRepository->find($id);
|
||||
if ($group && $group->getUsers()->contains($user)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -65,6 +73,12 @@ class FileVoter extends AbstractFileVoter
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 'group':
|
||||
$group = $this->groupRepository->find($id);
|
||||
if ($group && $group->getUsers()->contains($user)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
105
src/Security/GroupVoter.php
Normal file
105
src/Security/GroupVoter.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
use App\Entity\Group;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
|
||||
class GroupVoter extends Voter
|
||||
{
|
||||
// Les actions que ce voter supporte
|
||||
public const VIEW = 'VIEW';
|
||||
public const SUBMIT = 'SUBMIT';
|
||||
public const UPDATE = 'UPDATE';
|
||||
public const DELETE = 'DELETE';
|
||||
public const MOVETOACTIVE = 'MOVETOACTIVE';
|
||||
public const MOVETOINACTIVE = 'MOVETOINACTIVE';
|
||||
public const TOME = 'TOME';
|
||||
public const CANSUBSCRIBE = 'CANSUBSCRIBE';
|
||||
|
||||
protected function supports(string $attribute, $subject): bool
|
||||
{
|
||||
$attributes = [self::VIEW, self::SUBMIT, self::UPDATE, self::DELETE, self::MOVETOACTIVE, self::MOVETOINACTIVE, self::TOME, self::CANSUBSCRIBE];
|
||||
|
||||
return in_array($attribute, $attributes) && $subject instanceof Group;
|
||||
}
|
||||
|
||||
private function canView(Group $group, User $user): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
private function canSubmit(Group $group, User $user): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
private function canUpdate(Group $group, User $user): bool
|
||||
{
|
||||
$hasMaster = $user->hasRole('ROLE_ADMIN') || $user->hasRole('ROLE_MASTER');
|
||||
$hasUser = $group->getUsers()->contains($user);
|
||||
$hasStatus = Group::ACTIVE === $group->getStatus();
|
||||
|
||||
return $hasMaster || ($hasUser && $hasStatus);
|
||||
}
|
||||
|
||||
private function canDelete(Group $group, User $user): bool
|
||||
{
|
||||
$hasMaster = $user->hasRole('ROLE_ADMIN') || $user->hasRole('ROLE_MASTER');
|
||||
$hasUser = $group->getUsers()->contains($user);
|
||||
$hasStatus = Group::ACTIVE === $group->getStatus();
|
||||
|
||||
return $hasMaster || ($hasUser && $hasStatus);
|
||||
}
|
||||
|
||||
private function canMoveToInactive(Group $group, User $user): bool
|
||||
{
|
||||
$hasUser = $user->hasRole('ROLE_ADMIN') || $user->hasRole('ROLE_MASTER') || $group->getUsers()->contains($user);
|
||||
$hasStatus = Group::ACTIVE === $group->getStatus();
|
||||
dump($hasUser);
|
||||
dump($hasStatus);
|
||||
|
||||
return $hasUser && $hasStatus;
|
||||
}
|
||||
|
||||
private function canMoveToActive(Group $group, User $user): bool
|
||||
{
|
||||
$hasUser = $user->hasRole('ROLE_ADMIN') || $user->hasRole('ROLE_MASTER') || $group->getUsers()->contains($user);
|
||||
$hasStatus = Group::INACTIVE === $group->getStatus();
|
||||
|
||||
return $hasUser && $hasStatus;
|
||||
}
|
||||
|
||||
private function toMe(Group $group, User $user): bool
|
||||
{
|
||||
return $group->getUsers()->contains($user);
|
||||
}
|
||||
|
||||
private function canSubscribe(Group $group, User $user): bool
|
||||
{
|
||||
return $group->isOpen() && !$group->getUsers()->contains($user);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, $group, TokenInterface $token): bool
|
||||
{
|
||||
$user = $token->getUser();
|
||||
if (!$user instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return match ($attribute) {
|
||||
self::VIEW => $this->canView($group, $user),
|
||||
self::SUBMIT => $this->canSubmit($group, $user),
|
||||
self::UPDATE => $this->canUpdate($group, $user),
|
||||
self::DELETE => $this->canDelete($group, $user),
|
||||
self::MOVETOACTIVE => $this->canMoveToActive($group, $user),
|
||||
self::MOVETOINACTIVE => $this->canMoveToInactive($group, $user),
|
||||
self::TOME => $this->toMe($group, $user),
|
||||
self::CANSUBSCRIBE => $this->canSubscribe($group, $user),
|
||||
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
}
|
@@ -19,10 +19,11 @@ class ProjectVoter extends Voter
|
||||
public const MOVEVOTED = 'MOVEVOTED';
|
||||
public const MOVEARCHIVED = 'MOVEARCHIVED';
|
||||
public const TOME = 'TOME';
|
||||
public const CANSUBSCRIBE = 'CANSUBSCRIBE';
|
||||
|
||||
protected function supports(string $attribute, $subject): bool
|
||||
{
|
||||
$attributes = [self::VIEW, self::SUBMIT, self::UPDATE, self::DELETE, self::MOVEDRAFT, self::MOVETOVOTE, self::MOVEVOTED, self::MOVEARCHIVED, self::TOME];
|
||||
$attributes = [self::VIEW, self::SUBMIT, self::UPDATE, self::DELETE, self::MOVEDRAFT, self::MOVETOVOTE, self::MOVEVOTED, self::MOVEARCHIVED, self::TOME, self::CANSUBSCRIBE];
|
||||
|
||||
return in_array($attribute, $attributes) && $subject instanceof Project;
|
||||
}
|
||||
@@ -97,6 +98,11 @@ class ProjectVoter extends Voter
|
||||
return $project->getUsers()->contains($user);
|
||||
}
|
||||
|
||||
private function canSubscribe(Project $project, User $user): bool
|
||||
{
|
||||
return $project->isOpen() && !$project->getUsers()->contains($user);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, $project, TokenInterface $token): bool
|
||||
{
|
||||
$user = $token->getUser();
|
||||
@@ -114,6 +120,7 @@ class ProjectVoter extends Voter
|
||||
self::MOVEVOTED => $this->canMoveVoted($project, $user),
|
||||
self::MOVEARCHIVED => $this->canMoveArchived($project, $user),
|
||||
self::TOME => $this->toMe($project, $user),
|
||||
self::CANSUBSCRIBE => $this->canSubscribe($project, $user),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
69
templates/group/edit.html.twig
Normal file
69
templates/group/edit.html.twig
Normal file
@@ -0,0 +1,69 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %} = {{title}}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1>{{title}}</h1>
|
||||
|
||||
{{ form_start(form) }}
|
||||
{{ form_widget(form.submit) }}
|
||||
<a href="{{ path(routecancel) }}" class="btn btn-secondary me-5">Annuler</a>
|
||||
{% if mode=="update" and is_granted('DELETE', group) %}
|
||||
<a href="{{ path(routedelete,{id:group.id}) }}" class="btn btn-danger float-end" onclick="return confirm('Confirmez-vous la suppression de cet enregistrement ?')">Supprimer</a>
|
||||
{% endif %}
|
||||
|
||||
{% include('include/error.html.twig') %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 mx-auto">
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">Information</div>
|
||||
<div class="card-body">
|
||||
{{ form_row(form.title) }}
|
||||
{{ form_row(form.open) }}
|
||||
{{ form_row(form.dueDate) }}
|
||||
{{ form_row(form.users) }}
|
||||
{{ form_row(form.summary) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if mode=="update" %}
|
||||
{{ render(path("bninefiles_files",{domain:'group',id:group.id, editable:1})) }}
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">Timeline</div>
|
||||
<div class="card-body">
|
||||
{% include('group/timeline.html.twig') %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if mode=="update" %}
|
||||
<div class="col-md-8 mx-auto">
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">Pad du Groupe</div>
|
||||
<div class="card-body">
|
||||
{{ form_widget(form.description) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block localscript %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$("#group_title").focus();
|
||||
|
||||
$('#dataTables').DataTable({
|
||||
columnDefs: [ { "targets": "no-sort", "orderable": false }, { "targets": "no-string", "type" : "num" } ],
|
||||
responsive: true,
|
||||
iDisplayLength: 100,
|
||||
order: [[ 1, "asc" ]]
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
44
templates/group/list.html.twig
Normal file
44
templates/group/list.html.twig
Normal file
@@ -0,0 +1,44 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %} = {{title}}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1>{{title}}</h1>
|
||||
<a href="{{ path(routesubmit) }}" class="btn btn-success">Ajouter</a>
|
||||
|
||||
<div class="dataTable_wrapper">
|
||||
<table class="table table-striped table-bordered table-hover" id="dataTables" style="width:100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="100px" class="no-sort">Action</th>
|
||||
<th>Title</th>
|
||||
<th width="200px">Modifié le</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for project in projects %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ path(routeupdate,{id:project.id}) }}" class="me-2"><i class="fas fa-file fa-2x"></i></a>
|
||||
</td>
|
||||
<td>{{project.title}}</td>
|
||||
<td>{{project.timelines.first ? project.timelines.first.createdAt|date('d/m/Y H:i') : '' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block localscript %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#dataTables').DataTable({
|
||||
columnDefs: [ { "targets": "no-sort", "orderable": false }, { "targets": "no-string", "type" : "num" } ],
|
||||
responsive: true,
|
||||
iDisplayLength: 100,
|
||||
order: [[ 2, "asc" ]]
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
57
templates/group/timeline.html.twig
Normal file
57
templates/group/timeline.html.twig
Normal file
@@ -0,0 +1,57 @@
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
|
||||
|
||||
<div class="timeline">
|
||||
{% for event in group.timelines %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-marker"></div>
|
||||
<div class="timeline-content">
|
||||
<p class="text-muted small mb-1">
|
||||
{{ event.createdAt|date('d/m/Y H:i') }} par <strong>{{ event.user.username }}</strong><br>
|
||||
{% for field, change in event.description %}
|
||||
<strong>{{ field }}</strong>
|
||||
{% if field=="status" %}
|
||||
<span class="text-danger">{{ change.old ?? '' }}</span>
|
||||
→
|
||||
<span class="text-success">{{ change.new ?? '' }}</span>
|
||||
{% endif %}
|
||||
|
||||
{% if not change.old is defined and change[0] is defined %}
|
||||
<span>{{ change[0] }}</span>
|
||||
{% endif %}
|
||||
<br>
|
||||
{% endfor %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.timeline {
|
||||
border-left: 3px solid #dee2e6;
|
||||
padding-left: 2rem;
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
}
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
margin-bottom: 10px;
|
||||
margin-left: -23px;
|
||||
}
|
||||
.timeline-content {
|
||||
line-height:14px;
|
||||
}
|
||||
.timeline-marker {
|
||||
position: absolute;
|
||||
left: -1.1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 50%;
|
||||
background: #0d6efd;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 0 0 3px #0d6efd44;
|
||||
}
|
||||
.timeline-content {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
</style>
|
89
templates/group/view.html.twig
Normal file
89
templates/group/view.html.twig
Normal file
@@ -0,0 +1,89 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %} = {{title}}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1 style="display:flex;">
|
||||
<div style="flex-grow:1">{{title}}</div>
|
||||
<div class="">{{group.status}}</div>
|
||||
</h1>
|
||||
|
||||
<div class="mb-3">
|
||||
{% if is_granted('UPDATE', group) %}
|
||||
<a href="{{ path(routeupdate,{id:group.id}) }}" class="btn btn-success me-1">Modifier</a>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ path(routecancel) }}" class="btn btn-secondary me-5">Retour</a>
|
||||
|
||||
{% if is_granted('CANSUBSCRIBE', group) %}
|
||||
<a href="{{ path(routesubscribe,{id:group.id}) }}" class="btn btn-secondary me-1" onclick="return confirm('Participer à ce groupe ?')">
|
||||
<i class="fas fa-users"></i> Participer à ce groupe
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if is_granted('MOVETOINACTIVE', group) and group.status=="Actif"%}
|
||||
<a href="{{ path(routemove,{id:group.id, status:"INACTIVE"}) }}" class="btn btn-primary me-1" onclick="return confirm('Statut = Inactif ?')">
|
||||
<i class="fas fa-angle-double-right"></i> Statut = Inactif
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if is_granted('DELETE', group) %}
|
||||
<a href="{{ path(routedelete,{id:group.id}) }}" class="btn btn-danger float-end" onclick="return confirm('Confirmez-vous la suppression de cet enregistrement ?')">
|
||||
Supprimer
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if is_granted('MOVETOACTIVE', group) and group.status=="Inactif" %}
|
||||
<a href="{{ path(routemove,{id:group.id, status:"ACTIVE"}) }}" class="btn btn-warning me-1 float-end" onclick="return confirm('Statut = Actif ?')">
|
||||
<i class="fas fa-angle-double-left"></i> Statut = Actif
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 mx-auto">
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">Information</div>
|
||||
<div class="card-body">
|
||||
<b>Titre</b> = {{ group.title }}<br>
|
||||
<b>Groupe Ouvert</b> = {{ group.open ? "Oui" : "Non" }}<br>
|
||||
<b>A Voter pour le</b> = {{ group.dueDate ? group.dueDate|date("d/m/Y"):"" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">Résumé</div>
|
||||
<div class="card-body">
|
||||
{{ group.summary|nl2br }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">Participants</div>
|
||||
<div class="card-body">
|
||||
{%for user in group.users%}
|
||||
{{loop.first ? user.username : ' - '~user.username}}
|
||||
{%endfor%}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ render(path("bninefiles_files",{domain:'group',id:group.id, editable:0})) }}
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">Timeline</div>
|
||||
<div class="card-body">
|
||||
{% include('group/timeline.html.twig') %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8 mx-auto">
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">Pad du Group</div>
|
||||
<div class="card-body">
|
||||
{{ group.description ? group.description|markdown_to_html|nl2br : "" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@@ -3,121 +3,179 @@
|
||||
|
||||
|
||||
{%block body%}
|
||||
<h2>Projets</h2>
|
||||
<ul id="tabProjects" class="nav nav-tabs">
|
||||
<li class="me-5">
|
||||
<a href="{{ path('app_user_project_submit') }}" class="btn btn-success">Ajouter</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="#" data-status="A Voter"">A Voter</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#" data-status="Brouillon">Brouillon</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#" data-status="Voté">Voté</a>
|
||||
</li>
|
||||
<li class="nav-item ms-auto">
|
||||
<a class="nav-link" href="#" data-status="Archivé">Archivé</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-success nav-tome" href="#">Mes Projets</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="d-flex align-items-center">
|
||||
<h2>Mes Projets</h2>
|
||||
<a href="{{ path('app_user_project_submit') }}" class="btn btn-success rounded-circle ms-3" title="Ajouter un projet"><i class="fas fa-plus" style="-webkit-text-stroke: 1px;"></i></a>
|
||||
</div>
|
||||
|
||||
<ul id="tabProjects" class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-success nav-tome" href="#">Mes Projets</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#" data-status="A Voter"">A Voter</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#" data-status="Brouillon">Brouillon</a>
|
||||
</li>
|
||||
<li class="nav-item ms-auto">
|
||||
<a class="nav-link" href="#" data-status="Voté"><small>Voté</small></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#" data-status="Archivé"><small>Archivé</small></a>
|
||||
</li>
|
||||
|
||||
<div id="containerdraft" data-status="Brouillon" class='containerStatus d-flex flex-wrap mt-3' style='justify-content: left'>
|
||||
<h3 style="width:100%">Brouillon</h3>
|
||||
</div>
|
||||
<div id="containervoted" data-status="A Voter" class='containerStatus d-flex flex-wrap mt-3' style='justify-content: left'>
|
||||
<h3 style="width:100%">A Voter</h3>
|
||||
</div>
|
||||
<div id="containertovote" data-status="Voté" class='containerStatus d-flex flex-wrap mt-3' style='justify-content: left'>
|
||||
<h3 style="width:100%">Voté</h3>
|
||||
</div>
|
||||
<div id="containerarchived" data-status="Archivé" class='containerStatus d-flex flex-wrap mt-3' style='justify-content: left'>
|
||||
<h3 style="width:100%">Archivé</h3>
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
<div style="display:none">
|
||||
{% for project in projects %}
|
||||
{% if is_granted('VIEW', project) %}
|
||||
<div class='card cardProject' data-status="{{project.status}}" data-tome="{{is_granted('TOME', project)}}" style='width:300px; margin-right:10px;'>
|
||||
<div class='card-header d-flex justify-content-between align-items-center'>
|
||||
<h5>{{project.title}}</h5>
|
||||
<div id="containerdraft" data-status="Brouillon" class='containerProjectStatus d-flex flex-wrap mt-3' style='justify-content: left'>
|
||||
<h3 style="width:100%">Brouillon</h3>
|
||||
</div>
|
||||
<div id="containervoted" data-status="A Voter" class='containerProjectStatus d-flex flex-wrap mt-3' style='justify-content: left'>
|
||||
<h3 style="width:100%">A Voter</h3>
|
||||
</div>
|
||||
<div id="containertovote" data-status="Voté" class='containerProjectStatus d-flex flex-wrap mt-3' style='justify-content: left'>
|
||||
<h3 style="width:100%">Voté</h3>
|
||||
</div>
|
||||
<div id="containerarchived" data-status="Archivé" class='containerProjectStatus d-flex flex-wrap mt-3' style='justify-content: left'>
|
||||
<h3 style="width:100%">Archivé</h3>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a href="{{ path("app_user_project_view",{id:project.id}) }}" class="btn btn-secondary btn-sm"><i class="fas fa-eye"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class='card-body'>
|
||||
{{project.summary|nl2br}}
|
||||
<div style="display:none">
|
||||
{% for project in projects %}
|
||||
{% if is_granted('VIEW', project) %}
|
||||
<div class='card cardProject' data-status="{{project.status}}" data-tome="{{is_granted('TOME', project)}}" style='width:300px; margin-right:10px;;margin-bottom:10px;'>
|
||||
<div class='card-header d-flex justify-content-between align-items-center'>
|
||||
<h5>{{project.title}}</h5>
|
||||
|
||||
{% if project.status=="Voté" or project.status=="Archivé" %}
|
||||
<small>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
{% for option in project.options %}
|
||||
{{ option.title|title }} = {{ option.nbVote }}<br>
|
||||
{% endfor %}
|
||||
<div>
|
||||
<a href="{{ path("app_user_project_view",{id:project.id}) }}" class="btn btn-secondary btn-sm"><i class="fas fa-eye"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
Votes Blancs = {{ project.nbVoteWhite }}<br>
|
||||
Votes Nuls = {{ project.nbVoteNull }}
|
||||
<div class='card-body'>
|
||||
{{project.summary|nl2br}}
|
||||
|
||||
{% if project.status=="Voté" or project.status=="Archivé" %}
|
||||
<small>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
{% for option in project.options %}
|
||||
{{ option.title|title }} = {{ option.nbVote }}<br>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
Votes Blancs = {{ project.nbVoteWhite }}<br>
|
||||
Votes Nuls = {{ project.nbVoteNull }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<strong>Résultat = </strong><br>
|
||||
{{ project.resultVote|nl2br }}
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class='card-footer' style="line-height:14px">
|
||||
<small><em>
|
||||
{% if project.dueDate %}
|
||||
<b>A Voter pour le</b> = {{ project.dueDate|date("d/m/Y") }}<br>
|
||||
{% endif %}
|
||||
<b>Nature</b> = {{ project.nature }}<br>
|
||||
<b>Participants</b> =
|
||||
{%for user in project.users%}
|
||||
{{loop.first ? user.username : ' - '~user.username}}
|
||||
{%endfor%}
|
||||
</em></small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<strong>Résultat = </strong><br>
|
||||
{{ project.resultVote|nl2br }}
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class='card-footer' style="line-height:14px">
|
||||
<small><em>
|
||||
{% if project.dueDate %}
|
||||
<b>A Voter pour le</b> = {{ project.dueDate|date("d/m/Y") }}<br>
|
||||
{% endif %}
|
||||
<b>Nature</b> = {{ project.nature }}<br>
|
||||
<b>Propriétaires</b> =
|
||||
{%for user in project.users%}
|
||||
{{loop.first ? user.username : ' - '~user.username}}
|
||||
{%endfor%}
|
||||
</em></small>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<h2>Groupes</h2>
|
||||
<a href="{{ path('app_user_group_submit') }}" class="btn btn-success rounded-circle ms-3" title="Ajouter un groupe"><i class="fas fa-plus" style="-webkit-text-stroke: 1px;"></i></a>
|
||||
</div>
|
||||
<ul id="tabGroups" class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active text-success nav-tome" href="#">Mes Groupes</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#" data-status="Actif"">Actif</a>
|
||||
</li>
|
||||
<li class="nav-item ms-auto">
|
||||
<a class="nav-link" href="#" data-status="Inactif"><small>Inactif</small></a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div id="containeractif" data-status="Actif" class='containerGroupStatus d-flex flex-wrap mt-3' style='justify-content: left'>
|
||||
<h3 style="width:100%">Actif</h3>
|
||||
</div>
|
||||
<div id="containerinactif" data-status="Inactif" class='containerGroupStatus d-flex flex-wrap mt-3' style='justify-content: left'>
|
||||
<h3 style="width:100%">Inactif</h3>
|
||||
</div>
|
||||
|
||||
<div style="display:none">
|
||||
{% for group in groups %}
|
||||
{% if is_granted('VIEW', group) %}
|
||||
<div class='card cardGroup' data-status="{{group.status}}" data-tome="{{is_granted('TOME', group)}}" style='width:250px; margin-right:10px;margin-bottom:10px;'>
|
||||
<div class='card-header d-flex justify-content-between align-items-center'>
|
||||
<h5>{{group.title}}</h5>
|
||||
|
||||
<div>
|
||||
<a href="{{ path("app_user_group_view",{id:group.id}) }}" class="btn btn-secondary btn-sm"><i class="fas fa-eye"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class='card-body'>
|
||||
{{group.summary|nl2br}}
|
||||
</div>
|
||||
|
||||
<div class='card-footer' style="line-height:14px">
|
||||
<small><em>
|
||||
{% if group.dueDate %}
|
||||
<b>A Voter pour le</b> = {{ group.dueDate|date("d/m/Y") }}<br>
|
||||
{% endif %}
|
||||
<b>Propriétaires</b> =
|
||||
{%for user in group.users%}
|
||||
{{loop.first ? user.username : ' - '~user.username}}
|
||||
{%endfor%}
|
||||
</em></small>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{%endblock%}
|
||||
|
||||
{% block localscript %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Cacher tous les containers sauf celui actif au chargement
|
||||
let initialStatus = $('.nav-link.active').data('status');
|
||||
$('.containerStatus').removeClass("d-flex");
|
||||
$('.containerStatus').hide();
|
||||
$('.containerProjectStatus').removeClass("d-flex");
|
||||
$('.containerProjectStatus').hide();
|
||||
|
||||
$('.cardProject').each(function () {
|
||||
const $card = $(this);
|
||||
const status = $card.data('status');
|
||||
const $container = $('.containerStatus[data-status="' + status + '"]')
|
||||
const $container = $('.containerProjectStatus[data-status="' + status + '"]')
|
||||
|
||||
if ($container.length) {
|
||||
$container.append($card);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(initialStatus);
|
||||
$('.containerStatus[data-status="' + initialStatus + '"]').addClass("d-flex");
|
||||
|
||||
$('#tabProjects .nav-link').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Gère les onglets actifs
|
||||
$('.nav-link').removeClass('active');
|
||||
$('#tabProjects .nav-link').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
|
||||
if ($(this).hasClass('nav-tome')) {
|
||||
@@ -127,7 +185,7 @@
|
||||
// On affiche que les cardProject avec tome
|
||||
$(".cardProject[data-tome='1']").show();
|
||||
|
||||
$('.containerStatus').each(function () {
|
||||
$('.containerProjectStatus').each(function () {
|
||||
const $container = $(this);
|
||||
const hasTome = $container.find('.cardProject[data-tome="1"]').length > 0;
|
||||
|
||||
@@ -146,11 +204,69 @@
|
||||
let status = $(this).data('status');
|
||||
|
||||
// Cache tous les containers, puis affiche celui correspondant au data-status
|
||||
$('.containerStatus').removeClass('d-flex').hide();
|
||||
$('.containerStatus[data-status="' + status + '"]').addClass("d-flex").show();
|
||||
$('.containerProjectStatus').removeClass('d-flex').hide();
|
||||
$('.containerProjectStatus[data-status="' + status + '"]').addClass("d-flex").show();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#tabProjects .nav-link.active').click();
|
||||
});
|
||||
|
||||
|
||||
$(document).ready(function() {
|
||||
// Cacher tous les containers sauf celui actif au chargement
|
||||
$('.containerGroupStatus').removeClass("d-flex");
|
||||
$('.containerGroupStatus').hide();
|
||||
|
||||
$('.cardGroup').each(function () {
|
||||
const $card = $(this);
|
||||
const status = $card.data('status');
|
||||
const $container = $('.containerGroupStatus[data-status="' + status + '"]')
|
||||
|
||||
if ($container.length) {
|
||||
$container.append($card);
|
||||
}
|
||||
});
|
||||
|
||||
$('#tabGroups .nav-link').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Gère les onglets actifs
|
||||
$('#tabGroups .nav-link').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
|
||||
if ($(this).hasClass('nav-tome')) {
|
||||
// On masque tt les cardGroup
|
||||
$(".cardGroup").hide();
|
||||
|
||||
// On affiche que les cardGroup avec tome
|
||||
$(".cardGroup[data-tome='1']").show();
|
||||
|
||||
$('.containerGroupStatus').each(function () {
|
||||
const $container = $(this);
|
||||
const hasTome = $container.find('.cardGroup[data-tome="1"]').length > 0;
|
||||
|
||||
if (hasTome) {
|
||||
$container.addClass('d-flex').show();
|
||||
} else {
|
||||
$container.removeClass('d-flex').hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
// On affiche tt les cardGroup
|
||||
$(".cardGroup").show();
|
||||
|
||||
// Récupère le data-status sélectionné
|
||||
let status = $(this).data('status');
|
||||
|
||||
// Cache tous les containers, puis affiche celui correspondant au data-status
|
||||
$('.containerGroupStatus').removeClass('d-flex').hide();
|
||||
$('.containerGroupStatus[data-status="' + status + '"]').addClass("d-flex").show();
|
||||
}
|
||||
});
|
||||
|
||||
$('#tabGroups .nav-link.active').click();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
@@ -21,6 +21,8 @@
|
||||
<div class="card-header">Information</div>
|
||||
<div class="card-body">
|
||||
{{ form_row(form.title) }}
|
||||
{{ form_row(form.open) }}
|
||||
|
||||
{{ form_row(form.nature) }}
|
||||
{{ form_row(form.dueDate) }}
|
||||
{{ form_row(form.users) }}
|
||||
@@ -64,8 +66,8 @@
|
||||
<a href="{{ path(routeupdateoption,{idproject:project.id,id:option.id}) }}" class="me-2"><i class="fas fa-file fa-2x"></i></a>
|
||||
</td>
|
||||
<td>{{option.title}}</td>
|
||||
<td>{{option.whyYes?option.whyYes|markdown_to_html:""}}</td>
|
||||
<td>{{option.whyNot?option.whyNot|markdown_to_html:""}}</td>
|
||||
<td>{{option.whyYes?option.whyYes|markdown_to_html|nl2br:""}}</td>
|
||||
<td>{{option.whyNot?option.whyNot|markdown_to_html|nl2br:""}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
@@ -15,6 +15,12 @@
|
||||
|
||||
<a href="{{ path(routecancel) }}" class="btn btn-secondary me-5">Retour</a>
|
||||
|
||||
{% if is_granted('CANSUBSCRIBE', project) %}
|
||||
<a href="{{ path(routesubscribe,{id:project.id}) }}" class="btn btn-secondary me-1" onclick="return confirm('Participer à ce projet ?')">
|
||||
<i class="fas fa-users"></i> Participer à ce projet
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if is_granted('MOVETOVOTE', project) and project.status=="Brouillon"%}
|
||||
<a href="{{ path(routemove,{id:project.id, status:"TOVOTE"}) }}" class="btn btn-primary me-1" onclick="return confirm('Statut = à Voter ?')">
|
||||
<i class="fas fa-angle-double-right"></i> Statut = à Voter
|
||||
@@ -59,6 +65,7 @@
|
||||
<div class="card-header">Information</div>
|
||||
<div class="card-body">
|
||||
<b>Titre</b> = {{ project.title }}<br>
|
||||
<b>Projet Ouvert</b> = {{ project.open ? "Oui" : "Non" }}<br>
|
||||
<b>Nature</b> = {{ project.nature }}<br>
|
||||
<b>A Voter pour le</b> = {{ project.dueDate ? project.dueDate|date("d/m/Y"):"" }}
|
||||
</div>
|
||||
@@ -71,6 +78,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">Participants</div>
|
||||
<div class="card-body">
|
||||
{%for user in project.users%}
|
||||
{{loop.first ? user.username : ' - '~user.username}}
|
||||
{%endfor%}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ render(path("bninefiles_files",{domain:'project',id:project.id, editable:0})) }}
|
||||
|
||||
<div class="card mt-3">
|
||||
@@ -112,11 +128,11 @@
|
||||
<div style="display:flex;">
|
||||
<div style="flex:auto; padding:10px;">
|
||||
<center><i class="fas fa-plus"></i></center>
|
||||
{{option.whyYes?option.whyYes|markdown_to_html:""}}
|
||||
{{option.whyYes?option.whyYes|markdown_to_html|nl2br:""}}
|
||||
</div>
|
||||
<div style="flex:auto; padding:10px;">
|
||||
<center><i class="fas fa-minus"></i></center>
|
||||
{{option.whyNot?option.whyNot|markdown_to_html:""}}
|
||||
{{option.whyNot?option.whyNot|markdown_to_html|nl2br:""}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,7 +141,7 @@
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">Description du Projet</div>
|
||||
<div class="card-body">
|
||||
{{ project.description ? project.description|markdown_to_html : "" }}
|
||||
{{ project.description ? project.description|markdown_to_html|nl2br: "" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -7,6 +7,7 @@
|
||||
|
||||
|
||||
{{ form_start(form) }}
|
||||
|
||||
{{ form_widget(form.submit) }}
|
||||
<a href="{{ path(routecancel) }}" class="btn btn-secondary ms-1">Annuler</a>
|
||||
{%if mode=="update" %}<a href="{{ path(routedelete,{id:form.vars.value.id}) }}" class="btn btn-danger float-end" onclick="return confirm('Confirmez-vous la suppression de cet enregistrement ?')">Supprimer</a>{%endif%}
|
||||
@@ -31,13 +32,6 @@
|
||||
{{ form_row(form.email) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">Redmine</div>
|
||||
<div class="card-body">
|
||||
{{ form_row(form.apikey) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{%if form.roles is defined%}
|
||||
|
Reference in New Issue
Block a user