diff --git a/config/packages/nelmio_api_doc.yaml b/config/packages/nelmio_api_doc.yaml index 70280858833d07d19b32a9ada23812dc09b650aa..9ec5a78abe3817d82ac01e5bbf2e00418e1ff713 100644 --- a/config/packages/nelmio_api_doc.yaml +++ b/config/packages/nelmio_api_doc.yaml @@ -12,6 +12,9 @@ nelmio_api_doc: bearerFormat: JWT security: - OAuth2: [] # On répète le Nom arbitraire, et il faut l'utiliser dans les controllers @Security(name="OAuth2") +# definitions: +# entry: +# $ref: ../JsonSchema/entrySchema.json areas: # to filter documented areas path_patterns: - ^/api(?!/doc$) # Accepts routes under /api except /api/doc diff --git a/src/Controller/ApiBaseController.php b/src/Controller/ApiBaseController.php index 123bd74b9e7eeb7b72a11091067a43e70a310543..d5afdd9e73c7da99f5f8723415b03864000d6133 100644 --- a/src/Controller/ApiBaseController.php +++ b/src/Controller/ApiBaseController.php @@ -293,6 +293,33 @@ class ApiBaseController extends AbstractController return $entry; } + /** + * - On ajoute les commentaires de l'entrée source à la suite de ceux de l'entrée cible + * - On ajoute les items de l'entrée source à la suite de ceux de l'entrée cible + * - On ajoute le label 'merged' au headword commun aux deux entrées + * + * @param Entry $targetEntry + * @param Entry $sourceEntry + * @return Entry + */ + public function mergeEntries(Entry $targetEntry, Entry $sourceEntry) + { + $targetEntry->setComments($targetEntry->getComments() . "\n\nCommentaires ajoutés suite à une fusion:\n" . $sourceEntry->getComments()); + + $sourceAttributes = $sourceEntry->getAttributes(); + $targetAttributes = $targetEntry->getAttributes(); + $targetAttributes['Items'] = array_merge_recursive($targetAttributes['Items'], $sourceAttributes['Items']); + $targetEntry->setAttributes($targetAttributes); + + $mergedLabel = $this->doctrine->getRepository(Label::class)->findOneBy([ + 'category' => Label::LABEL_CATEGORY_SYSTEM, + 'name' => Label::LABEL_MERGED, + ]); + $targetEntry->getHeadword()->addLabel($mergedLabel); + + return $targetEntry; + } + /** * @param Headword $headword * @return Entry|null diff --git a/src/Controller/ApiEntryController.php b/src/Controller/ApiEntryController.php index 7db43ba1bba0170e7924e77590f389f745cffe24..7c7ac61952947c6919044723bf0189ff18d636cc 100644 --- a/src/Controller/ApiEntryController.php +++ b/src/Controller/ApiEntryController.php @@ -9,6 +9,7 @@ use App\Entity\Lexicon; use App\Entity\User; use App\Manager\WiktionaryManager; use Doctrine\Persistence\ManagerRegistry; +use JsonSchema\Validator; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -61,7 +62,7 @@ class ApiEntryController extends ApiBaseController /** * Recherche d'une entrée par graphie et par langue, dans un ensemble de lexiques * - * @Route("/search", name="search", methods={"POST"}) + * @Route("/search", name="api_search", methods={"POST"}) * * @OA\Response(response=200, description="success", * @OA\JsonContent(type="array", @@ -115,7 +116,7 @@ class ApiEntryController extends ApiBaseController /** * Crée une entrée dans les lexiques spécifiés à partir d'un mot. Si force=true, on crée l'entrée même si le mot n'est pas trouvé dans le wiktionnaire * - * @Route("/create", name="create_entry", methods={"POST"}) + * @Route("/create", name="api_create_entry", methods={"POST"}) * * @OA\Response(response=200, description="success", @OA\JsonContent(type="string")) * @OA\Response(response=401, description="error", @OA\JsonContent(type="string")) @@ -184,7 +185,7 @@ class ApiEntryController extends ApiBaseController } /** - * Copie une sélection d'entrée vers des lexiques cibles. Copie également les labels spécifiés à partir d'un mot. 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"}) * @@ -202,7 +203,7 @@ class ApiEntryController extends ApiBaseController * @OA\Items( * type="integer" * ) - * ) + * ), * @OA\Property(property="entries", type="array", * example={1, 2}, * @OA\Items( @@ -233,7 +234,7 @@ class ApiEntryController extends ApiBaseController if (!in_array($data['merge'], Lexicon::MERGE_MODES)) { return $this->createJsonResponse(401, ['error' => sprintf("Le mode de fusion « merge » doit être overwrite OU ignore OU merge")]); } - $entries = $this->doctrine->getRepository(Entry::class)->find($data['entries']); + $entries = $this->doctrine->getRepository(Entry::class)->findById($data['entries']); if (!$entries) { return $this->createJsonResponse(401, ['error' => sprintf("Aucune entrée trouvée")]); } @@ -241,32 +242,71 @@ class ApiEntryController extends ApiBaseController if (!$lexicons) { return $this->createJsonResponse(401); } - - // On récupère ou on crée le mot-vedette - if (!$headword) { - if ($forceCreation || $wiktionaryManager->search($data['graphy'], $language)) { - $headword = $this->newHeadword($data['graphy'], $language); - } else { - return $this->createJsonResponse(401, ['warning' => sprintf("Le mot «%s» n'existe ni dans Balex ni dans le wiktionnaire.", $data['graphy'])]); - } + $language = $this->getLexiconsLanguage($lexicons); + if (!$language) { + return $this->createJsonResponse(401, ['error' => sprintf("Les lexiques cibles doivent tous appartenir à la même langue")]); } - // On crée l'entrée dans chaque lexique si elle n'y existe pas (et dans le lexique Zéro si possible) + $added = []; + $overwritten = []; + $ignored = []; + $merged = []; + // On crée l'entrée dans chaque lexique si elle n'y existe pas foreach ($lexicons as $lexicon) { - if (!$this->doctrine->getRepository(Entry::class)->findBy(['lexicon' => $lexicon, 'headword' => $headword])) { - $entry = $this->createEntryInLexicon($headword, $lexicon); - $this->success[] = sprintf("Entrée créée dans le lexique : %s.", $lexicon); - } else { - $this->warning[] = sprintf("Cette entrée est déjà présente dans le lexique %s.", $lexicon); + + foreach ($entries as $entry) { + + if ($language !== $entry->getLanguage()) { + return $this->createJsonResponse(401, ['error' => sprintf("L'entrée %s n'est pas dans la même langue que les lexiques cibles", $entry->getId())]); + } + + $targetEntry = $this->doctrine->getRepository(Entry::class)->findOneBy([ + 'headword' => $entry->getHeadword(), + 'lexicon' => $lexicon, + ]); + + // Si l'entrée existe déjà + if ($targetEntry) { + switch ($data['merge']) { + case Lexicon::MERGE_OVERWRITE: // On remplace l'entrée cible si elle existe + $targetEntry->setAttributes($entry->getAttributes()); + $targetEntry->setComments($entry->getComments()); + $overwritten[] = $targetEntry->getId(); + break; + case Lexicon::MERGE_IGNORE: // On ne fait rien si l'entrée cible existe + $ignored[] = $targetEntry->getId(); + break; + case Lexicon::MERGE_MERGE: // On fusionne les 2 entrées + $targetEntry = $this->mergeEntries($targetEntry, $entry); + $merged[] = $targetEntry->getId(); + break; + } + } else { + $newEntry = clone($entry); + $newEntry->setLexicon($lexicon); + $newEntry->setCreatedAt(new \DateTimeImmutable()); + $newEntry->setUpdatedAt(null); + $this->doctrine->getManager()->persist($newEntry); + $this->doctrine->getManager()->flush(); + $added[] = $newEntry->getId(); + } + } } $this->doctrine->getManager()->flush(); - return $this->createJsonResponse(200); + $result = array_filter([ + 'added' => $added, + 'overwritten' => $overwritten, + 'ignored' => $ignored, + 'merged' => $merged, + ]); + + return $this->createJsonResponse(200, $result); } /** - * @Route("/get/{id}", name="get_entry", methods={"GET"}) + * @Route("/get/{id}", name="api_get_entry", methods={"GET"}) * * @OA\Response( * response=200, @@ -285,6 +325,7 @@ class ApiEntryController extends ApiBaseController * description="id of the entry to be returned", * @OA\Schema(type="string") * ) + * * @OA\Tag(name="Entries") * @Security(name="OAuth2") */ @@ -300,7 +341,72 @@ class ApiEntryController extends ApiBaseController } /** - * @Route("/remove", name="entry_delete", methods={"DELETE"}) + * @Route("/edit/{id}", name="api_edit_entry", methods={"POST"}) + * + * @OA\Response( + * response=200, + * description="Success", + * @OA\JsonContent( + * ref=@Model(type=Entry::class, groups={"entry:read"}) + * ) + * ) + * @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\Parameter( + * name="id", + * in="path", + * description="id of the entry to edit", + * @OA\Schema(type="string") + * ) + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"attributes"}, + * @OA\Property(property="comment", type="string", example="commentaire optionnel ajouté à la suite des commentaires existants"), + * @OA\Property(property="attributes", type="object") + * ) + * ) + * @OA\Tag(name="Entries") + * @Security(name="OAuth2") + */ + public function editEntry(Request $request, SerializerInterface $serializer, Entry $entry = null) + { + if ($entry === null) { + return $this->createJsonResponse(401, ['error' => sprintf("Pas d'entrée trouvée pour cette Id")]); + } + $data = json_decode($request->getContent(), true); + if (!$data) { + return $this->createJsonResponse(401, ['error' => sprintf("Json non valide")]); + } + if ($missingFields = $this->getMissingFields($data, ['attributes'])) { + return $this->createJsonResponse(401, ['error' => sprintf("Veuillez fournir une valeur pour: %s", implode(', ', $missingFields))]); + } + $attributes = json_decode(json_encode($data['attributes'])); + $validator = new Validator(); + $validator->validate($attributes, (object) ['$ref' => __DIR__ . "/../JsonSchema/entrySchema.json"]); + if (!$validator->isValid()) { + $message = "JSON does not validate. Violations:\n"; + foreach ($validator->getErrors() as $error) { + $message .= sprintf("[%s] %s\n", $error['property'], $error['message']); + } + return $this->createJsonResponse(401, ['error' => $message]); + } else { + $entry->setAttributes($data['attributes']); + if ($data['comment'] ?? null) { + $entry->setComments($entry->getComments() . "\n" . $data['comment']); + } + $this->doctrine->getManager()->flush(); + } + + $updatedEntryData = $serializer->serialize($entry, 'json', ['groups' => ["entry:read", "headword:read", "lexicon:read"]]); + + return new JsonResponse($updatedEntryData, 200, [], true); + } + + /** + * @Route("/remove", name="api_entry_delete", methods={"DELETE"}) * * @OA\Response( * response=200, diff --git a/src/Controller/LexiconController.php b/src/Controller/LexiconController.php index 4e907e8ec0de1d14cafb28bc58e5e408f4a26bf8..e6e544f320e36c4804d437e6b777cc5da50d4651 100644 --- a/src/Controller/LexiconController.php +++ b/src/Controller/LexiconController.php @@ -2,6 +2,7 @@ namespace App\Controller; +use App\Entity\Entry; use App\Entity\Lexicon; use App\Repository\LexiconRepository; use Doctrine\Persistence\ManagerRegistry; @@ -22,6 +23,12 @@ class LexiconController extends AbstractController */ public function index(LexiconRepository $lexiconRepository): Response { +// $entry1 = $this->getDoctrine()->getRepository(Entry::class)->find(1); +// $entry2 = $this->getDoctrine()->getRepository(Entry::class)->find(9); +// +// +// dump($entry1->getAttributes(), $entry2->getAttributes(), array_merge_recursive($entry1->getAttributes()['Items'], $entry2->getAttributes()['Items']));die(); + return $this->render('lexicon/index.html.twig', [ 'lexicons' => $lexiconRepository->findAll(), ]); diff --git a/src/Entity/Entry.php b/src/Entity/Entry.php index 33e80d4b47c73d8dddcfd06ea99173d70adc315d..dcd073ae7a80b52be445343e6633d1adb7ea7934 100644 --- a/src/Entity/Entry.php +++ b/src/Entity/Entry.php @@ -34,6 +34,7 @@ class Entry private $attributes = []; /** + * @Groups({"entry:read"}) * @ORM\Column(type="text", nullable=true) */ private $comments; @@ -68,12 +69,6 @@ class Entry */ private $lexicon; - /** - * @Groups({"entry:write"}) - * @var Lexicon[] - */ - private $lexicons; - public function __construct() { } @@ -195,20 +190,4 @@ class Entry return $this; } - - /** - * @return Lexicon[] - */ - public function getLexicons(): array - { - return $this->lexicons; - } - - /** - * @param Lexicon[] $lexicons - */ - public function setLexicons(array $lexicons): void - { - $this->lexicons = $lexicons; - } } diff --git a/src/Security/Voter/LexiconVoter.php b/src/Security/Voter/LexiconVoter.php index f60c47b594c0893da5d3a881f9fe6be790bed76b..520dd6bc121bce455e70975e9fac0abe3ca281b8 100644 --- a/src/Security/Voter/LexiconVoter.php +++ b/src/Security/Voter/LexiconVoter.php @@ -59,7 +59,7 @@ class LexiconVoter extends Voter return true; } - if (in_array($user, $lexicon->getGroup()->getMembers())) { + if ($lexicon->getGroup() && in_array($user, $lexicon->getGroup()->getMembers())) { return true; } @@ -77,7 +77,7 @@ class LexiconVoter extends Voter return true; } - if (in_array($user, $lexicon->getGroup()->getMembers())) { + if ($lexicon->getGroup() && in_array($user, $lexicon->getGroup()->getMembers())) { return true; }