diff --git a/migrations/Version20220125084520.php b/migrations/Version20220125084520.php new file mode 100644 index 0000000000000000000000000000000000000000..6adb95ef2255e6f1db38b11d4f1b5a18be3c9a37 --- /dev/null +++ b/migrations/Version20220125084520.php @@ -0,0 +1,31 @@ +<?php + +declare(strict_types=1); + +namespace DoctrineMigrations; + +use Doctrine\DBAL\Schema\Schema; +use Doctrine\Migrations\AbstractMigration; + +/** + * Auto-generated Migration: Please modify to your needs! + */ +final class Version20220125084520 extends AbstractMigration +{ + public function getDescription(): string + { + return ''; + } + + public function up(Schema $schema): void + { + // this up() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE TABLE invitation_editeur_capsule (id INT AUTO_INCREMENT NOT NULL, capsule_id INT NOT NULL, email VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE invitation_editeur_capsule'); + } +} diff --git a/src/Controller/CapsuleController.php b/src/Controller/CapsuleController.php index b169c9f4fc4eaedd9d4035c34513e45f1a36d189..819dbcf74d8cab32d0ff9df9252e64d6eae1a6bb 100644 --- a/src/Controller/CapsuleController.php +++ b/src/Controller/CapsuleController.php @@ -3,6 +3,7 @@ namespace App\Controller; use App\Entity\Capsule; +use App\Entity\CapsuleEditor; use App\Entity\User; use App\Form\DeleteCapsuleFormType; use App\Form\DuplicateCapsuleFormType; @@ -68,6 +69,7 @@ class CapsuleController extends AbstractController $video_url = htmlspecialchars($form->get('video_url')->getData()); $capsule = $this->createCapsuleInDB($form, $current_user); + $this->addCapsuleAuthorAsEditor($capsule->getId(), $current_user->getId()); return $this->forward('App\Controller\ProjectController::create', [ 'capsule' => $capsule, @@ -148,7 +150,7 @@ class CapsuleController extends AbstractController $entityManager = $this->getDoctrine()->getManager(); $capsule = $entityManager->getRepository(Capsule::class)->find($id); - if (!$capsule) { + if (! $capsule) { throw $this->createNotFoundException( 'No capsule found for id ' . $id ); @@ -219,7 +221,6 @@ class CapsuleController extends AbstractController throw new \Exception('The retrieved capsule is not an instance of Capsule.'); } - if (! $capsule_repository->doCapsuleBelongsToUser($parent_capsule, $current_user)) { $this->addFlash( 'warning', @@ -288,4 +289,15 @@ class CapsuleController extends AbstractController return $capsule; } + + private function addCapsuleAuthorAsEditor(int $capsule_id, int $user_id): void + { + $capsule_editor = new CapsuleEditor(); + $capsule_editor->setCapsuleId($capsule_id); + $capsule_editor->setUserId($user_id); + + $entity_manager = $this->getDoctrine()->getManager(); + $entity_manager->persist($capsule_editor); + $entity_manager->flush(); + } } diff --git a/src/Controller/CapsuleEditorsController.php b/src/Controller/CapsuleEditorsController.php new file mode 100644 index 0000000000000000000000000000000000000000..e6379d253af4f0ad9f58f1bc4a5ffd7c1c50b893 --- /dev/null +++ b/src/Controller/CapsuleEditorsController.php @@ -0,0 +1,159 @@ +<?php + +namespace App\Controller; + +use App\Entity\Capsule; +use App\Entity\CapsuleEditor; +use App\Entity\CapsulePendingEditor; +use App\Entity\User; +use App\Form\CapsuleEditorsFormType; +use App\Repository\CapsuleEditorRepository; +use App\Repository\CapsulePendingEditorRepository; +use App\Repository\UserRepository; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; +use Symfony\Contracts\Translation\TranslatorInterface; + +class CapsuleEditorsController extends AbstractController +{ + /** + * @Route("/capsule/{capsule_id}/editors", name="edit_capsule_editors") + */ + public function editCapsuleEditors( + Request $request, + int $capsule_id, + TranslatorInterface $translator, + CapsulePendingEditorRepository $capsule_pending_editor_repository, + CapsuleEditorRepository $capsule_editor_repository, + UserRepository $user_repository + ): Response { + $form = $this->createForm(CapsuleEditorsFormType::class); + $form->handleRequest($request); + $current_user = $this->getUser(); + + if (! $current_user instanceof User) { + return $this->redirectToRoute('app_logout'); + } + + $entity_manager = $this->getDoctrine()->getManager(); + $capsule_repository = $entity_manager->getRepository(Capsule::class); + $capsule = $capsule_repository->find($capsule_id); + + if (! $capsule) { + throw $this->createNotFoundException( + 'No capsule found for id ' . $capsule_id + ); + } + + $current_capsule_editor_ids = $capsule_editor_repository->findEditorIdsByCapsuleId($capsule_id); + $current_capsule_editors_users = $user_repository->getUsersFromIds($current_capsule_editor_ids); + + if (! in_array($current_user->getId(), $current_capsule_editor_ids)) { + $this->addFlash( + 'warning', + $translator->trans( + 'editors.user_not_editor_error', + [ + 'capsule_name' => $capsule->getName() + ] + ) + ); + + return $this->redirectToRoute('capsule_list'); + } + + $pending_editors = $capsule_pending_editor_repository->findBy(['capsule_id' => $capsule_id]); + $pending_editors_emails = $capsule_pending_editor_repository->getPendingEditorsEmails($capsule_id); + + if ($form->isSubmitted() && $form->isValid()) { + $editor_email = $form->get('email')->getData(); + + $user_associated_with_email_address = $entity_manager + ->getRepository(User::class) + ->findOneBy(['email' => $editor_email]); + + if (! $user_associated_with_email_address instanceof User) { + if (in_array($editor_email, $pending_editors_emails)) { + $this->addFlash( + 'warning', + $translator->trans( + 'editors.add.pending_editor.already_added', + [ + 'user_email' => $editor_email + ] + ) + ); + + return $this->redirectToRoute('edit_capsule_editors', [ + 'capsule_id' => $capsule_id + ]); + } + + $pending_editor = new CapsulePendingEditor(); + $pending_editor->setCapsuleId($capsule_id); + $pending_editor->setEmail($editor_email); + $entity_manager->persist($pending_editor); + $entity_manager->flush(); + + $this->addFlash( + 'success', + $translator->trans( + 'editors.add.pending_editor.success', + [ + 'user_email' => $editor_email + ] + ) + ); + + return $this->redirectToRoute('edit_capsule_editors', [ + 'capsule_id' => $capsule_id + ]); + } + + if (in_array($user_associated_with_email_address->getId(), $current_capsule_editor_ids)) { + $this->addFlash( + 'warning', + $translator->trans( + 'editors.add.user.already_added', + [ + 'user_email' => $editor_email + ] + ) + ); + + return $this->redirectToRoute('edit_capsule_editors', [ + 'capsule_id' => $capsule_id + ]); + } + $capsule_editor = new CapsuleEditor(); + $capsule_editor->setUserId($user_associated_with_email_address->getId()); + $capsule_editor->setCapsuleId($capsule_id); + $entity_manager->persist($capsule_editor); + $entity_manager->flush(); + + $this->addFlash( + 'success', + $translator->trans( + 'editors.add.user.success', + [ + 'capsule_name' => $capsule->getName(), + 'user_email' => $editor_email + ] + ) + ); + + return $this->redirectToRoute('edit_capsule_editors', [ + 'capsule_id' => $capsule_id + ]); + } + + return $this->render('capsule/list_editors.html.twig', [ + 'userPermissionsCapsuleForm' => $form->createView(), + 'capsule_name' => $capsule->getName(), + 'editors' => $current_capsule_editors_users, + 'pending_editors' => $pending_editors + ]); + } +} diff --git a/src/Entity/CapsuleEditor.php b/src/Entity/CapsuleEditor.php new file mode 100644 index 0000000000000000000000000000000000000000..4834b54a2ed69cfed557797743b9dd43786053d6 --- /dev/null +++ b/src/Entity/CapsuleEditor.php @@ -0,0 +1,59 @@ +<?php + +namespace App\Entity; + +use App\Repository\CapsuleEditorRepository; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ORM\Table(name="editeur_capsule") + * @ORM\Entity(repositoryClass=CapsuleEditorRepository::class) + */ +class CapsuleEditor +{ + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + private int $id; + + /** + * @ORM\Column(type="integer") + */ + private int $user_id; + + /** + * @ORM\Column(type="integer") + */ + private int $capsule_id; + + public function getId(): int + { + return $this->id; + } + + public function getUserId(): int + { + return $this->user_id; + } + + public function setUserId(int $user_id): self + { + $this->user_id = $user_id; + + return $this; + } + + public function getCapsuleId(): int + { + return $this->capsule_id; + } + + public function setCapsuleId(int $capsule_id): self + { + $this->capsule_id = $capsule_id; + + return $this; + } +} diff --git a/src/Entity/CapsulePendingEditor.php b/src/Entity/CapsulePendingEditor.php new file mode 100644 index 0000000000000000000000000000000000000000..6dd010a96f837a03da48c7a80119c096cfd35d9e --- /dev/null +++ b/src/Entity/CapsulePendingEditor.php @@ -0,0 +1,62 @@ +<?php + +namespace App\Entity; + +use App\Repository\CapsulePendingEditorRepository; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ORM\Table(name="invitation_editeur_capsule") + * @ORM\Entity(repositoryClass=CapsulePendingEditorRepository::class) + */ +class CapsulePendingEditor +{ + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + private int $id; + + /** + * @ORM\Column(type="integer") + * @ORM\Column(name="capsule_id", type="integer", nullable=false) + * @ORM\ManyToOne(targetEntity="App\Entity\Capsule", inversedBy="id") + * @ORM\JoinColumn(name="capsule_id", referencedColumnName="id") + */ + private int $capsule_id; + + /** + * @ORM\Column(name="email", type="string", length=255, nullable=false) + */ + private string $email; + + public function getId(): int + { + return $this->id; + } + + public function getCapsuleId(): int + { + return $this->capsule_id; + } + + public function setCapsuleId(int $capsule_id): self + { + $this->capsule_id = $capsule_id; + + return $this; + } + + public function getEmail(): string + { + return $this->email; + } + + public function setEmail(string $email): self + { + $this->email = $email; + + return $this; + } +} diff --git a/src/Form/CapsuleEditorsFormType.php b/src/Form/CapsuleEditorsFormType.php new file mode 100644 index 0000000000000000000000000000000000000000..c6ccec919dc8206875622a423caea472607784b7 --- /dev/null +++ b/src/Form/CapsuleEditorsFormType.php @@ -0,0 +1,41 @@ +<?php + +namespace App\Form; + +use App\Entity\User; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\EmailType; +use Symfony\Component\Form\Extension\Core\Type\PasswordType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Security\Core\Validator\Constraints\UserPassword; +use Symfony\Component\Validator\Constraints\NotBlank; + +class CapsuleEditorsFormType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add( + 'email', + EmailType::class, + [ + 'constraints' => [new NotBlank(['message' => 'email.not_blank'])], + 'label' => 'editors.add_email_address', + 'empty_data' => '' + ] + ) + ->add( + 'validate', + SubmitType::class, + ['label' => 'general.validate'] + ); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([]); + } +} diff --git a/src/Repository/CapsuleEditorRepository.php b/src/Repository/CapsuleEditorRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..06146b7e5fe8cf1ccf2608956dcc7cb86be39ddc --- /dev/null +++ b/src/Repository/CapsuleEditorRepository.php @@ -0,0 +1,66 @@ +<?php + +namespace App\Repository; + +use App\Entity\CapsuleEditor; +use App\Entity\User; +use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\Persistence\ManagerRegistry; + +/** + * @method CapsuleEditor|null find($id, $lockMode = null, $lockVersion = null) + * @method CapsuleEditor|null findOneBy(array $criteria, array $orderBy = null) + * @method CapsuleEditor[] findAll() + * @method CapsuleEditor[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class CapsuleEditorRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, CapsuleEditor::class); + } + + /** + * @return array<int> + */ + public function findEditorIdsByCapsuleId(int $capsule_id): array + { + $editors = $this->findBy(['capsule_id' => $capsule_id]); + + $editor_ids = []; + foreach ($editors as $editor) { + $editor_ids[] = $editor->getUserId(); + } + + return $editor_ids; + } + + // /** + // * @return CapsuleEditor[] Returns an array of CapsuleEditor objects + // */ + /* + public function findByExampleField($value) + { + return $this->createQueryBuilder('c') + ->andWhere('c.exampleField = :val') + ->setParameter('val', $value) + ->orderBy('c.id', 'ASC') + ->setMaxResults(10) + ->getQuery() + ->getResult() + ; + } + */ + + /* + public function findOneBySomeField($value): ?CapsuleEditor + { + return $this->createQueryBuilder('c') + ->andWhere('c.exampleField = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + */ +} diff --git a/src/Repository/CapsulePendingEditorRepository.php b/src/Repository/CapsulePendingEditorRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..1504d2e8f9065246a327cfc2069f9abf50a34080 --- /dev/null +++ b/src/Repository/CapsulePendingEditorRepository.php @@ -0,0 +1,43 @@ +<?php + +namespace App\Repository; + +use App\Entity\CapsulePendingEditor; +use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\Persistence\ManagerRegistry; + +/** + * @method CapsulePendingEditor|null find($id, $lockMode = null, $lockVersion = null) + * @method CapsulePendingEditor|null findOneBy(array $criteria, array $orderBy = null) + * @method CapsulePendingEditor[] findAll() + * @method CapsulePendingEditor[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class CapsulePendingEditorRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, CapsulePendingEditor::class); + } + + /** + * @return array<string> + */ + public function getPendingEditorsEmails(int $capsule_id): array + { + $editors_emails_result = $this->createQueryBuilder('c') + ->select('c.email') + ->andWhere('c.capsule_id = :val') + ->setParameter('val', $capsule_id) + ->getQuery() + ->getResult() + ; + + $editors_emails = []; + + foreach ($editors_emails_result as $editor_email_result) { + $editors_emails[] = $editor_email_result['email']; + } + + return $editors_emails; + } +} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index 28987a479e6a77f48bec686763edb56a22379b37..c05f501dca9e554be24b21770e2edb8644f387b6 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -4,6 +4,8 @@ namespace App\Repository; use App\Entity\User; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\DBAL\Exception; +use Doctrine\DBAL\Exception\DatabaseObjectNotFoundException; use Doctrine\Persistence\ManagerRegistry; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; @@ -57,4 +59,23 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader ->getOneOrNullResult() ; } + + /** + * @param array<int> $user_ids + * @return array<User> + */ + public function getUsersFromIds(array $user_ids): array + { + $users = []; + + foreach ($user_ids as $user_id) { + $user = $this->findOneBy(['id' => $user_id]); + if (null === $user) { + throw new Exception('User was not found'); + } + $users[] = $user; + } + + return $users; + } } diff --git a/templates/capsule/index.html.twig b/templates/capsule/index.html.twig index c18fa5a17306da3c14718cc9a186e3e0704b011e..6bc351eba424fd3447bc61d1870c48faaeb5adad 100644 --- a/templates/capsule/index.html.twig +++ b/templates/capsule/index.html.twig @@ -64,7 +64,7 @@ <i class="fa-thin fa-gears"></i> <div class="list-item text-nowrap"> - <a href="" class="links text-decoration-none"> + <a href="/capsule/{{ capsule.getId() }}/editors" class="links text-decoration-none"> <i class="fas fa-cog m-2"></i> {{ 'capsule.edit_permissions.link'|trans }} </a> diff --git a/templates/capsule/list_editors.html.twig b/templates/capsule/list_editors.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..d92f65372c09d4177aab76715f005e9af8f21a57 --- /dev/null +++ b/templates/capsule/list_editors.html.twig @@ -0,0 +1,68 @@ +{% extends 'layout.html.twig' %} + +{% block title %} + {{ 'editors.title'|trans }} + - + {{ parent() }} +{% endblock %} + +{% block body %} + + <div> + <div class="row w-100 gx-0"> + <div class="row-title-box"> + <h3 class="row-title"> + {{ 'editors.title_name'|trans({'%capsule_name%': capsule_name}) }} + </h3> + </div> + </div> + + {% for flashWarning in app.flashes('warning') %} + <div class="text-center alert alert-warning col-5 mx-auto my-5 mt-2" role="alert"> + {{ flashWarning }} + </div> + {% endfor %} + + {% for flashSuccess in app.flashes('success') %} + <div class="text-center alert alert-success col-5 mx-auto my-5 mt-2" role="alert"> + {{ flashSuccess }} + </div> + {% endfor %} + + <div class="d-flex flex-row flex-md-row justify-content-start"> + <div class="d-flex flex-row pe-md-5 pb-3 fw-normal me-0 me-md-5"> + <div class="pe-3 pe-md-4 text-nowrap"> + <h5> + {{ 'editors.current_editors_title'|trans }} + </h5> + <ul class="ps-0"> + {% for editor in editors %} + <li class="text-capitalize text-secondary list-unstyled p-1"> + {{ editor.getFirstName() }} {{ editor.getLastName() }} + </li> + {% endfor %} + </ul> + <h5> + {{ 'editors.pending_editors_title'|trans }} + </h5> + <ul class="ps-1"> + {% for pending_editor in pending_editors %} + <li class="text-secondary list-unstyled p-1"> + {{ pending_editor.getEmail() }} + </li> + {% endfor %} + </ul> + </div> + </div> + + <div class="d-flex flex-column justify-content-center ms-5"> + {{ form_start(userPermissionsCapsuleForm, {'attr': {novalidate: 'novalidate'}}) }} + {{ form_row(userPermissionsCapsuleForm.email, {'row_attr': {'class' : 'm-auto mb-4 col-12'}}) }} + {{ form_row(userPermissionsCapsuleForm.validate, {'row_attr': {'class' : 'm-auto mb-5 col-2'}}) }} + {{ form_end(userPermissionsCapsuleForm) }} + </div> + + </div> + </div> + +{% endblock %} \ No newline at end of file diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml index b6e3029b3d13c41d03059eed90d7f7ca774c477f..bf29b4e2b8abe317dfeeee637e2b88db7a2e653f 100644 --- a/translations/messages.en.yaml +++ b/translations/messages.en.yaml @@ -106,4 +106,20 @@ user: change_password: Change password updated_success: The password has been updated edit_profile: Edit my profile - edit_password: Edit my password \ No newline at end of file + edit_password: Edit my password + +editors: + title: Editors + title_name: Editors of capsule %capsule_name% + add_email_address: Add new editor with email address + current_editors_title: Current editors + pending_editors_title: Pending editors + user_not_editor_error: You are not editor of the capsule + add: + pending_editor: + success: The user user_email has been added to pending editor list. + He will receive an email to invite him register on MemoRekall and to inform him he has been added as an editor of this capsule. + already_added: The user user_email has already been added to pending editor list + user: + success: The user user_email is now an editor of the capsule capsule_name + already_added: The user user_email is already an editor of this capsule \ No newline at end of file diff --git a/translations/messages.fr.yaml b/translations/messages.fr.yaml index 9fff9516c6726f3625dc8882cd317b063f54b269..9d2ebcf0c437b7a429c405079e7904ae513f6fd5 100644 --- a/translations/messages.fr.yaml +++ b/translations/messages.fr.yaml @@ -104,4 +104,20 @@ user: change_password: Changer mon mot de passe updated_success: Votre mot de passe a bien été modifié edit_profile: Modifier mon mot de passe - edit_password: Modifier mon mot de passe \ No newline at end of file + edit_password: Modifier mon mot de passe + +editors: + title: Editeurs d'une capsule + title_name: Editeurs de la capsule %capsule_name% + add_email_address: Ajouter un nouvel editeur avec son adresse e-mail + current_editors_title: Editeurs actuels + pending_editors_title: Editeurs en attente de confirmation + user_not_editor_error: Vous n'êtes pas éditeur de la capsule + add: + pending_editor: + success: L'utilisateur user_email a bien été ajouté à la liste des editeurs en attente + Il recevera un e-mail l'invitant à créer un compte MemoRekall et l'informant qu'il a été ajouté en tant qu'éditeur de la capsule. + already_added: L'utilisateur user_email a déjà été ajouté à la liste des éditeurs en attente + user: + success: L'utilisateur user_email est maintenant éditeur de la capsule capsule_name + already_added: L'utilisateur user_email est déjà editeur de la capsule \ No newline at end of file