migration symfo
This commit is contained in:
3
.env
3
.env
@ -18,3 +18,6 @@ CAS_MAIL=mail
|
|||||||
CAS_LASTNAME=lastname
|
CAS_LASTNAME=lastname
|
||||||
CAS_FIRSTNAME=firstname
|
CAS_FIRSTNAME=firstname
|
||||||
|
|
||||||
|
ETHERPAD_URL=http://localhost:9001
|
||||||
|
ETHERPAD_INTERNALURL=http://etherpad:9001
|
||||||
|
ETHERPAD_APIKEY=changeme # identique que data/etherpad/apikey/APIKEY.txt
|
4
.env.dev
4
.env.dev
@ -1,4 +0,0 @@
|
|||||||
|
|
||||||
###> symfony/framework-bundle ###
|
|
||||||
APP_SECRET=628790eb09e2cf96d93a21f4f43433d8
|
|
||||||
###< symfony/framework-bundle ###
|
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -25,6 +25,7 @@ phpstan.neon
|
|||||||
/public/bundles/
|
/public/bundles/
|
||||||
/var/
|
/var/
|
||||||
/vendor/
|
/vendor/
|
||||||
|
/data/
|
||||||
###< symfony/framework-bundle ###
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
###> friendsofphp/php-cs-fixer ###
|
###> friendsofphp/php-cs-fixer ###
|
||||||
|
16
compose.yaml
16
compose.yaml
@ -34,6 +34,22 @@ services:
|
|||||||
- ./vendor:/app/vendor:delegated
|
- ./vendor:/app/vendor:delegated
|
||||||
- ./public/bundles:/app/public/bundles: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:
|
adminer:
|
||||||
image: adminer
|
image: adminer
|
||||||
|
@ -12,7 +12,9 @@ parameters:
|
|||||||
casMail: "%env(resolve:CAS_MAIL)%"
|
casMail: "%env(resolve:CAS_MAIL)%"
|
||||||
casLastname: "%env(resolve:CAS_LASTNAME)%"
|
casLastname: "%env(resolve:CAS_LASTNAME)%"
|
||||||
casFirstname: "%env(resolve:CAS_FIRSTNAME)%"
|
casFirstname: "%env(resolve:CAS_FIRSTNAME)%"
|
||||||
|
etherpadUrl: "%env(resolve:ETHERPAD_URL)%"
|
||||||
|
etherpadInternalUrl: "%env(resolve:ETHERPAD_INTERNALURL)%"
|
||||||
|
etherpadApiKey: "%env(resolve:ETHERPAD_APIKEY)%"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
_defaults:
|
_defaults:
|
||||||
|
@ -2,23 +2,18 @@
|
|||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Entity\User;
|
use App\Repository\ProjectRepository;
|
||||||
|
use App\Service\EtherpadService;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
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\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
class HomeController extends AbstractController
|
class HomeController extends AbstractController
|
||||||
{
|
{
|
||||||
#[Route('/', name: 'app_home')]
|
#[Route('/', name: 'app_home')]
|
||||||
public function home(Request $request): Response
|
public function home(ProjectRepository $projectRepository): Response
|
||||||
{
|
{
|
||||||
$user = $this->getUser();
|
$projects = $projectRepository->findAll();
|
||||||
if (!$user instanceof User) {
|
|
||||||
throw new AccessDeniedException('Vous n\'avez pas accès à cette ressource.');
|
|
||||||
}
|
|
||||||
$projects = $user->getProjects();
|
|
||||||
|
|
||||||
return $this->render('home/home.html.twig', [
|
return $this->render('home/home.html.twig', [
|
||||||
'usemenu' => true,
|
'usemenu' => true,
|
||||||
@ -35,4 +30,19 @@ class HomeController extends AbstractController
|
|||||||
'usesidebar' => true,
|
'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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,6 +121,9 @@ class ProjectController extends AbstractController
|
|||||||
$attribute = constant(ProjectVoter::class.'::MOVE'.$status);
|
$attribute = constant(ProjectVoter::class.'::MOVE'.$status);
|
||||||
$this->denyAccessUnlessGranted($attribute, $project);
|
$this->denyAccessUnlessGranted($attribute, $project);
|
||||||
|
|
||||||
|
if (Project::VOTED == $status) {
|
||||||
|
}
|
||||||
|
|
||||||
$project->setStatus(constant(Project::class.'::'.$status));
|
$project->setStatus(constant(Project::class.'::'.$status));
|
||||||
$em->flush();
|
$em->flush();
|
||||||
|
|
||||||
|
@ -42,6 +42,15 @@ class Project
|
|||||||
#[ORM\Column(type: 'datetime', nullable: true)]
|
#[ORM\Column(type: 'datetime', nullable: true)]
|
||||||
private ?\DateTimeInterface $dueDate = null;
|
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>
|
* @var Collection<int, User>
|
||||||
*/
|
*/
|
||||||
@ -51,14 +60,14 @@ class Project
|
|||||||
/**
|
/**
|
||||||
* @var Collection<int, ProjectTimeline>
|
* @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'])]
|
#[ORM\OrderBy(['createdAt' => 'DESC'])]
|
||||||
private Collection $timelines;
|
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'])]
|
#[ORM\OrderBy(['title' => 'ASC'])]
|
||||||
private Collection $options;
|
private Collection $options;
|
||||||
|
|
||||||
@ -146,6 +155,42 @@ class Project
|
|||||||
return $this;
|
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>
|
* @return Collection<int, User>
|
||||||
*/
|
*/
|
||||||
@ -182,4 +227,21 @@ class Project
|
|||||||
{
|
{
|
||||||
return $this->options;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,9 @@ class ProjectOption
|
|||||||
#[ORM\Column(type: 'text', nullable: true)]
|
#[ORM\Column(type: 'text', nullable: true)]
|
||||||
private ?string $whyNot = null;
|
private ?string $whyNot = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'integer')]
|
||||||
|
private int $nbVote = 0;
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
@ -78,4 +81,16 @@ class ProjectOption
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getNbVote(): int
|
||||||
|
{
|
||||||
|
return $this->nbVote;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setNbVote(int $nbVote): self
|
||||||
|
{
|
||||||
|
$this->nbVote = $nbVote;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ use Bnine\MdEditorBundle\Form\Type\MarkdownType;
|
|||||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
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\SubmitType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
@ -28,6 +29,12 @@ class ProjectType extends AbstractType
|
|||||||
'label' => 'Titre',
|
'label' => 'Titre',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
->add('dueDate', DateType::class, [
|
||||||
|
'label' => 'A Voter pour le',
|
||||||
|
'required' => false,
|
||||||
|
'html5' => true,
|
||||||
|
])
|
||||||
|
|
||||||
->add('summary', TextareaType::class, [
|
->add('summary', TextareaType::class, [
|
||||||
'label' => 'Résumé',
|
'label' => 'Résumé',
|
||||||
])
|
])
|
||||||
|
40
src/Form/ProjectVotedType.php
Normal file
40
src/Form/ProjectVotedType.php
Normal 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',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
31
src/Form/Type/ProjectOptionType.php
Normal file
31
src/Form/Type/ProjectOptionType.php
Normal 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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -18,10 +18,11 @@ class ProjectVoter extends Voter
|
|||||||
public const MOVETOVOTE = 'MOVETOVOTE';
|
public const MOVETOVOTE = 'MOVETOVOTE';
|
||||||
public const MOVEVOTED = 'MOVEVOTED';
|
public const MOVEVOTED = 'MOVEVOTED';
|
||||||
public const MOVEARCHIVED = 'MOVEARCHIVED';
|
public const MOVEARCHIVED = 'MOVEARCHIVED';
|
||||||
|
public const TOME = 'TOME';
|
||||||
|
|
||||||
protected function supports(string $attribute, $subject): bool
|
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;
|
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');
|
$hasUser = $user->hasRole('ROLE_ADMIN') || $user->hasRole('ROLE_MASTER');
|
||||||
$hasStatus = Project::TOVOTE === $project->getStatus() || Project::ARCHIVED === $project->getStatus();
|
$hasStatus = Project::TOVOTE === $project->getStatus() || Project::ARCHIVED === $project->getStatus();
|
||||||
dump($hasStatus);
|
|
||||||
|
|
||||||
return $hasUser && $hasStatus;
|
return $hasUser && $hasStatus;
|
||||||
}
|
}
|
||||||
@ -92,13 +92,17 @@ class ProjectVoter extends Voter
|
|||||||
return $hasUser && $hasStatus;
|
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
|
protected function voteOnAttribute(string $attribute, $project, TokenInterface $token): bool
|
||||||
{
|
{
|
||||||
$user = $token->getUser();
|
$user = $token->getUser();
|
||||||
if (!$user instanceof User) {
|
if (!$user instanceof User) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
dump($attribute);
|
|
||||||
|
|
||||||
return match ($attribute) {
|
return match ($attribute) {
|
||||||
self::VIEW => $this->canView($project, $user),
|
self::VIEW => $this->canView($project, $user),
|
||||||
@ -109,6 +113,7 @@ class ProjectVoter extends Voter
|
|||||||
self::MOVETOVOTE => $this->canMoveToVote($project, $user),
|
self::MOVETOVOTE => $this->canMoveToVote($project, $user),
|
||||||
self::MOVEVOTED => $this->canMoveVoted($project, $user),
|
self::MOVEVOTED => $this->canMoveVoted($project, $user),
|
||||||
self::MOVEARCHIVED => $this->canMoveArchived($project, $user),
|
self::MOVEARCHIVED => $this->canMoveArchived($project, $user),
|
||||||
|
self::TOME => $this->toMe($project, $user),
|
||||||
default => false,
|
default => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
117
src/Service/EtherpadService.php
Normal file
117
src/Service/EtherpadService.php
Normal 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 l’URL iframe
|
||||||
|
$iframeUrl = sprintf(
|
||||||
|
'%s/p/%s?showControls=true&showChat=false&userName=%s',
|
||||||
|
rtrim($this->etherpadUrl, '/'),
|
||||||
|
$padName,
|
||||||
|
urlencode($user->getUsername())
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'iframeUrl' => $iframeUrl,
|
||||||
|
'cookie' => $cookie,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createAuthorIfNeeded(User $user): string
|
||||||
|
{
|
||||||
|
// Idéalement, stocker $authorId en base pour éviter de le recréer
|
||||||
|
$response = $this->client->request('GET', $this->etherpadInternalUrl.'/api/1.2.15/createAuthor', [
|
||||||
|
'query' => [
|
||||||
|
'apikey' => $this->etherpadApiKey,
|
||||||
|
'name' => $user->getUsername(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = $response->toArray(false);
|
||||||
|
|
||||||
|
return $data['data']['authorID'] ?? throw new \RuntimeException('Impossible de créer l\'author');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createGroupIfNeeded(string $padName): string
|
||||||
|
{
|
||||||
|
// Ici tu peux lier un groupID en base à ton padName
|
||||||
|
$response = $this->client->request('GET', $this->etherpadInternalUrl.'/api/1.2.15/createGroup', [
|
||||||
|
'query' => [
|
||||||
|
'apikey' => $this->etherpadApiKey,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = $response->toArray(false);
|
||||||
|
|
||||||
|
return $data['data']['groupID'] ?? throw new \RuntimeException('Impossible de créer le group');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createGroupPadIfNeeded(string $groupId, string $padName): void
|
||||||
|
{
|
||||||
|
$this->client->request('GET', $this->etherpadInternalUrl.'/api/1.2.15/createGroupPad', [
|
||||||
|
'query' => [
|
||||||
|
'apikey' => $this->etherpadApiKey,
|
||||||
|
'groupID' => $groupId,
|
||||||
|
'padName' => $padName,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createSession(string $groupId, string $authorId, int $validUntil): string
|
||||||
|
{
|
||||||
|
$response = $this->client->request('GET', $this->etherpadInternalUrl.'/api/1.2.15/createSession', [
|
||||||
|
'query' => [
|
||||||
|
'apikey' => $this->etherpadApiKey,
|
||||||
|
'groupID' => $groupId,
|
||||||
|
'authorID' => $authorId,
|
||||||
|
'validUntil' => $validUntil,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = $response->toArray(false);
|
||||||
|
|
||||||
|
return $data['data']['sessionID'] ?? throw new \RuntimeException('Impossible de créer la session');
|
||||||
|
}
|
||||||
|
}
|
1
templates/etherpad/show.html.twig
Normal file
1
templates/etherpad/show.html.twig
Normal file
@ -0,0 +1 @@
|
|||||||
|
<iframe src="{{ iframeUrl }}" style="width:100%; height:600px; border:0;"></iframe>
|
@ -4,12 +4,44 @@
|
|||||||
|
|
||||||
{%block body%}
|
{%block body%}
|
||||||
<h2>Projets</h2>
|
<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 %}
|
{% for project in projects %}
|
||||||
{% if is_granted('VIEW', project) %}
|
{% 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'>
|
<div class='card-header d-flex justify-content-between align-items-center'>
|
||||||
<h5>{{project.title}}</h5>
|
<h5>{{project.title}}</h5>
|
||||||
|
|
||||||
@ -20,9 +52,12 @@
|
|||||||
<div class='card-body'>
|
<div class='card-body'>
|
||||||
{{project.summary|nl2br}}
|
{{project.summary|nl2br}}
|
||||||
</div>
|
</div>
|
||||||
<div class='card-footer'>
|
<div class='card-footer' style="line-height:14px">
|
||||||
<small><em>
|
<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> =
|
<b>Propriétaires</b> =
|
||||||
{%for user in project.users%}
|
{%for user in project.users%}
|
||||||
{{loop.first ? user.username : ' - '~user.username}}
|
{{loop.first ? user.username : ' - '~user.username}}
|
||||||
@ -32,7 +67,68 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
</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();
|
||||||
|
|
||||||
|
$('.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%}
|
|
@ -18,9 +18,9 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4 mx-auto">
|
<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-header">Titre</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{{ form_row(form.title) }}
|
{{ form_widget(form.title) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -30,17 +30,17 @@
|
|||||||
<div class="col-md-2"></div>
|
<div class="col-md-2"></div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<div class="card-header">Information</div>
|
<div class="card-header">Pourquoi Voter Pour</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{{ form_row(form.whyYes) }}
|
{{ form_widget(form.whyYes) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<div class="card-header">Information</div>
|
<div class="card-header">Pourquoi Voter Contre</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{{ form_row(form.whyNot) }}
|
{{ form_widget(form.whyNot) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{{ form_row(form.title) }}
|
{{ form_row(form.title) }}
|
||||||
{{ form_row(form.nature) }}
|
{{ form_row(form.nature) }}
|
||||||
|
{{ form_row(form.dueDate) }}
|
||||||
{{ form_row(form.users) }}
|
{{ form_row(form.users) }}
|
||||||
{{ form_row(form.summary) }}
|
{{ form_row(form.summary) }}
|
||||||
</div>
|
</div>
|
||||||
@ -51,7 +52,9 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th width="100px" class="no-sort">Action</th>
|
<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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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>
|
<a href="{{ path(routeupdateoption,{idproject:project.id,id:option.id}) }}" class="me-2"><i class="fas fa-file fa-2x"></i></a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{option.title}}</td>
|
<td>{{option.title}}</td>
|
||||||
|
<td>{{option.whyYes?option.whyYes|markdown_to_html:""}}</td>
|
||||||
|
<td>{{option.whyNot?option.whyNot|markdown_to_html:""}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -85,6 +90,13 @@
|
|||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
$("#project_title").focus();
|
$("#project_title").focus();
|
||||||
|
|
||||||
|
$('#dataTables').DataTable({
|
||||||
|
columnDefs: [ { "targets": "no-sort", "orderable": false }, { "targets": "no-string", "type" : "num" } ],
|
||||||
|
responsive: true,
|
||||||
|
iDisplayLength: 100,
|
||||||
|
order: [[ 1, "asc" ]]
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -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 %}
|
|
||||||
|
@ -8,32 +8,59 @@
|
|||||||
<div class="">{{project.status}}</div>
|
<div class="">{{project.status}}</div>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{% if is_granted('UPDATE', project) %}
|
<div class="mb-3">
|
||||||
<a href="{{ path(routeupdate,{id:project.id}) }}" class="btn btn-success me-1">Modifier</a>
|
{% if is_granted('UPDATE', project) %}
|
||||||
{% endif %}
|
<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="row">
|
||||||
<div class="col-md-4 mx-auto">
|
<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-header">Information</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<b>Titre</b> = {{ project.title }}<br>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -55,6 +82,23 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-8 mx-auto">
|
<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 mt-3">
|
||||||
<div class="card-header">Description du Projet</div>
|
<div class="card-header">Description du Projet</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
Reference in New Issue
Block a user