svg
This commit is contained in:
@ -3,19 +3,13 @@
|
|||||||
"license": "proprietary",
|
"license": "proprietary",
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
"repositories": [
|
|
||||||
{
|
|
||||||
"type": "vcs",
|
|
||||||
"url": "https://github.com/afornerot/bNine-MdEditorBundle"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.2",
|
"php": ">=8.2",
|
||||||
"ext-ctype": "*",
|
"ext-ctype": "*",
|
||||||
"ext-iconv": "*",
|
"ext-iconv": "*",
|
||||||
"apereo/phpcas": "^1.6",
|
"apereo/phpcas": "^1.6",
|
||||||
"bnine/filesbundle": "^1.0",
|
"bnine/filesbundle": "^1.0",
|
||||||
"bnine/mdeditorbundle": "*",
|
"bnine/mdeditorbundle": "^1.1",
|
||||||
"doctrine/dbal": "^3",
|
"doctrine/dbal": "^3",
|
||||||
"doctrine/doctrine-bundle": "^2.13",
|
"doctrine/doctrine-bundle": "^2.13",
|
||||||
"doctrine/doctrine-migrations-bundle": "^3.3",
|
"doctrine/doctrine-migrations-bundle": "^3.3",
|
||||||
|
17
composer.lock
generated
17
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "6f7395c5fc09ba0bc1a2325b862a69c7",
|
"content-hash": "2d59dc750783494ae1dbf58d9a3025c3",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "apereo/phpcas",
|
"name": "apereo/phpcas",
|
||||||
@ -140,16 +140,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "bnine/mdeditorbundle",
|
"name": "bnine/mdeditorbundle",
|
||||||
"version": "v0.1.5",
|
"version": "v1.1.3",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/afornerot/bNine-MdEditorBundle.git",
|
"url": "https://github.com/afornerot/bNine-MdEditorBundle.git",
|
||||||
"reference": "b5897339b8aed8f32578b3ebdf1e9ffa4ef83fa0"
|
"reference": "ad2c2829177a6788452fde1d70170ba5c5c6d0d6"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/afornerot/bNine-MdEditorBundle/zipball/b5897339b8aed8f32578b3ebdf1e9ffa4ef83fa0",
|
"url": "https://api.github.com/repos/afornerot/bNine-MdEditorBundle/zipball/ad2c2829177a6788452fde1d70170ba5c5c6d0d6",
|
||||||
"reference": "b5897339b8aed8f32578b3ebdf1e9ffa4ef83fa0",
|
"reference": "ad2c2829177a6788452fde1d70170ba5c5c6d0d6",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@ -173,6 +173,7 @@
|
|||||||
"Bnine\\MdEditorBundle\\": "src/"
|
"Bnine\\MdEditorBundle\\": "src/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
"license": [
|
"license": [
|
||||||
"MIT"
|
"MIT"
|
||||||
],
|
],
|
||||||
@ -191,10 +192,10 @@
|
|||||||
"symfony"
|
"symfony"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/afornerot/bNine-MdEditorBundle/tree/v0.1.5",
|
"issues": "https://github.com/afornerot/bNine-MdEditorBundle/issues",
|
||||||
"issues": "https://github.com/afornerot/bNine-MdEditorBundle/issues"
|
"source": "https://github.com/afornerot/bNine-MdEditorBundle/tree/v1.1.3"
|
||||||
},
|
},
|
||||||
"time": "2025-08-02T20:06:19+00:00"
|
"time": "2025-08-02T20:38:10+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
|
@ -29,4 +29,3 @@ services:
|
|||||||
App\Security\DynamicAuthenticator:
|
App\Security\DynamicAuthenticator:
|
||||||
arguments:
|
arguments:
|
||||||
$modeAuth: '%env(MODE_AUTH)%'
|
$modeAuth: '%env(MODE_AUTH)%'
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ class ProjectController extends AbstractController
|
|||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
$em->flush();
|
$em->flush();
|
||||||
|
|
||||||
return $this->redirectToRoute('app_admin_project');
|
// return $this->redirectToRoute('app_admin_project');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->render('project/edit.html.twig', [
|
return $this->render('project/edit.html.twig', [
|
||||||
|
@ -8,8 +8,18 @@ use Doctrine\Common\Collections\Collection;
|
|||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: ProjectRepository::class)]
|
#[ORM\Entity(repositoryClass: ProjectRepository::class)]
|
||||||
|
#[ORM\HasLifecycleCallbacks]
|
||||||
class Project
|
class Project
|
||||||
{
|
{
|
||||||
|
public const DRAFT = 'Brouillon';
|
||||||
|
public const TOVOTE = 'A Voter';
|
||||||
|
public const VOTED = 'Voté';
|
||||||
|
public const ARCHIVED = 'Archivé';
|
||||||
|
|
||||||
|
public const NATURE_COLLECTIVE = 'Collective';
|
||||||
|
public const NATURE_STRATEGIC = 'Stratégique';
|
||||||
|
public const NATURE_TACTICAL = 'Tactique';
|
||||||
|
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
@ -18,11 +28,20 @@ class Project
|
|||||||
#[ORM\Column(length: 255, unique: true)]
|
#[ORM\Column(length: 255, unique: true)]
|
||||||
private ?string $title = null;
|
private ?string $title = null;
|
||||||
|
|
||||||
#[ORM\Column()]
|
#[ORM\Column(type: 'text')]
|
||||||
private int $status;
|
private string $summary;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'text', nullable: true)]
|
||||||
|
private ?string $description = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 20)]
|
||||||
|
private string $nature;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private string $status;
|
||||||
|
|
||||||
#[ORM\Column(type: 'datetime', nullable: true)]
|
#[ORM\Column(type: 'datetime', nullable: true)]
|
||||||
private ?\DateTimeInterface $updateAt = null;
|
private ?\DateTimeInterface $dueDate = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, User>
|
* @var Collection<int, User>
|
||||||
@ -30,9 +49,17 @@ class Project
|
|||||||
#[ORM\ManyToMany(targetEntity: User::class, mappedBy: 'projects')]
|
#[ORM\ManyToMany(targetEntity: User::class, mappedBy: 'projects')]
|
||||||
private Collection $users;
|
private Collection $users;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, ProjectTimeline>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectTimeline::class, cascade: ['remove'])]
|
||||||
|
#[ORM\OrderBy(['createdAt' => 'DESC'])]
|
||||||
|
private Collection $timelines;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->users = new ArrayCollection();
|
$this->users = new ArrayCollection();
|
||||||
|
$this->timelines = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
@ -52,30 +79,66 @@ class Project
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUpdateAt(): ?\DateTimeInterface
|
public function getSummary(): string
|
||||||
{
|
{
|
||||||
return $this->updateAt;
|
return $this->summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setUpdateAt(?\DateTimeInterface $updateAt): static
|
public function setSummary(string $summary): static
|
||||||
{
|
{
|
||||||
$this->updateAt = $updateAt;
|
$this->summary = $summary;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getStatus(): ?int
|
public function getDescription(): ?string
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDescription(?string $description): static
|
||||||
|
{
|
||||||
|
$this->description = $description;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNature(): string
|
||||||
|
{
|
||||||
|
return $this->nature;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setNature(string $nature): static
|
||||||
|
{
|
||||||
|
$this->nature = $nature;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatus(): string
|
||||||
{
|
{
|
||||||
return $this->status;
|
return $this->status;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setStatus(int $status): static
|
public function setStatus(string $status): static
|
||||||
{
|
{
|
||||||
$this->status = $status;
|
$this->status = $status;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getDueDate(): ?\DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->dueDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDueDate(?\DateTimeInterface $dueDate): static
|
||||||
|
{
|
||||||
|
$this->dueDate = $dueDate;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Collection<int, User>
|
* @return Collection<int, User>
|
||||||
*/
|
*/
|
||||||
@ -102,4 +165,9 @@ class Project
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getTimelines(): Collection
|
||||||
|
{
|
||||||
|
return $this->timelines;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
86
src/Entity/ProjectTimeline.php
Normal file
86
src/Entity/ProjectTimeline.php
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\ProjectTimelineRepository;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: ProjectTimelineRepository::class)]
|
||||||
|
class ProjectTimeline
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Project::class, inversedBy: 'timelines')]
|
||||||
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
|
private Project $project;
|
||||||
|
|
||||||
|
#[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 getProject(): Project
|
||||||
|
{
|
||||||
|
return $this->project;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setProject(Project $project): static
|
||||||
|
{
|
||||||
|
$this->project = $project;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
155
src/EventListener/ProjectListener.php
Normal file
155
src/EventListener/ProjectListener.php
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\EventListener;
|
||||||
|
|
||||||
|
use App\Entity\Project;
|
||||||
|
use App\Entity\ProjectTimeline;
|
||||||
|
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 ProjectListener
|
||||||
|
{
|
||||||
|
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 Project) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$timeline = new ProjectTimeline();
|
||||||
|
$timeline->setProject($entity);
|
||||||
|
$timeline->setUser($user);
|
||||||
|
$timeline->setCreatedAt(new \DateTime());
|
||||||
|
$timeline->setDescription(['created' => true]);
|
||||||
|
|
||||||
|
$em->persist($timeline);
|
||||||
|
$meta = $em->getClassMetadata(ProjectTimeline::class);
|
||||||
|
$uow->computeChangeSet($meta, $timeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||||
|
if (!$entity instanceof Project) {
|
||||||
|
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 ProjectTimeline();
|
||||||
|
$timeline->setProject($entity);
|
||||||
|
$timeline->setUser($user);
|
||||||
|
$timeline->setCreatedAt(new \DateTime());
|
||||||
|
$timeline->setDescription($changes);
|
||||||
|
|
||||||
|
$em->persist($timeline);
|
||||||
|
$meta = $em->getClassMetadata(ProjectTimeline::class);
|
||||||
|
$uow->computeChangeSet($meta, $timeline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledCollectionUpdates() as $col) {
|
||||||
|
$owner = $col->getOwner();
|
||||||
|
|
||||||
|
if (!$owner instanceof Project) {
|
||||||
|
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 ProjectTimeline();
|
||||||
|
$timeline->setProject($owner);
|
||||||
|
$timeline->setUser($user);
|
||||||
|
$timeline->setCreatedAt(new \DateTime());
|
||||||
|
$timeline->setDescription($changes);
|
||||||
|
|
||||||
|
$em->persist($timeline);
|
||||||
|
$meta = $em->getClassMetadata(ProjectTimeline::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);
|
||||||
|
}
|
||||||
|
}
|
@ -4,10 +4,12 @@ namespace App\Form;
|
|||||||
|
|
||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
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\SubmitType;
|
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\Extension\Core\Type\TextType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
@ -26,8 +28,32 @@ class ProjectType extends AbstractType
|
|||||||
'label' => 'Titre',
|
'label' => 'Titre',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
->add('summary', TextareaType::class, [
|
||||||
|
'label' => 'Résumé',
|
||||||
|
])
|
||||||
|
|
||||||
->add('status', ChoiceType::class, [
|
->add('status', ChoiceType::class, [
|
||||||
'choices' => ['Brouillon' => 0],
|
'label' => 'Statut',
|
||||||
|
'choices' => [
|
||||||
|
Project::DRAFT => Project::DRAFT,
|
||||||
|
Project::TOVOTE => Project::TOVOTE,
|
||||||
|
Project::VOTED => Project::VOTED,
|
||||||
|
Project::ARCHIVED => Project::ARCHIVED,
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
->add('nature', ChoiceType::class, [
|
||||||
|
'label' => 'Nature',
|
||||||
|
'choices' => [
|
||||||
|
Project::NATURE_COLLECTIVE => Project::NATURE_COLLECTIVE,
|
||||||
|
Project::NATURE_STRATEGIC => Project::NATURE_STRATEGIC,
|
||||||
|
Project::NATURE_TACTICAL => Project::NATURE_TACTICAL,
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
->add('description', MarkdownType::class, [
|
||||||
|
'label' => 'Description du Projet',
|
||||||
|
'markdown_height' => 900,
|
||||||
])
|
])
|
||||||
|
|
||||||
->add('users', EntityType::class, [
|
->add('users', EntityType::class, [
|
||||||
|
18
src/Repository/ProjectTimelineRepository.php
Normal file
18
src/Repository/ProjectTimelineRepository.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\ProjectTimeline;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<ProjectTimeline>
|
||||||
|
*/
|
||||||
|
class ProjectTimelineRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, ProjectTimeline::class);
|
||||||
|
}
|
||||||
|
}
|
60
src/Security/ProjectVoter.php
Normal file
60
src/Security/ProjectVoter.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Security;
|
||||||
|
|
||||||
|
use App\Entity\Project;
|
||||||
|
use App\Entity\User;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
|
|
||||||
|
class ProjectVoter extends Voter
|
||||||
|
{
|
||||||
|
// Les actions que ce voter supporte
|
||||||
|
private const EDIT = 'EDIT';
|
||||||
|
private const VIEW = 'VIEW';
|
||||||
|
private const DELETE = 'DELETE';
|
||||||
|
private const MOVEDRAFT = 'MOVEDRAFT';
|
||||||
|
|
||||||
|
protected function supports(string $attribute, $subject): bool
|
||||||
|
{
|
||||||
|
$attributes = [self::EDIT, self::VIEW, self::DELETE, self::MOVEDRAFT];
|
||||||
|
|
||||||
|
return in_array($attribute, $attributes) && $subject instanceof Project;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function canView(Project $project, User $user): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function canEdit(Project $project, User $user): bool
|
||||||
|
{
|
||||||
|
return $user->hasRole('ROLE_ADMIN') || (Project::DRAFT === $project->getStatus() && $project->getUsers()->contains($user));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function canDelete(Project $project, User $user): bool
|
||||||
|
{
|
||||||
|
return $user->hasRole('ROLE_ADMIN') || (Project::DRAFT === $project->getStatus() && $project->getUsers()->contains($user));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function canMoveDraft(Project $project, User $user): bool
|
||||||
|
{
|
||||||
|
return $user->hasRole('ROLE_ADMIN') || (Project::TOVOTE === $project->getStatus() && $project->getUsers()->contains($user));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function voteOnAttribute(string $attribute, $project, TokenInterface $token): bool
|
||||||
|
{
|
||||||
|
$user = $token->getUser();
|
||||||
|
if (!$user instanceof User) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return match ($attribute) {
|
||||||
|
self::VIEW => $this->canView($project, $user),
|
||||||
|
self::EDIT => $this->canEdit($project, $user),
|
||||||
|
self::DELETE => $this->canDelete($project, $user),
|
||||||
|
self::MOVEDRAFT => $this->canMoveDraft($project, $user),
|
||||||
|
default => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,7 @@
|
|||||||
"version": "v1.0.4"
|
"version": "v1.0.4"
|
||||||
},
|
},
|
||||||
"bnine/mdeditorbundle": {
|
"bnine/mdeditorbundle": {
|
||||||
"version": "v0.1.1"
|
"version": "v1.1.1"
|
||||||
},
|
},
|
||||||
"doctrine/deprecations": {
|
"doctrine/deprecations": {
|
||||||
"version": "1.1",
|
"version": "1.1",
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
<script src="{{ asset('lib/jquery/jquery.min.js') }}"></script>
|
<script src="{{ asset('lib/jquery/jquery.min.js') }}"></script>
|
||||||
<script src="{{ asset('lib/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
|
<script src="{{ asset('lib/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
|
||||||
<script src="{{ asset('lib/fontawesome/fontawesome-free.index.js') }}"></script>
|
<!-- <script src="{{ asset('lib/fontawesome/fontawesome-free.index.js') }}"></script> -->
|
||||||
<script src="{{ asset('lib/datatables/datatables.min.js') }}"></script>
|
<script src="{{ asset('lib/datatables/datatables.min.js') }}"></script>
|
||||||
<script src="{{ asset('lib/datatables/datatables.init.js') }}"></script>
|
<script src="{{ asset('lib/datatables/datatables.init.js') }}"></script>
|
||||||
<script src="{{ asset('lib/select2/select2.min.js') }}"></script>
|
<script src="{{ asset('lib/select2/select2.min.js') }}"></script>
|
||||||
@ -30,6 +30,7 @@
|
|||||||
<script src="{{ asset('lib/jqueryui/jquery-ui.min.js') }}"></script>
|
<script src="{{ asset('lib/jqueryui/jquery-ui.min.js') }}"></script>
|
||||||
<script src="{{ asset('lib/chart/chart.js') }}"></script>
|
<script src="{{ asset('lib/chart/chart.js') }}"></script>
|
||||||
|
|
||||||
|
<script src="{{ asset('bundles/bninemdeditor/bninemdeditor.js') }}"></script>
|
||||||
<script src="{{ asset('lib/app/app.js') }}"></script>
|
<script src="{{ asset('lib/app/app.js') }}"></script>
|
||||||
|
|
||||||
{% block javascripts %}
|
{% block javascripts %}
|
||||||
|
@ -1,12 +1,25 @@
|
|||||||
{% extends 'base.html.twig' %}
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{%block body%}
|
{%block body%}
|
||||||
<h2>Projets</h2>
|
<h2>Projets</h2>
|
||||||
|
<div class='d-flex' style='justify-content: left'>
|
||||||
<div class='d-flex' style='justify-content: center'>
|
|
||||||
{% for project in projects %}
|
{% for project in projects %}
|
||||||
<div class='card'>
|
<div class='card' style='width:300px'>
|
||||||
<h5>{{project.title}}</h5>
|
<div class='card-header d-flex justify-content-between align-items-center'>
|
||||||
{{ render(path("bninefiles_files",{domain:'project',id:project.id, editable:0})) }}
|
{{project.title}}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{% if is_granted('EDIT', project) %}
|
||||||
|
<button type="button" class="btn btn-primary btn-sm"><i class="fas fa-pencil"></i></button>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm"><i class="fas fa-eye"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='card-body'>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
@ -14,31 +14,40 @@
|
|||||||
{% include('include/error.html.twig') %}
|
{% include('include/error.html.twig') %}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 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">
|
||||||
{{ form_row(form.title) }}
|
{{ form_row(form.title) }}
|
||||||
|
{{ form_row(form.nature) }}
|
||||||
{{ form_row(form.status) }}
|
{{ form_row(form.status) }}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6 mx-auto">
|
|
||||||
<div class="card mt-3">
|
|
||||||
<div class="card-header">Permissions</div>
|
|
||||||
<div class="card-body">
|
|
||||||
{{ form_row(form.users) }}
|
{{ form_row(form.users) }}
|
||||||
</div>
|
{{ form_row(form.summary) }}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if mode=="update" %}
|
|
||||||
{{ render(path("bninefiles_files",{domain:'project',id:project.id, editable:1})) }}
|
{{ render(path("bninefiles_files",{domain:'project',id:project.id, editable:1})) }}
|
||||||
{% endif %}
|
|
||||||
{{ form_end(form) }}
|
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">Timeline</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% include('project/timeline.html.twig') %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-8 mx-auto">
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">Description du Projet</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{{ form_widget(form.description) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ form_end(form) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block localscript %}
|
{% block localscript %}
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
<a href="{{ path(routeupdate,{id:project.id}) }}" class="me-2"><i class="fas fa-file fa-2x"></i></a>
|
<a href="{{ path(routeupdate,{id:project.id}) }}" class="me-2"><i class="fas fa-file fa-2x"></i></a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{project.title}}</td>
|
<td>{{project.title}}</td>
|
||||||
<td>{{project.updateAt}}</td>
|
<td>{{project.timelines.first ? project.timelines.first.createdAt|date('d/m/Y H:i') : '' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
57
templates/project/timeline.html.twig
Normal file
57
templates/project/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 project.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 %}
|
||||||
|
= <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>
|
Reference in New Issue
Block a user