migration symfo

This commit is contained in:
2025-09-04 19:30:58 +02:00
parent efea97228b
commit 48781d086e
20 changed files with 515 additions and 65 deletions

3
.env
View File

@ -18,3 +18,6 @@ CAS_MAIL=mail
CAS_LASTNAME=lastname
CAS_FIRSTNAME=firstname
ETHERPAD_URL=http://localhost:9001
ETHERPAD_INTERNALURL=http://etherpad:9001
ETHERPAD_APIKEY=changeme # identique que data/etherpad/apikey/APIKEY.txt

View File

@ -1,4 +0,0 @@
###> symfony/framework-bundle ###
APP_SECRET=628790eb09e2cf96d93a21f4f43433d8
###< symfony/framework-bundle ###

1
.gitignore vendored
View File

@ -25,6 +25,7 @@ phpstan.neon
/public/bundles/
/var/
/vendor/
/data/
###< symfony/framework-bundle ###
###> friendsofphp/php-cs-fixer ###

View File

@ -34,6 +34,22 @@ services:
- ./vendor:/app/vendor:delegated
- ./public/bundles:/app/public/bundles:delegated
etherpad:
image: etherpad/etherpad:latest
container_name: etherpad
restart: unless-stopped
environment:
- ADMIN_PASSWORD=changeme
- TITLE=Mon Etherpad
- DEFAULT_PAD_TEXT=Bienvenue dans Etherpad!
- API_KEY=changeme
- AUTHENTICATION_METHOD=apikey
ports:
- "9001:9001"
volumes:
- ./volume/etherpad/data:/opt/etherpad-lite/var
- ./volume/etherpad/settings:/opt/etherpad-lite/settings
- ./volume/etherpad/apikey/APIKEY.txt:/opt/etherpad-lite/APIKEY.txt
adminer:
image: adminer

View File

@ -12,7 +12,9 @@ parameters:
casMail: "%env(resolve:CAS_MAIL)%"
casLastname: "%env(resolve:CAS_LASTNAME)%"
casFirstname: "%env(resolve:CAS_FIRSTNAME)%"
etherpadUrl: "%env(resolve:ETHERPAD_URL)%"
etherpadInternalUrl: "%env(resolve:ETHERPAD_INTERNALURL)%"
etherpadApiKey: "%env(resolve:ETHERPAD_APIKEY)%"
services:
_defaults:

View File

@ -2,23 +2,18 @@
namespace App\Controller;
use App\Entity\User;
use App\Repository\ProjectRepository;
use App\Service\EtherpadService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class HomeController extends AbstractController
{
#[Route('/', name: 'app_home')]
public function home(Request $request): Response
public function home(ProjectRepository $projectRepository): Response
{
$user = $this->getUser();
if (!$user instanceof User) {
throw new AccessDeniedException('Vous n\'avez pas accès à cette ressource.');
}
$projects = $user->getProjects();
$projects = $projectRepository->findAll();
return $this->render('home/home.html.twig', [
'usemenu' => true,
@ -35,4 +30,19 @@ class HomeController extends AbstractController
'usesidebar' => true,
]);
}
#[Route('/user/etherpad/{id}', name: 'app_ehterpad')]
public function etherpad(string $id, EtherpadService $etherpadService): Response
{
$padAccess = $etherpadService->preparePadAccess($this->getUser(), $id);
dump(vars: $padAccess);
$response = $this->render('etherpad/show.html.twig', [
'iframeUrl' => $padAccess['iframeUrl'],
]);
$response->headers->setCookie($padAccess['cookie']);
return $response;
}
}

View File

@ -121,6 +121,9 @@ class ProjectController extends AbstractController
$attribute = constant(ProjectVoter::class.'::MOVE'.$status);
$this->denyAccessUnlessGranted($attribute, $project);
if (Project::VOTED == $status) {
}
$project->setStatus(constant(Project::class.'::'.$status));
$em->flush();

View File

@ -42,6 +42,15 @@ class Project
#[ORM\Column(type: 'datetime', nullable: true)]
private ?\DateTimeInterface $dueDate = null;
#[ORM\Column(type: 'integer')]
private int $nbVoteWhite = 0;
#[ORM\Column(type: 'integer')]
private int $nbVoteNull = 0;
#[ORM\Column(type: 'string', nullable: true)]
private ?string $resultVote = null;
/**
* @var Collection<int, User>
*/
@ -51,14 +60,14 @@ class Project
/**
* @var Collection<int, ProjectTimeline>
*/
#[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectTimeline::class, cascade: ['remove'])]
#[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectTimeline::class, cascade: ['remove'], orphanRemoval: true)]
#[ORM\OrderBy(['createdAt' => 'DESC'])]
private Collection $timelines;
/**
* @var Collection<int, ProjectTimeline>
* @var Collection<int, ProjectOption>
*/
#[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectOption::class, cascade: ['remove'])]
#[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectOption::class, cascade: ['remove'], orphanRemoval: true)]
#[ORM\OrderBy(['title' => 'ASC'])]
private Collection $options;
@ -146,6 +155,42 @@ class Project
return $this;
}
public function getNbVoteWhite(): int
{
return $this->nbVoteWhite;
}
public function setNbVoteWhite(int $nbVoteWhite): self
{
$this->nbVoteWhite = $nbVoteWhite;
return $this;
}
public function getNbVoteNull(): int
{
return $this->nbVoteNull;
}
public function setNbVoteNull(int $nbVoteNull): self
{
$this->nbVoteNull = $nbVoteNull;
return $this;
}
public function getResultVote(): ?string
{
return $this->resultVote;
}
public function setResultVote(?string $resultVote): self
{
$this->resultVote = $resultVote;
return $this;
}
/**
* @return Collection<int, User>
*/
@ -182,4 +227,21 @@ class Project
{
return $this->options;
}
public function addOption(ProjectOption $option): self
{
if (!$this->options->contains($option)) {
$this->options->add($option);
$option->setProject($this);
}
return $this;
}
public function removeOption(ProjectOption $option): self
{
$this->options->removeElement($option);
return $this;
}
}

View File

@ -26,6 +26,9 @@ class ProjectOption
#[ORM\Column(type: 'text', nullable: true)]
private ?string $whyNot = null;
#[ORM\Column(type: 'integer')]
private int $nbVote = 0;
public function getId(): ?int
{
return $this->id;
@ -78,4 +81,16 @@ class ProjectOption
return $this;
}
public function getNbVote(): int
{
return $this->nbVote;
}
public function setNbVote(int $nbVote): self
{
$this->nbVote = $nbVote;
return $this;
}
}

View File

@ -8,6 +8,7 @@ use Bnine\MdEditorBundle\Form\Type\MarkdownType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
@ -28,6 +29,12 @@ class ProjectType extends AbstractType
'label' => 'Titre',
])
->add('dueDate', DateType::class, [
'label' => 'A Voter pour le',
'required' => false,
'html5' => true,
])
->add('summary', TextareaType::class, [
'label' => 'Résumé',
])

View File

@ -0,0 +1,40 @@
<?php
namespace App\Form;
use App\Entity\Project;
use App\Form\Type\ProjectOptionType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ProjectVotedType 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('options', CollectionType::class, [
'entry_type' => ProjectOptionType::class,
'entry_options' => ['label' => false],
'label' => false,
'allow_add' => false,
'allow_delete' => false,
'by_reference' => false,
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Project::class,
'mode' => 'submit',
]);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Form\Type;
use App\Entity\ProjectOption;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ProjectOptionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', null, [
'disabled' => true,
'label' => 'Titre',
])
->add('nbVote', IntegerType::class, [
'label' => 'Nombre de votes',
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => ProjectOption::class,
]);
}
}

View File

@ -18,10 +18,11 @@ class ProjectVoter extends Voter
public const MOVETOVOTE = 'MOVETOVOTE';
public const MOVEVOTED = 'MOVEVOTED';
public const MOVEARCHIVED = 'MOVEARCHIVED';
public const TOME = 'TOME';
protected function supports(string $attribute, $subject): bool
{
$attributes = [self::VIEW, self::SUBMIT, self::UPDATE, self::DELETE, self::MOVEDRAFT, self::MOVETOVOTE, self::MOVEVOTED, self::MOVEARCHIVED];
$attributes = [self::VIEW, self::SUBMIT, self::UPDATE, self::DELETE, self::MOVEDRAFT, self::MOVETOVOTE, self::MOVEVOTED, self::MOVEARCHIVED, self::TOME];
return in_array($attribute, $attributes) && $subject instanceof Project;
}
@ -79,7 +80,6 @@ class ProjectVoter extends Voter
{
$hasUser = $user->hasRole('ROLE_ADMIN') || $user->hasRole('ROLE_MASTER');
$hasStatus = Project::TOVOTE === $project->getStatus() || Project::ARCHIVED === $project->getStatus();
dump($hasStatus);
return $hasUser && $hasStatus;
}
@ -92,13 +92,17 @@ class ProjectVoter extends Voter
return $hasUser && $hasStatus;
}
private function toMe(Project $project, User $user): bool
{
return $project->getUsers()->contains($user);
}
protected function voteOnAttribute(string $attribute, $project, TokenInterface $token): bool
{
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
dump($attribute);
return match ($attribute) {
self::VIEW => $this->canView($project, $user),
@ -109,6 +113,7 @@ class ProjectVoter extends Voter
self::MOVETOVOTE => $this->canMoveToVote($project, $user),
self::MOVEVOTED => $this->canMoveVoted($project, $user),
self::MOVEARCHIVED => $this->canMoveArchived($project, $user),
self::TOME => $this->toMe($project, $user),
default => false,
};
}

View File

@ -0,0 +1,117 @@
<?php
namespace App\Service;
use App\Entity\User;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class EtherpadService
{
private HttpClientInterface $client;
private string $etherpadUrl;
private string $etherpadInternalUrl;
private string $etherpadApiKey;
public function __construct(HttpClientInterface $client, ParameterBagInterface $parameter)
{
$this->client = $client;
$this->etherpadUrl = $parameter->get('etherpadUrl');
$this->etherpadInternalUrl = $parameter->get('etherpadInternalUrl');
$this->etherpadApiKey = $parameter->get('etherpadApiKey');
}
public function preparePadAccess(User $user, string $padName): array
{
// 1. Vérifier/créer l'auteur
$authorId = $this->createAuthorIfNeeded($user);
// 2. Vérifier/créer le groupe lié à ce pad
$groupId = $this->createGroupIfNeeded($padName);
// 3. Créer le pad si pas encore créé
$this->createGroupPadIfNeeded($groupId, $padName);
// 4. Créer une session temporaire (ex: 1h)
$validUntil = time() + 3600;
$sessionId = $this->createSession($groupId, $authorId, $validUntil);
// 5. Construire le cookie à ajouter à la réponse
$cookie = Cookie::create('sessionID', $sessionId)
->withDomain(parse_url($this->etherpadInternalUrl, PHP_URL_HOST))
->withPath('/')
->withSecure(true)
->withHttpOnly(false)
->withExpires($validUntil);
// 6. Construire lURL iframe
$iframeUrl = sprintf(
'%s/p/%s?showControls=true&showChat=false&userName=%s',
rtrim($this->etherpadUrl, '/'),
$padName,
urlencode($user->getUsername())
);
return [
'iframeUrl' => $iframeUrl,
'cookie' => $cookie,
];
}
private function createAuthorIfNeeded(User $user): string
{
// Idéalement, stocker $authorId en base pour éviter de le recréer
$response = $this->client->request('GET', $this->etherpadInternalUrl.'/api/1.2.15/createAuthor', [
'query' => [
'apikey' => $this->etherpadApiKey,
'name' => $user->getUsername(),
],
]);
$data = $response->toArray(false);
return $data['data']['authorID'] ?? throw new \RuntimeException('Impossible de créer l\'author');
}
private function createGroupIfNeeded(string $padName): string
{
// Ici tu peux lier un groupID en base à ton padName
$response = $this->client->request('GET', $this->etherpadInternalUrl.'/api/1.2.15/createGroup', [
'query' => [
'apikey' => $this->etherpadApiKey,
],
]);
$data = $response->toArray(false);
return $data['data']['groupID'] ?? throw new \RuntimeException('Impossible de créer le group');
}
private function createGroupPadIfNeeded(string $groupId, string $padName): void
{
$this->client->request('GET', $this->etherpadInternalUrl.'/api/1.2.15/createGroupPad', [
'query' => [
'apikey' => $this->etherpadApiKey,
'groupID' => $groupId,
'padName' => $padName,
],
]);
}
private function createSession(string $groupId, string $authorId, int $validUntil): string
{
$response = $this->client->request('GET', $this->etherpadInternalUrl.'/api/1.2.15/createSession', [
'query' => [
'apikey' => $this->etherpadApiKey,
'groupID' => $groupId,
'authorID' => $authorId,
'validUntil' => $validUntil,
],
]);
$data = $response->toArray(false);
return $data['data']['sessionID'] ?? throw new \RuntimeException('Impossible de créer la session');
}
}

View File

@ -0,0 +1 @@
<iframe src="{{ iframeUrl }}" style="width:100%; height:600px; border:0;"></iframe>

View File

@ -4,12 +4,44 @@
{%block body%}
<h2>Projets</h2>
<a href="{{ path('app_user_project_submit') }}" class="btn btn-success mb-3">Ajouter</a>
<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='d-flex flex-wrap' style='justify-content: left'>
<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>
<div style="display:none">
{% for project in projects %}
{% if is_granted('VIEW', project) %}
<div class='card' style='width:300px; margin-right:10px;'>
<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>
@ -20,9 +52,12 @@
<div class='card-body'>
{{project.summary|nl2br}}
</div>
<div class='card-footer'>
<div class='card-footer' style="line-height:14px">
<small><em>
<b>Statut</b> = {{ project.status }}<br>
{% 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}}
@ -32,7 +67,68 @@
</div>
{% endif %}
{% endfor %}
</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();
$('.cardProject').each(function () {
const $card = $(this);
const status = $card.data('status');
const $container = $('.containerStatus[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');
$(this).addClass('active');
if ($(this).hasClass('nav-tome')) {
// On masque tt les cardProject
$(".cardProject").hide();
// On affiche que les cardProject avec tome
$(".cardProject[data-tome='1']").show();
$('.containerStatus').each(function () {
const $container = $(this);
const hasTome = $container.find('.cardProject[data-tome="1"]').length > 0;
if (hasTome) {
$container.addClass('d-flex').show();
} else {
$container.removeClass('d-flex').hide();
}
});
}
else {
// On affiche tt les cardProject
$(".cardProject").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
$('.containerStatus').removeClass('d-flex').hide();
$('.containerStatus[data-status="' + status + '"]').addClass("d-flex").show();
}
});
});
</script>
{% endblock %}
{%endblock%}

View File

@ -18,9 +18,9 @@
<div class="row">
<div class="col-md-4 mx-auto">
<div class="card mt-3">
<div class="card-header">Information</div>
<div class="card-header">Titre</div>
<div class="card-body">
{{ form_row(form.title) }}
{{ form_widget(form.title) }}
</div>
</div>
</div>
@ -30,17 +30,17 @@
<div class="col-md-2"></div>
<div class="col-md-4">
<div class="card mt-3">
<div class="card-header">Information</div>
<div class="card-header">Pourquoi Voter Pour</div>
<div class="card-body">
{{ form_row(form.whyYes) }}
{{ form_widget(form.whyYes) }}
</div>
</div>
</div>
<div class="col-md-4">
<div class="card mt-3">
<div class="card-header">Information</div>
<div class="card-header">Pourquoi Voter Contre</div>
<div class="card-body">
{{ form_row(form.whyNot) }}
{{ form_widget(form.whyNot) }}
</div>
</div>
</div>

View File

@ -22,6 +22,7 @@
<div class="card-body">
{{ form_row(form.title) }}
{{ form_row(form.nature) }}
{{ form_row(form.dueDate) }}
{{ form_row(form.users) }}
{{ form_row(form.summary) }}
</div>
@ -51,7 +52,9 @@
<thead>
<tr>
<th width="100px" class="no-sort">Action</th>
<th>Title</th>
<th>Titre</th>
<th width="35%" class="no-sort">Pour</th>
<th width="35%" class="no-sort">Contre</th>
</tr>
</thead>
<tbody>
@ -61,6 +64,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>
</tr>
{% endfor %}
</tbody>
@ -85,6 +90,13 @@
<script>
$(document).ready(function() {
$("#project_title").focus();
$('#dataTables').DataTable({
columnDefs: [ { "targets": "no-sort", "orderable": false }, { "targets": "no-string", "type" : "num" } ],
responsive: true,
iDisplayLength: 100,
order: [[ 1, "asc" ]]
});
});
</script>
{% endblock %}

View File

@ -1,12 +1 @@
{% if is_granted('MOVEDRAFT', project) %}
<a href="{{ path(routemove,{id:project.id, status:"DRAFT"}) }}" class="btn btn-primary me-1" onclick="return confirm('Confirmez-vous le passage au vote ?')">Mettre en Brouillon</a>
{% endif %}
{% if is_granted('MOVETOVOTE', project) %}
<a href="{{ path(routemove,{id:project.id, status:"TOVOTE"}) }}" class="btn btn-primary me-1" onclick="return confirm('Confirmez-vous le passage au vote ?')">Mettre au Vote</a>
{% endif %}
{% if is_granted('MOVEVOTED', project) %}
<a href="{{ path(routemove,{id:project.id, status:"VOTED"}) }}" class="btn btn-primary me-1" onclick="return confirm('Confirmez-vous le vote ?')">Voter</a>
{% endif %}
{% if is_granted('DELETE', project) %}
<a href="{{ path(routedelete,{id:project.id}) }}" class="btn btn-danger float-end" onclick="return confirm('Confirmez-vous la suppression de cet enregistrement ?')">Supprimer</a>
{% endif %}

View File

@ -8,32 +8,59 @@
<div class="">{{project.status}}</div>
</h1>
{% if is_granted('UPDATE', project) %}
<a href="{{ path(routeupdate,{id:project.id}) }}" class="btn btn-success me-1">Modifier</a>
{% endif %}
<div class="mb-3">
{% if is_granted('UPDATE', project) %}
<a href="{{ path(routeupdate,{id:project.id}) }}" class="btn btn-success me-1">Modifier</a>
{% endif %}
<a href="{{ path(routecancel) }}" class="btn btn-secondary me-5">Retour</a>
<a href="{{ path(routecancel) }}" class="btn btn-secondary me-5">Retour</a>
{% 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
</a>
{% endif %}
{% if is_granted('MOVEVOTED', project) and project.status=="A Voter" %}
<a href="{{ path(routemove,{id:project.id, status:"VOTED"}) }}" class="btn btn-primary me-1" onclick="return confirm('Statut = Voté ?')">
<i class="fas fa-angle-double-right"></i> Statut = Voté
</a>
{% endif %}
{% if is_granted('MOVEARCHIVED', project) and project.status=="Voté" %}
<a href="{{ path(routemove,{id:project.id, status:"ARCHIVED"}) }}" class="btn btn-primary me-1" onclick="return confirm('Statut = Archivé ?')">
<i class="fas fa-angle-double-right"></i> Statut = Archivé
</a>
{% endif %}
{% if is_granted('DELETE', project) %}
<a href="{{ path(routedelete,{id:project.id}) }}" class="btn btn-danger float-end" onclick="return confirm('Confirmez-vous la suppression de cet enregistrement ?')">
Supprimer
</a>
{% endif %}
{% if is_granted('MOVEDRAFT', project) and project.status=="A Voter"%}
<a href="{{ path(routemove,{id:project.id, status:"DRAFT"}) }}" class="btn btn-warning me-1 float-end" onclick="return confirm('Statut = Brouillon ?')">
<i class="fas fa-angle-double-left"></i> Statut = Brouillon
</a>
{% endif %}
{% if is_granted('MOVETOVOTE', project) and project.status=="Voté"%}
<a href="{{ path(routemove,{id:project.id, status:"TOVOTE"}) }}" class="btn btn-warning me-1 float-end" onclick="return confirm('Statut = à Voter ?')">
<i class="fas fa-angle-double-left"></i> Statut = à Voter
</a>
{% endif %}
{% if is_granted('MOVEVOTED', project) and project.status=="Archivé"%}
<a href="{{ path(routemove,{id:project.id, status:"VOTED"}) }}" class="btn btn-warning me-1 float-end" onclick="return confirm('Statut = Voté ?')">
<i class="fas fa-angle-double-left"></i> Statut = Voté
</a>
{% endif %}
</div>
{% if is_granted('MOVEDRAFT', project) %}
<a href="{{ path(routemove,{id:project.id, status:"DRAFT"}) }}" class="btn btn-primary me-1" onclick="return confirm('Confirmez-vous le passage au vote ?')">Mettre en Brouillon</a>
{% endif %}
{% if is_granted('MOVETOVOTE', project) %}
<a href="{{ path(routemove,{id:project.id, status:"TOVOTE"}) }}" class="btn btn-primary me-1" onclick="return confirm('Confirmez-vous le passage au vote ?')">Mettre au Vote</a>
{% endif %}
{% if is_granted('MOVEVOTED', project) %}
<a href="{{ path(routemove,{id:project.id, status:"VOTED"}) }}" class="btn btn-primary me-1" onclick="return confirm('Confirmez-vous le vote ?')">Voter</a>
{% endif %}
{% if is_granted('DELETE', project) %}
<a href="{{ path(routedelete,{id:project.id}) }}" class="btn btn-danger float-end" onclick="return confirm('Confirmez-vous la suppression de cet enregistrement ?')">Supprimer</a>
{% endif %}
<div class="row">
<div class="col-md-4 mx-auto">
<div class="card mt-3">
<div class="card mt-3">
<div class="card-header">Information</div>
<div class="card-body">
<b>Titre</b> = {{ project.title }}<br>
<b>Nature</b> = {{ project.nature }}<br><br>
<b>Nature</b> = {{ project.nature }}<br>
<b>A Voter pour le</b> = {{ project.dueDate ? project.dueDate|date("d/m/Y"):"" }}
</div>
</div>
@ -55,6 +82,23 @@
</div>
<div class="col-md-8 mx-auto">
<div style="display:flex;" class="mt-3">
{% for option in project.options %}
<div class="card" style="flex:1 1 0">
<center><h2>{{ option.title }}</h2></center>
<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:""}}
</div>
<div style="flex:auto; padding:10px;">
<center><i class="fas fa-minus"></i></center>
{{option.whyNot?option.whyNot|markdown_to_html:""}}
</div>
</div>
</div>
{% endfor %}
</div>
<div class="card mt-3">
<div class="card-header">Description du Projet</div>
<div class="card-body">