diff --git a/src/Controller/ApiBaseController.php b/src/Controller/ApiBaseController.php index f14817729cc89ae9a2cf960c33dd6d22976e8869..aa31272cccbdfc404ddb652e2219382088e711c8 100644 --- a/src/Controller/ApiBaseController.php +++ b/src/Controller/ApiBaseController.php @@ -320,7 +320,9 @@ class ApiBaseController extends AbstractController */ public function mergeEntries(Entry $targetEntry, Entry $sourceEntry) { - $targetEntry->setComments($targetEntry->getComments() . "\n\nCommentaires ajoutés suite à une fusion:\n" . $sourceEntry->getComments()); + if ($sourceEntry->getComments() && $sourceEntry->getComments() != $targetEntry->getComments()) { + $targetEntry->setCommentsBy($targetEntry->getComments() . "\n\nCommentaires ajoutés suite à une fusion:\n" . $sourceEntry->getComments(), $this->getUser()); + } $sourceAttributes = $sourceEntry->getAttributes(); $targetAttributes = $targetEntry->getAttributes(); diff --git a/src/Controller/ApiEntryController.php b/src/Controller/ApiEntryController.php index 22fb564871f819eaaebf6386f76f33eb0c6abfa8..00c659395712736e801293f6a655386dfc2d6817 100644 --- a/src/Controller/ApiEntryController.php +++ b/src/Controller/ApiEntryController.php @@ -185,7 +185,7 @@ class ApiEntryController extends ApiBaseController } /** - * Copie une sélection d'entrée vers des lexiques cibles. Si force=true, on crée l'entrée même si le mot n'est pas trouvé dans le wiktionnaire + * Copie une sélection d'entrée vers des lexiques cibles. Si force=true, on crée l'entrée même si le mot n'est pas trouvé dans le wiktionnaire * * @Route("/copy", name="api_copy_entries", methods={"POST"}) * @@ -270,7 +270,7 @@ class ApiEntryController extends ApiBaseController switch ($data['merge']) { case Lexicon::MERGE_OVERWRITE: // On remplace l'entrée cible si elle existe $targetEntry->setAttributes($entry->getAttributes()); - $targetEntry->setComments($entry->getComments()); + $targetEntry->setCommentsBy($entry->getComments(), $this->getUser()); $overwritten[] = $targetEntry->getId(); break; case Lexicon::MERGE_IGNORE: // On ne fait rien si l'entrée cible existe @@ -394,8 +394,8 @@ class ApiEntryController extends ApiBaseController return $this->createJsonResponse(401, ['error' => $message]); } else { $entry->setAttributes($data['attributes']); - if ($data['comment'] ?? null) { - $entry->setComments($entry->getComments() . "\n" . $data['comment']); + if ($data['comment'] ?? null && $data['comment'] != $entry->getComments()) { + $entry->setCommentsBy($entry->getComments() . "\n" . $data['comment'], $this->getUser()); } $this->doctrine->getManager()->flush(); } diff --git a/src/Controller/ApiTraceController.php b/src/Controller/ApiTraceController.php new file mode 100644 index 0000000000000000000000000000000000000000..7e23ec5fc6eef85104ceb2e2cc9568ec71af5c9e --- /dev/null +++ b/src/Controller/ApiTraceController.php @@ -0,0 +1,75 @@ +<?php + +namespace App\Controller; + +use App\Entity\Entry; +use App\Entity\Graphy; +use App\Entity\Headword; +use App\Entity\Label; +use App\Entity\Lexicon; +use App\Entity\Trace; +use App\Manager\WiktionaryManager; +use App\Repository\LabelRepository; +use Doctrine\Persistence\ManagerRegistry; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; +use Nelmio\ApiDocBundle\Annotation\Security; +use OpenApi\Annotations as OA; +use Nelmio\ApiDocBundle\Annotation\Model; +use Symfony\Component\Serializer\SerializerInterface; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; + +/** + * @Route("/api/trace") + */ +class ApiTraceController extends ApiBaseController +{ + /** + * + * @Route("/create", name="api_trace_create", methods={"POST"}) + * + * @OA\Response(response=200, description="success", @OA\JsonContent(type="string")) + * @OA\Response(response=401, description="error", @OA\JsonContent(type="string")) + * @OA\Response(response=403, description="error", @OA\JsonContent(type="string")) + * @OA\Response(response=500, description="error", @OA\JsonContent(type="string")) + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"origin", "action"}, + * @OA\Property(property="origin", type="string", example="Prisms"), + * @OA\Property(property="action", type="string", example="partie annulée"), + * @OA\Property(property="additional_info", type="object"), + * ) + * ) + * @OA\Tag(name="Traces") + * @Security(name="OAuth2") + */ + public function createTrace(Request $request): Response + { + $data = json_decode($request->getContent(), true); + if (!$data) { + return $this->createJsonResponse(401, ['error' => sprintf("Json non valide")]); + } + if ($missingFields = $this->getMissingFields($data, ['origin', 'action'])) { + return $this->createJsonResponse(401, ['error' => sprintf("Veuillez fournir une valeur pour: %s", implode(', ', $missingFields))]); + } + + $trace = new Trace(); + $trace->setCreatedBy($this->getUser()); + $trace->setAction($data['action']); + $trace->setOrigin($data['origin']); + if ($data['additional_info'] ?? null) { + $trace->setAdditionalInfo($data['additional_info']); + } + $this->doctrine->getManager()->persist($trace); + $this->doctrine->getManager()->flush(); + + $this->success[] = sprintf("Trace id %s créée.", $trace->getId()); + + return $this->createJsonResponse(200); + } +} diff --git a/src/Entity/Entry.php b/src/Entity/Entry.php index dcd073ae7a80b52be445343e6633d1adb7ea7934..945fcf529d821e547293ca8adfd4b2975614bc9c 100644 --- a/src/Entity/Entry.php +++ b/src/Entity/Entry.php @@ -3,6 +3,8 @@ namespace App\Entity; use App\Repository\EntryRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; use OpenApi\Annotations as OA; @@ -19,6 +21,8 @@ use OpenApi\Annotations as OA; */ class Entry { + use LoggableTrait; + /** * @ORM\Id * @ORM\GeneratedValue @@ -39,6 +43,12 @@ class Entry */ private $comments; + /** + * @Groups({"entry:read"}) + * @ORM\Column(type="text", nullable=true) + */ + private $discussion; + /** * @Groups({"entry:read"}) * @ORM\Column(type="string", length=100) @@ -69,8 +79,14 @@ class Entry */ private $lexicon; + /** + * @ORM\OneToMany(targetEntity=Log::class, mappedBy="lexicon", cascade={"remove", "persist"}) + */ + private $logs; + public function __construct() { + $this->logs = new ArrayCollection(); } public function __toString() @@ -124,7 +140,7 @@ class Entry return $this->comments; } - public function setComments(?string $comments): self + private function setComments(?string $comments): self { $this->comments = $comments; @@ -190,4 +206,46 @@ class Entry return $this; } + + public function getDiscussion(): ?string + { + return $this->discussion; + } + + private function setDiscussion(?string $discussion): self + { + $this->discussion = $discussion; + + return $this; + } + + /** + * @return Collection<int, Log> + */ + public function getLogs(): Collection + { + return $this->logs; + } + + public function addLog(Log $log): self + { + if (!$this->logs->contains($log)) { + $this->logs[] = $log; + $log->setLexicon($this); + } + + return $this; + } + + public function removeLog(Log $log): self + { + if ($this->logs->removeElement($log)) { + // set the owning side to null (unless already changed) + if ($log->getLexicon() === $this) { + $log->setLexicon(null); + } + } + + return $this; + } } diff --git a/src/Entity/Headword.php b/src/Entity/Headword.php index 5fff1a73f13b5589d32f5eafde7436078dc98636..0a901efec6da879c4863bbca8e6d6a77d5a438b1 100644 --- a/src/Entity/Headword.php +++ b/src/Entity/Headword.php @@ -2,7 +2,6 @@ namespace App\Entity; -use ApiPlatform\Core\Annotation\ApiResource; use App\Repository\HeadwordRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; diff --git a/src/Entity/Lexicon.php b/src/Entity/Lexicon.php index 80fe18529edec312e6cc44382bba6c2612e8b575..8d4b5e8b13784d66486ce51eb3d08c88153776c4 100644 --- a/src/Entity/Lexicon.php +++ b/src/Entity/Lexicon.php @@ -2,7 +2,6 @@ namespace App\Entity; -use ApiPlatform\Core\Annotation\ApiResource; use App\Repository\LexiconRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -15,6 +14,9 @@ use Symfony\Component\Serializer\Annotation\Groups; */ class Lexicon { + use LoggableTrait; + + const TYPE_USER = 'User'; const TYPE_GROUP = 'Group'; const TYPE_ZERO = 'Zero'; @@ -57,6 +59,18 @@ class Lexicon */ private $category; + /** + * @Groups({"lexicon:read"}) + * @ORM\Column(type="text", nullable=true) + */ + private $comments; + + /** + * @Groups({"lexicon:read"}) + * @ORM\Column(type="text", nullable=true) + */ + private $discussion; + /** * @ORM\OneToOne(targetEntity=User::class, mappedBy="lexicon") * @Groups({"lexicon:read", "lexicon:write"}) @@ -79,6 +93,11 @@ class Lexicon */ private $graphyLists; + /** + * @ORM\OneToMany(targetEntity=Log::class, mappedBy="lexicon", cascade={"remove", "persist"}) + */ + private $logs; + public function __toString() { switch ($this->getCategory()) { @@ -93,6 +112,7 @@ class Lexicon { $this->entries = new ArrayCollection(); $this->graphyLists = new ArrayCollection(); + $this->logs = new ArrayCollection(); } public function isZero() @@ -259,4 +279,58 @@ class Lexicon return $this; } + + public function getComments(): ?string + { + return $this->comments; + } + + private function setComments(?string $comments): self + { + $this->comments = $comments; + + return $this; + } + + public function getDiscussion(): ?string + { + return $this->discussion; + } + + private function setDiscussion(?string $discussion): self + { + $this->discussion = $discussion; + + return $this; + } + + /** + * @return Collection<int, Log> + */ + public function getLogs(): Collection + { + return $this->logs; + } + + public function addLog(Log $log): self + { + if (!$this->logs->contains($log)) { + $this->logs[] = $log; + $log->setLexicon($this); + } + + return $this; + } + + public function removeLog(Log $log): self + { + if ($this->logs->removeElement($log)) { + // set the owning side to null (unless already changed) + if ($log->getLexicon() === $this) { + $log->setLexicon(null); + } + } + + return $this; + } } diff --git a/src/Entity/Log.php b/src/Entity/Log.php new file mode 100644 index 0000000000000000000000000000000000000000..32cc1590ed4ec59cf876df6a2cd85c0136527e43 --- /dev/null +++ b/src/Entity/Log.php @@ -0,0 +1,151 @@ +<?php + +namespace App\Entity; + +use App\Repository\LexiconRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * @ORM\Entity(repositoryClass=LexiconRepository::class) + * @ORM\HasLifecycleCallbacks + */ +class Log +{ + const CATEGORY_UPDATE = 'Update'; + + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + private $id; + + /** + * @Groups({"log:read"}) + * @ORM\Column(type="datetime_immutable") + */ + private $createdAt; + + /** + * @ORM\ManyToOne(targetEntity=User::class, inversedBy="logs") + * @Groups({"log:read"}) + */ + private $createdBy; + + /** + * @ORM\Column(type="string", length=80) + * @Groups({"log:read"}) + */ + private $category; + + /** + * @Groups({"log:read"}) + * @ORM\Column(type="text", nullable=true) + */ + private $content; + + /** + * @ORM\ManyToOne(targetEntity=Lexicon::class, inversedBy="logs") + */ + private $lexicon; + + /** + * @ORM\ManyToOne(targetEntity=Entry::class, inversedBy="logs") + */ + private $entry; + + public function __toString() + { + return $this->getCategory() . ' du ' . $this->getCreatedAt()->format('d/m/Y à h:i:s'); + } + + public function __construct() + { + $this->createdAt = new \DateTimeImmutable(); + } + + public function isUpdate() + { + return $this->getCategory() === self::CATEGORY_UPDATE; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): self + { + $this->createdAt = $createdAt; + + return $this; + } + + public function getContent(): ?string + { + return $this->content; + } + + public function setContent(?string $content): self + { + $this->content = $content; + + return $this; + } + + public function getCreatedBy(): ?User + { + return $this->createdBy; + } + + public function setCreatedBy(?User $createdBy): self + { + $this->createdBy = $createdBy; + + return $this; + } + + public function getLexicon(): ?Lexicon + { + return $this->lexicon; + } + + public function setLexicon(?Lexicon $lexicon): self + { + $this->lexicon = $lexicon; + + return $this; + } + + public function getEntry(): ?Entry + { + return $this->entry; + } + + public function setEntry(?Entry $entry): self + { + $this->entry = $entry; + + return $this; + } + + public function getCategory(): ?string + { + return $this->category; + } + + public function setCategory(string $category): self + { + $this->category = $category; + + return $this; + } +} diff --git a/src/Entity/LoggableTrait.php b/src/Entity/LoggableTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..cfa426704b6ad9f7e115bf9403bbe3bb8f9574d4 --- /dev/null +++ b/src/Entity/LoggableTrait.php @@ -0,0 +1,28 @@ +<?php + + +namespace App\Entity; + + +trait LoggableTrait +{ + public function setCommentsBy($comments, User $user) + { + $log = new Log(); + $log->setCreatedBy($user); + $log->setContent($this->getComments()); + $this->setComments($comments); + + return $this; + } + + public function setDiscussionBy($discussions, User $user) + { + $log = new Log(); + $log->setCreatedBy($user); + $log->setContent($this->getDiscussions()); + $this->setDiscussions($discussions); + + return $this; + } +} \ No newline at end of file diff --git a/src/Entity/Trace.php b/src/Entity/Trace.php new file mode 100644 index 0000000000000000000000000000000000000000..e866f9d36a98b1c53e0dff1fc445ed1385df4a00 --- /dev/null +++ b/src/Entity/Trace.php @@ -0,0 +1,130 @@ +<?php + +namespace App\Entity; + +use App\Repository\LexiconRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; +use OpenApi\Annotations as OA; + +/** + * @ORM\Entity(repositoryClass=LexiconRepository::class) + * @ORM\HasLifecycleCallbacks + */ +class Trace +{ + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + private $id; + + /** + * @Groups({"trace:read"}) + * @ORM\Column(type="datetime_immutable") + */ + private $createdAt; + + /** + * @ORM\ManyToOne(targetEntity=User::class, inversedBy="traces") + * @Groups({"trace:read"}) + */ + private $createdBy; + + /** + * @Groups({"trace:read"}) + * @ORM\Column(type="string", nullable=true) + */ + private $origin; + + /** + * @Groups({"trace:read"}) + * @ORM\Column(type="string", nullable=true) + */ + private $action; + + /** + * @ORM\Column(type="json", nullable=true) + * @Groups({"trace:read"}) + * @OA\Property(type="json") + */ + private $additionalInfo = []; + + public function __toString() + { + return $this->getAction() . ' par ' . $this->getCreatedBy() . ' depuis ' . $this->getOrigin() . ' le ' . $this->getCreatedAt()->format('d/m/Y à h:i:s'); + } + + public function __construct() + { + $this->createdAt = new \DateTimeImmutable(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): self + { + $this->createdAt = $createdAt; + + return $this; + } + + public function getOrigin(): ?string + { + return $this->origin; + } + + public function setOrigin(?string $origin): self + { + $this->origin = $origin; + + return $this; + } + + public function getAction(): ?string + { + return $this->action; + } + + public function setAction(?string $action): self + { + $this->action = $action; + + return $this; + } + + public function getAdditionalInfo(): ?array + { + return $this->additionalInfo; + } + + public function setAdditionalInfo(?array $additionalInfo): self + { + $this->additionalInfo = $additionalInfo; + + return $this; + } + + public function getCreatedBy(): ?User + { + return $this->createdBy; + } + + public function setCreatedBy(?User $createdBy): self + { + $this->createdBy = $createdBy; + + return $this; + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php index e5c9d03cde3b160bf8439198077d3ca0bcd1ff05..063a7e8a99d5d8bfa551e328c8a294e4a8aaabcf 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -197,6 +197,16 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface */ private $groupMemberships; + /** + * @ORM\OneToMany(targetEntity=Log::class, mappedBy="createdBy", cascade={"remove"}) + */ + private $logs; + + /** + * @ORM\OneToMany(targetEntity=Trace::class, mappedBy="createdBy", cascade={"remove"}) + */ + private $traces; + public function __toString() { return $this->getPseudo(); @@ -217,6 +227,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface $this->groupMemberships = new ArrayCollection(); $this->labels = new ArrayCollection(); $this->createdGraphyLists = new ArrayCollection(); + $this->logs = new ArrayCollection(); } /** @@ -773,4 +784,34 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + + /** + * @return Collection<int, Log> + */ + public function getLogs(): Collection + { + return $this->logs; + } + + public function addLog(Log $log): self + { + if (!$this->logs->contains($log)) { + $this->logs[] = $log; + $log->setCreatedBy($this); + } + + return $this; + } + + public function removeLog(Log $log): self + { + if ($this->logs->removeElement($log)) { + // set the owning side to null (unless already changed) + if ($log->getCreatedBy() === $this) { + $log->setCreatedBy(null); + } + } + + return $this; + } }