diff --git a/assets/styles/app.scss b/assets/styles/app.scss index 9b4fea673e248e6f997981aeec51f9612fc9cca7..a35eb1cd9ccfe7afe7b691c2d6d0c2c53a2c4191 100644 --- a/assets/styles/app.scss +++ b/assets/styles/app.scss @@ -53,7 +53,6 @@ button[type=submit]{ border-radius: 3px; line-height: 1.5; font-size: 1rem; - margin: 30px 0 0 0; padding: .5rem 1rem; text-align: center; text-transform: uppercase; @@ -83,8 +82,6 @@ button[type=submit]{ box-shadow: 0 0 10px rgba(0,0,0,.5); border-radius: 2px; margin-bottom: 40px; - align-items: center; - justify-content: center; } .list-item { @@ -125,7 +122,6 @@ button[type=submit]{ text-decoration: none; float: left; color: rgba(255,255,255,.75) !important; - font-size: 24px; line-height: 60px; background: -webkit-linear-gradient(top left, #FA772E, #FC4326); -webkit-background-clip: text; 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/Builder/CapsuleBuilder.php b/src/Builder/CapsuleBuilder.php index ae2e0aecd1816c4ec3507ae7a71dc6003a6e8110..b219d3bcab024d36e363d369c50fbf8a9c5f7d95 100644 --- a/src/Builder/CapsuleBuilder.php +++ b/src/Builder/CapsuleBuilder.php @@ -33,6 +33,7 @@ class CapsuleBuilder { $this->capsule->setCreationAuthor($creation_author); $this->hasRequiredCreationAuthor = true; + $this->capsule->addEditor($creation_author); return $this; } diff --git a/src/Controller/CapsuleController.php b/src/Controller/CapsuleController.php index b169c9f4fc4eaedd9d4035c34513e45f1a36d189..039087a3d5f81f33da31b630c650cd968b005a61 100644 --- a/src/Controller/CapsuleController.php +++ b/src/Controller/CapsuleController.php @@ -22,12 +22,18 @@ use Symfony\Contracts\Translation\TranslatorInterface; class CapsuleController extends AbstractController { + private TranslatorInterface $translator; + + public function __construct(TranslatorInterface $translator) + { + $this->translator = $translator; + } + /** * @Route("/my_capsules", name="capsule_list") * @Route("/", name="home") */ public function index( - CapsuleRepository $capsule_repository, PaginatorInterface $paginator, Request $request ): Response { @@ -37,7 +43,7 @@ class CapsuleController extends AbstractController return $this->redirectToRoute('app_logout'); } - $all_capsules = $capsule_repository->findBy(['creation_author' => $current_user]); + $all_capsules = $current_user->getCapsules(); $capsules = $paginator->paginate( $all_capsules, @@ -47,6 +53,7 @@ class CapsuleController extends AbstractController return $this->render('capsule/index.html.twig', [ 'capsules' => $capsules, + 'current_user' => $current_user, 'legacy_url' => $this->getParameter('app.legacy_external_prefix') ]); } @@ -109,9 +116,15 @@ class CapsuleController extends AbstractController return $this->redirectToRoute('app_logout'); } - $capsule = $capsuleRepository->findOneBy(['link_path' => $path, 'creation_author' => $current_user]); + $capsule = $capsuleRepository->findOneBy(['link_path' => $path]); + if (null === $capsule) { - $this->addFlash('warning', $translator->trans('capsule.edition_not_allowed')); + $this->addFlash('warning', $translator->trans('capsule.edit.not_found')); + return $this->redirectToRoute('capsule_list'); + } + + if (! $capsule->getEditors()->contains($current_user)) { + $this->addFlash('warning', $translator->trans('capsule.edit.not_allowed')); return $this->redirectToRoute('capsule_list'); } @@ -133,9 +146,7 @@ class CapsuleController extends AbstractController */ public function delete( int $id, - Request $request, - TranslatorInterface $translator, - CapsuleRepository $capsule_repository + Request $request ): Response { $form = $this->createForm(DeleteCapsuleFormType::class); $form->handleRequest($request); @@ -148,19 +159,17 @@ 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 ); } $capsule_name = $capsule->getName(); - $do_capsule_belongs_to_user = $capsule_repository->doCapsuleBelongsToUser($capsule, $current_user); - - if (! $do_capsule_belongs_to_user) { + if ($capsule->getCreationAuthor() !== $current_user) { $this->addFlash( 'warning', - $translator->trans( + $this->translator->trans( 'capsule.delete.error', [ 'capsule_name' => $capsule_name @@ -172,12 +181,14 @@ class CapsuleController extends AbstractController } if ($form->isSubmitted() && $form->isValid()) { + $current_user->removeCapsule($capsule); + $capsule->removeEditor($current_user); $entityManager->remove($capsule); $entityManager->flush(); $this->addFlash( 'success', - $translator->trans( + $this->translator->trans( 'capsule.delete.success', [ 'capsule_name' => $capsule_name @@ -200,9 +211,7 @@ class CapsuleController extends AbstractController public function duplicate( int $id, Request $request, - Filesystem $file_system, - TranslatorInterface $translator, - CapsuleRepository $capsule_repository + Filesystem $file_system ): Response { $form = $this->createForm(DuplicateCapsuleFormType::class); $form->handleRequest($request); @@ -219,11 +228,12 @@ class CapsuleController extends AbstractController throw new \Exception('The retrieved capsule is not an instance of Capsule.'); } + $capsule_editors = $parent_capsule->getEditors(); - if (! $capsule_repository->doCapsuleBelongsToUser($parent_capsule, $current_user)) { + if (! in_array($current_user, $capsule_editors->toArray())) { $this->addFlash( 'warning', - $translator->trans( + $this->translator->trans( 'capsule.duplicate.error', [ 'capsule_name' => $parent_capsule->getName() @@ -239,7 +249,7 @@ class CapsuleController extends AbstractController if (! $parent_directory_exists) { $this->addFlash( 'warning', - $translator->trans( + $this->translator->trans( 'project.not_exist', [ 'capsule_name' => $parent_capsule->getName() diff --git a/src/Controller/CapsuleEditorController.php b/src/Controller/CapsuleEditorController.php new file mode 100644 index 0000000000000000000000000000000000000000..39af5fe0d9eed11481ea796bcce07d88f0320f19 --- /dev/null +++ b/src/Controller/CapsuleEditorController.php @@ -0,0 +1,219 @@ +<?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\CapsulePendingEditorRepository; +use App\Repository\CapsuleRepository; +use App\Repository\UserRepository; +use Doctrine\ORM\EntityManagerInterface; +use http\Env; +use Symfony\Bridge\Twig\Mime\TemplatedEmail; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +class CapsuleEditorController extends AbstractController +{ + private TranslatorInterface $translator; + private MailerInterface $mailer; + private CapsulePendingEditorRepository $capsule_pending_editor_repository; + private EntityManagerInterface $entity_manager; + private UrlGeneratorInterface $urlGenerator; + + public function __construct( + MailerInterface $mailer, + TranslatorInterface $translator, + CapsulePendingEditorRepository $capsule_pending_editor_repository, + EntityManagerInterface $entity_manager, + UrlGeneratorInterface $urlGenerator + ) { + $this->mailer = $mailer; + $this->translator = $translator; + $this->capsule_pending_editor_repository = $capsule_pending_editor_repository; + $this->entity_manager = $entity_manager; + $this->urlGenerator = $urlGenerator; + } + + /** + * @Route("/capsule/{capsule_id}/editors", name="edit_capsule_editors") + */ + public function editCapsuleEditors( + Request $request, + int $capsule_id, + CapsuleRepository $capsule_repository, + UserRepository $user_repository + ): Response { + $current_user = $this->getUser(); + if (! $current_user instanceof User) { + return $this->redirectToRoute('app_logout'); + } + + $capsule = $capsule_repository->find($capsule_id); + if (! $capsule) { + throw $this->createNotFoundException( + 'No capsule found for id ' . $capsule_id + ); + } + + $current_capsule_editors_users = $capsule->getEditors()->toArray(); + if (! $capsule->getEditors()->contains($current_user)) { +// in_array($current_user, $current_capsule_editors_users)) { + $this->addFlash( + 'warning', + $this->translator->trans( + 'editors.user_not_editor_error', + [ + 'capsule_name' => $capsule->getName() + ] + ) + ); + + return $this->redirectToRoute('capsule_list'); + } + + $form = $this->createForm(CapsuleEditorsFormType::class); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $editor_email = $form->get('email')->getData(); + $user_associated_with_email_address = $user_repository + ->findOneBy(['email' => $editor_email]); + + if (! $user_associated_with_email_address instanceof User) { + $this->addPendingEditor($editor_email, $capsule, $current_user); + } else { + $this->addEditor( + $editor_email, + $capsule, + $current_user, + $user_associated_with_email_address, + $current_capsule_editors_users + ); + } + + return $this->redirectToRoute('edit_capsule_editors', [ + 'capsule_id' => $capsule_id + ]); + } + + $pending_editors = $this->capsule_pending_editor_repository->findBy(['capsule_id' => $capsule_id]); + + return $this->render('capsule/editors/list_editors.html.twig', [ + 'userPermissionsCapsuleForm' => $form->createView(), + 'capsule_name' => $capsule->getName(), + 'editors' => $current_capsule_editors_users, + 'pending_editors' => $pending_editors + ]); + } + + private function addPendingEditor(string $editor_email, Capsule $capsule, User $current_user): void + { + $pending_editors_emails = $this->capsule_pending_editor_repository->getPendingEditorsEmails($capsule->getId()); + + if (in_array($editor_email, $pending_editors_emails)) { + $this->addFlash( + 'warning', + $this->translator->trans( + 'editors.add.pending_editor.already_added', + [ + 'user_email' => $editor_email + ] + ) + ); + return; + } + + $pending_editor = new CapsulePendingEditor(); + $pending_editor->setCapsuleId($capsule->getId()); + $pending_editor->setEmail($editor_email); + $this->entity_manager->persist($pending_editor); + $this->entity_manager->flush(); + + $email = (new TemplatedEmail()) + ->to($editor_email) + ->subject($this->translator->trans('editors.add.pending_editor.email.title')) + ->htmlTemplate('capsule/editors/email_pending_editor.html.twig') + ->context([ + 'user' => $current_user, + 'capsule' => $capsule + ]); + + $this->mailer->send($email); + + $this->addFlash( + 'success', + $this->translator->trans( + 'editors.add.pending_editor.success', + [ + 'user_email' => $editor_email + ] + ) + ); + } + + /** + * @param array<User> $current_capsule_editors_users + * @throws TransportExceptionInterface + */ + private function addEditor( + string $editor_email, + Capsule $capsule, + User $current_user, + User $user_associated_with_email_address, + array $current_capsule_editors_users + ): void { + if (in_array($user_associated_with_email_address, $current_capsule_editors_users)) { + $this->addFlash( + 'warning', + $this->translator->trans( + 'editors.add.user.already_added', + [ + 'user_email' => $editor_email + ] + ) + ); + return; + } + + $capsule->addEditor($user_associated_with_email_address); + $this->entity_manager->persist($capsule); + $this->entity_manager->flush(); + + $email = (new TemplatedEmail()) + ->to($editor_email) + ->subject($this->translator->trans('editors.add.user.email.title')) + ->htmlTemplate('capsule/editors/email_editor.html.twig') + ->context([ + 'user' => $current_user, + 'capsule' => $capsule, + 'capsule_edit_link' => $this->urlGenerator->generate( + 'edit_capsule', + [ 'path' => $capsule->getLinkPath() ], + UrlGeneratorInterface::ABSOLUTE_URL + ) + ]); + + $this->mailer->send($email); + + $this->addFlash( + 'success', + $this->translator->trans( + 'editors.add.user.success', + [ + 'capsule_name' => $capsule->getName(), + 'user_email' => $editor_email + ] + ) + ); + } +} diff --git a/src/Controller/RegistrationController.php b/src/Controller/RegistrationController.php index e4f1245537cc8065dbf7a2dcb7dabd96ddd4b28d..faf3031cd5bba07f1c84c359574d3b02eadb83b8 100644 --- a/src/Controller/RegistrationController.php +++ b/src/Controller/RegistrationController.php @@ -2,8 +2,12 @@ namespace App\Controller; +use App\Entity\Capsule; +use App\Entity\PendingEditorInvitation; use App\Entity\User; use App\Form\RegistrationFormType; +use App\Repository\CapsuleRepository; +use App\Repository\PendingEditorInvitationRepository; use App\Repository\UserRepository; use App\Security\EmailVerifier; use Doctrine\ORM\EntityManagerInterface; @@ -12,7 +16,6 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Mime\Address; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Annotation\Route; use Symfony\Contracts\Translation\TranslatorInterface; @@ -22,13 +25,16 @@ class RegistrationController extends AbstractController { private EmailVerifier $emailVerifier; private RequestStack $requestStack; + private EntityManagerInterface $entity_manager; public function __construct( EmailVerifier $emailVerifier, - RequestStack $requestStack + RequestStack $requestStack, + EntityManagerInterface $entity_manager ) { $this->emailVerifier = $emailVerifier; $this->requestStack = $requestStack; + $this->entity_manager = $entity_manager; } /** @@ -36,8 +42,7 @@ class RegistrationController extends AbstractController */ public function register( Request $request, - UserPasswordHasherInterface $userPasswordHasher, - EntityManagerInterface $entityManager + UserPasswordHasherInterface $userPasswordHasher ): Response { if ($this->getUser()) { return $this->redirectToRoute('capsule_list'); @@ -58,21 +63,20 @@ class RegistrationController extends AbstractController ) ); - $entityManager->persist($user); - $entityManager->flush(); + $this->entity_manager->persist($user); + $this->entity_manager->flush(); // generate a signed url and email it to the user $this->emailVerifier->sendEmailConfirmation( 'app_verify_email', - $user, + $user->getId(), + $user->getEmail(), (new TemplatedEmail()) ->to($user->getEmail()) ->subject('Please Confirm your Email') ->htmlTemplate('registration/confirmation_email.html.twig') ); - // do anything else you need here, like send an email - // return $this->redirectToRoute('app_login'); $this->requestStack->getSession()->set('userid', $user->getId()); return $this->redirectToRoute('app_register_mail_sent'); } @@ -88,6 +92,8 @@ class RegistrationController extends AbstractController public function verifyUserEmail( Request $request, UserRepository $userRepository, + PendingEditorInvitationRepository $pending_editor_invitation_repository, + CapsuleRepository $capsule_repository, TranslatorInterface $translator ): Response { $id = $request->get('id'); @@ -111,6 +117,12 @@ class RegistrationController extends AbstractController return $this->redirectToRoute('app_register'); } + $this->addCapsuleToConfirmedPreviousPendingEditor( + $user, + $pending_editor_invitation_repository, + $capsule_repository + ); + $this->addFlash( 'email_verified_success', $translator->trans('registration.email_verified_success') @@ -132,4 +144,58 @@ class RegistrationController extends AbstractController ['user' => $user] ); } + + private function addCapsuleToConfirmedPreviousPendingEditor( + User $user, + PendingEditorInvitationRepository $pending_editor_invitation_repository, + CapsuleRepository $capsule_repository + ): void { + $pending_editor_invitations = $pending_editor_invitation_repository->findBy(['email' => $user->getEmail()]); + + if ($pending_editor_invitations == null) { + return; + } + + $capsules_ids = $this->removeInvitations($pending_editor_invitations); + $this->addEditorToCapsules($capsules_ids, $user, $capsule_repository); + } + + /** + * @param array<PendingEditorInvitation> $pending_editor_invitations + * @return array<int> + */ + private function removeInvitations(array $pending_editor_invitations): array + { + $capsules_ids = []; + + foreach ($pending_editor_invitations as $invitation) { + if (! $invitation instanceof PendingEditorInvitation) { + return $capsules_ids; + } + $capsules_ids[] = $invitation->getCapsuleId(); + $this->entity_manager->remove($invitation); + } + + return $capsules_ids; + } + + /** + * @param array<int> $capsules_ids + */ + private function addEditorToCapsules( + array $capsules_ids, + User $user, + CapsuleRepository $capsule_repository + ): void { + $capsules = $capsule_repository->findBy(['id' => $capsules_ids]); + + foreach ($capsules as $capsule) { + if (! $capsule instanceof Capsule) { + return; + } + $capsule->addEditor($user); + $this->entity_manager->persist($capsule); + } + $this->entity_manager->flush(); + } } diff --git a/src/Entity/Capsule.php b/src/Entity/Capsule.php index 497fa3377354d73958331c735b73e236c76ffbff..4e5249f4215ec05acdd33bcf933604fc6bf5bdaa 100644 --- a/src/Entity/Capsule.php +++ b/src/Entity/Capsule.php @@ -32,7 +32,7 @@ class Capsule /** * - * @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="capsulesCreated") + * @ORM\ManyToOne(targetEntity="App\Entity\User") * @ORM\JoinColumn(name="aut_crea", referencedColumnName="id", nullable=false) * */ @@ -46,7 +46,7 @@ class Capsule /** * - * @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="capsulesEdited") + * @ORM\ManyToOne(targetEntity="App\Entity\User") * @ORM\JoinColumn(name="aut_maj", referencedColumnName="id") * */ @@ -77,6 +77,22 @@ class Capsule */ private string $password; + /** + * @var Collection<User> + * + * @ORM\ManyToMany(targetEntity="App\Entity\User", inversedBy="capsules") + * @ORM\JoinTable(name="editeur_capsule", + * joinColumns={@ORM\JoinColumn(name="capsule_id", referencedColumnName="id")}, + * inverseJoinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")} + * ) + */ + private Collection $editors; + + public function __construct() + { + $this->editors = new ArrayCollection(); + } + public function getId(): int { return $this->id; @@ -172,4 +188,25 @@ class Capsule { $this->password = $password; } + + public function addEditor(User $editor): void + { + $editor->addCapsule($this); + $this->editors[] = $editor; + } + + public function removeEditor(User $editor): Capsule + { + $editor->removeCapsule($this); + $this->editors->removeElement($editor); + return $this; + } + + /** + * @return Collection<User> $editors + */ + public function getEditors(): Collection + { + return $this->editors; + } } 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/Entity/PendingEditorInvitation.php b/src/Entity/PendingEditorInvitation.php new file mode 100644 index 0000000000000000000000000000000000000000..b0bb5da206b3214717712efe4baf401eb146e43d --- /dev/null +++ b/src/Entity/PendingEditorInvitation.php @@ -0,0 +1,61 @@ +<?php + +namespace App\Entity; + +use App\Repository\PendingEditorInvitationRepository; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ORM\Table(name="invitation_editeur_capsule") + * @ORM\Entity(repositoryClass=PendingEditorInvitationRepository::class) + */ +class PendingEditorInvitation +{ + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + private int $id; + + /** + * @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(type="string", length=255) + */ + 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/Entity/User.php b/src/Entity/User.php index 0eca9ce6ecedb6fcbdc340ccc8de8cc85dad9d09..5e2fa78e79170b4c426426ee972c89725c669432 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -3,6 +3,8 @@ namespace App\Entity; use App\Repository\UserRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface; @@ -81,6 +83,17 @@ class User implements UserInterface, LegacyPasswordAuthenticatedUserInterface */ private string $salt; + /** + * @var Collection<Capsule> + * @ORM\ManyToMany(targetEntity="App\Entity\Capsule", mappedBy="editors") + */ + private Collection $capsules; + + public function __construct() + { + $this->capsules = new ArrayCollection(); + } + public function getId(): int { return $this->id; @@ -211,4 +224,28 @@ class User implements UserInterface, LegacyPasswordAuthenticatedUserInterface { $this->salt = $salt; } + + public function getFullName(): string + { + return $this->firstName . " " . $this->lastName; + } + + /** + * @return Collection<Capsule> + */ + public function getCapsules(): Collection + { + return $this->capsules; + } + + public function addCapsule(Capsule $capsule): void + { + $this->capsules[] = $capsule; + } + + public function removeCapsule(Capsule $capsule): User + { + $this->capsules->removeElement($capsule); + 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/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/CapsuleRepository.php b/src/Repository/CapsuleRepository.php index 7e5cd9f9e9820c23e9b5eda5685c5c3772130af8..97f8c23b7d7b22057dd08aca2f00a24193597bd0 100644 --- a/src/Repository/CapsuleRepository.php +++ b/src/Repository/CapsuleRepository.php @@ -21,11 +21,4 @@ class CapsuleRepository extends ServiceEntityRepository { parent::__construct($registry, Capsule::class); } - - public function doCapsuleBelongsToUser(Capsule $capsule, User $user): bool - { - return - $capsule->getCreationAuthor() === $user || - $capsule->getUpdateAuthor() === $user; - } } diff --git a/src/Repository/PendingEditorInvitationRepository.php b/src/Repository/PendingEditorInvitationRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..e891144dd03e5b31229c3707ea86c82f6bca48a8 --- /dev/null +++ b/src/Repository/PendingEditorInvitationRepository.php @@ -0,0 +1,62 @@ +<?php + +namespace App\Repository; + +use App\Entity\PendingEditorInvitation; +use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\Persistence\ManagerRegistry; + +/** + * @method PendingEditorInvitation|null find($id, $lockMode = null, $lockVersion = null) + * @method PendingEditorInvitation|null findOneBy(array $criteria, array $orderBy = null) + * @method PendingEditorInvitation[] findAll() + * @method PendingEditorInvitation[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class PendingEditorInvitationRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, PendingEditorInvitation::class); + } + + /** + * @return array<PendingEditorInvitation> + */ + public function getInvitationByCapsule(int $capsule_id): array + { + $query = $this->createQueryBuilder('i') + ->where("i.capsuleId like :capId") + ->setParameter("capId", $capsule_id) + ->getQuery(); + return $query->getArrayResult(); + } + + // /** + // * @return PendingEditorInvitation[] Returns an array of PendingEditorInvitation objects + // */ + /* + public function findByExampleField($value) + { + return $this->createQueryBuilder('p') + ->andWhere('p.exampleField = :val') + ->setParameter('val', $value) + ->orderBy('p.id', 'ASC') + ->setMaxResults(10) + ->getQuery() + ->getResult() + ; + } + */ + + /* + public function findOneBySomeField($value): ?PendingEditorInvitation + { + return $this->createQueryBuilder('p') + ->andWhere('p.exampleField = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + */ +} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index 28987a479e6a77f48bec686763edb56a22379b37..147f8c5c26d5ab001ea4d65733de9cf19b9aebd3 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,17 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader ->getOneOrNullResult() ; } + + /** + * @param array<int> $user_ids + * @return array<User> + */ + public function getUsersFromIds(array $user_ids): array + { + return $this->createQueryBuilder('u') + ->andWhere('u.id in (:ids)') + ->setParameter('ids', $user_ids) + ->getQuery() + ->getResult(); + } } diff --git a/src/Security/EmailVerifier.php b/src/Security/EmailVerifier.php index 84ee1dfdb13b02b19d1677a83d1c0a6552b4c118..ea4389fcac727b3d0ffa02f431f47845f93c0382 100644 --- a/src/Security/EmailVerifier.php +++ b/src/Security/EmailVerifier.php @@ -28,14 +28,15 @@ class EmailVerifier public function sendEmailConfirmation( string $verifyEmailRouteName, - User $user, + int $user_id, + string $email_address, TemplatedEmail $email ): void { $signatureComponents = $this->verifyEmailHelper->generateSignature( $verifyEmailRouteName, - (string) $user->getId(), - $user->getEmail(), - ['id' => $user->getId()] + (string) $user_id, + $email_address, + ['id' => $user_id] ); $context = $email->getContext(); diff --git a/templates/capsule/editors/email_editor.html.twig b/templates/capsule/editors/email_editor.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..02ed4041f2a601d1ea83b922e33bb678c672cef9 --- /dev/null +++ b/templates/capsule/editors/email_editor.html.twig @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8"> + <title> + {{ 'editors.add.user.email.title'|trans }} + </title> + </head> + + <body> + <div class="container d-flex flex-row justify-content-center"> + <div class="col-6"> + <p class="alert"> + {{ 'editors.add.user.email.text'|trans( + {'%user_name%': user.getFullName(), '%capsule_name%': capsule.getName()}) }} + <a href="{{ capsule_edit_link }}"> + {{ 'editors.add.user.email.link'|trans }} + </a> + </p> + + <p>{{ 'general.greeting'|trans }}</p> + </div> + </div> + </body> +</html> \ No newline at end of file diff --git a/templates/capsule/editors/email_pending_editor.html.twig b/templates/capsule/editors/email_pending_editor.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..8fcaa41b6ec8a88002effd775fbedf3f38ec32fd --- /dev/null +++ b/templates/capsule/editors/email_pending_editor.html.twig @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <title> + {{ 'editors.add.pending_editor.email.title'|trans }} + </title> +</head> + +<body> +<div class="container d-flex flex-row justify-content-center"> + <div class="col-6"> + <p class="alert"> + {{ 'editors.add.pending_editor.email.text'|trans({'%user_name%': user.getFullName(), '%capsule_name%': capsule.getName()}) }} : + + <a href="{{ 'editors.add.pending_editor.email.link'|trans }}"> + {{ 'editors.add.pending_editor.email.link_name'|trans }} + </a> + </p> + + <p>{{ 'general.greeting'|trans }}</p> + </div> +</div> +</body> +</html> \ No newline at end of file diff --git a/templates/capsule/editors/list_editors.html.twig b/templates/capsule/editors/list_editors.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..31e7e6627efd798d1b19c863bac93ea06d7584cd --- /dev/null +++ b/templates/capsule/editors/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-md-row flex-column justify-content-center"> + <div class="d-flex flex-column justify-content-center ms-md-5 ms-0 order-md-2 mb-4 col-sm-8 col-md-6 col-lg-5 col-xl-4"> + {{ form_start(userPermissionsCapsuleForm, {'attr': {novalidate: 'novalidate'}}) }} + {{ form_row(userPermissionsCapsuleForm.email, {'row_attr': {'class' : 'm-auto mb-4'}}) }} + {{ form_row(userPermissionsCapsuleForm.validate) }} + {{ form_end(userPermissionsCapsuleForm) }} + </div> + + <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> + </div> + +{% endblock %} \ No newline at end of file diff --git a/templates/capsule/index.html.twig b/templates/capsule/index.html.twig index c18fa5a17306da3c14718cc9a186e3e0704b011e..64c31b6c823d34e31fbe30f5e34c7f138cd64267 100644 --- a/templates/capsule/index.html.twig +++ b/templates/capsule/index.html.twig @@ -8,20 +8,23 @@ {% block body %} <div class="row gx-0"> - <div class="row-title-box"> - <h3 class="row-title"> + <div class="row-title-box d-flex justify-content-between align-items-center"> + <h2 class="row-title"> {{ 'capsule.title'|trans }} - </h3> + </h2> + <form class="d-none d-md-flex"> + <button id="btn-orange" formaction="/create"> + + {{ 'capsule.create_capsule'|trans }} + </button> + </form> </div> </div> -<div class="mt-4"> - <div class="d-flex justify-content-center align-items-center"> - <form> - <button id="btn-orange" formaction="/create"> - + {{ 'capsule.create_capsule'|trans }} - </button> - </form> - </div> + + <form class="d-md-none d-flex justify-content-center align-items-center"> + <button id="btn-orange" formaction="/create"> + + {{ 'capsule.create_capsule'|trans }} + </button> + </form> {% for flashWarning in app.flashes('warning') %} <div class="text-center alert alert-warning col-5 mx-auto my-5" role="alert"> @@ -37,9 +40,9 @@ </div> - <div class="capsules-list d-flex flex-column m-6"> + <div class="d-flex flex-column align-items-center mt-4 mb-4"> {% for capsule in capsules %} - <div class="capsule-item pb-4 m-5"> + <div class="capsule-item pb-4 col-12 col-lg-10 col-xl-8"> <div class="d-flex flex-column flex-md-row justify-content-center align-items-center mt-sm-4"> <div class="list-item"> <a href="/capsule/preview/{{ capsule.getLinkPath() }}" class="capsule-title"> @@ -64,7 +67,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> @@ -75,12 +78,16 @@ {{ 'capsule.duplicate.link'|trans }} </a> </div> + + {% if capsule.getCreationauthor() is same as current_user %} <div class="list-item text-nowrap"> <a href="/capsule/delete/{{ capsule.getId() }}" class="links text-decoration-none"> <i class="fas fa-trash m-2"></i> {{ 'capsule.delete.link'|trans }} </a> </div> + {% endif %} + <a href="capsule/edit/{{ capsule.getLinkPath() }}" class="list-item text-nowrap lh-md"> <button class="standard-button p-2"> {{ 'capsule.edit.link'|trans }} diff --git a/templates/layout.html.twig b/templates/layout.html.twig index bec0bce48dfa091ed8c509a023908b09a45e174a..deb0ae75ea6f813f39bca214619befa203c58f9c 100644 --- a/templates/layout.html.twig +++ b/templates/layout.html.twig @@ -18,7 +18,7 @@ </head> -<body class="container col-10 col-md-8 col-lg-6 m-auto"> +<body class="container col-10 col-md-8 m-auto"> <div class="position-relative d-flex flex-row align-items-center justify-content-center mb-5"> <a href="/my_capsules" class="align-self-center"> <img id="header-memorekall-logo" class="memorekall-logo" src="{{ asset('build/images/MemoRekall.png') }}"> diff --git a/templates/registration/register.html.twig b/templates/registration/register.html.twig index 45e7b0fdd73cf56386a7f22154511129a1796d82..c7096b6cec7044037752f1031d66aa96f4485cfb 100644 --- a/templates/registration/register.html.twig +++ b/templates/registration/register.html.twig @@ -11,14 +11,14 @@ {% endfor %} {{ form_start(registrationForm, {'attr': {novalidate: 'novalidate', 'class': 'd-flex flex-column justify-content-center'}}) }} - {{ form_row(registrationForm.firstName, {'row_attr': {'class' : 'form-group d-flex flex-column m-auto mb-4 col-6'}}) }} - {{ form_row(registrationForm.lastName, {'row_attr': {'class' : 'form-group d-flex flex-column m-auto mb-4 col-6'}}) }} - {{ form_row(registrationForm.email, {'row_attr': {'class' : 'form-group d-flex flex-column m-auto mb-4 col-6'}}) }} - {{ form_row(registrationForm.plainPassword.first, {'row_attr': {'class' : 'form-group d-flex flex-column m-auto mb-4 col-6'}}) }} - {{ form_row(registrationForm.plainPassword.second, {'row_attr': {'class' : 'form-group d-flex flex-column m-auto mb-4 col-6'}}) }} - {{ form_row(registrationForm.captcha, {'row_attr': {'class' : 'form-group d-flex flex-column m-auto mb-5 col-6'}}) }} + {{ form_row(registrationForm.firstName, {'row_attr': {'class' : 'form-group d-flex flex-column m-auto mb-4 col-12 col-sm-10 col-md-9 col-lg-8 col-xl-7 col-xxl-5'}}) }} + {{ form_row(registrationForm.lastName, {'row_attr': {'class' : 'form-group d-flex flex-column m-auto mb-4 col-12 col-sm-10 col-md-9 col-lg-8 col-xl-7 col-xxl-5'}}) }} + {{ form_row(registrationForm.email, {'row_attr': {'class' : 'form-group d-flex flex-column m-auto mb-4 col-12 col-sm-10 col-md-9 col-lg-8 col-xl-7 col-xxl-5'}}) }} + {{ form_row(registrationForm.plainPassword.first, {'row_attr': {'class' : 'form-group d-flex flex-column m-auto mb-4 col-12 col-sm-10 col-md-9 col-lg-8 col-xl-7 col-xxl-5'}}) }} + {{ form_row(registrationForm.plainPassword.second, {'row_attr': {'class' : 'form-group d-flex flex-column m-auto mb-4 col-12 col-sm-10 col-md-9 col-lg-8 col-xl-7 col-xxl-5'}}) }} + {{ form_row(registrationForm.captcha, {'row_attr': {'class' : 'form-group d-flex flex-column m-auto mb-5 col-12 col-sm-10 col-md-9 col-lg-8 col-xl-7 col-xxl-5'}}) }} {{ form_row(registrationForm.agreeTerms, {'row_attr': {'class' : 'form-group d-flex flex-column m-auto mb-4 col-auto justify-content-center'}, 'label_attr': { 'class' : 'ms-3'}}) }} - {{ form_row(registrationForm.submit, {'row_attr': {'class' : 'form-group d-flex flex-column m-auto mb-5 col-2'}}) }} + {{ form_row(registrationForm.submit, {'row_attr': {'class' : 'form-group d-flex flex-column m-auto mb-5'}}) }} {{ form_end(registrationForm) }} </div> {% endblock %} diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig index f86f18bd8a255236519ecd33e2fce7ad5ca36e53..aff2910422d3ff81631e8d00161287fd617f286e 100644 --- a/templates/security/login.html.twig +++ b/templates/security/login.html.twig @@ -5,21 +5,21 @@ {% endblock %} {% block body %} -<form method="post" class="d-flex flex-column justify-content-center"> {% if error %} <div class="alert alert-danger col-6 m-auto">{{ error.messageKey|trans(error.messageData, 'security') }}</div> {% endif %} {% for flashMessage in app.flashes('email_verified_success') %} - <div class="text-center alert alert-warning col-6 m-auto" role="alert" >{{ flashMessage }}</div> + <div class="text-center alert alert-warning col-6 m-auto" role="alert" >{{ flashMessage }}</div> {% endfor %} - <div class="form-group d-flex flex-column m-auto mb-4 mt-4 col-6"> +<form method="post" class="d-flex flex-column justify-content-center"> + <div class="form-group d-flex flex-column m-auto mb-4 mt-4 col-12 col-sm-10 col-md-9 col-lg-8 col-xl-7 col-xxl-5"> <label for="inputEmail" class="form-label"> {{ 'general.email'|trans }} </label> <input type="email" value="{{ last_username }}" name="email" id="inputEmail" class="form-control" autocomplete="email" required autofocus> </div> - <div class="form-group d-flex flex-column m-auto mb-4 col-6"> + <div class="form-group d-flex flex-column m-auto mb-4 col-12 col-sm-10 col-md-9 col-lg-8 col-xl-7 col-xxl-5"> <label for="inputPassword" class="form-label"> {{ 'general.password'|trans }} </label> @@ -45,7 +45,7 @@ {{ 'login.forgot_password_link'|trans }} </a> - <button class="btn btn-primary col-2 m-auto mt-4" type="submit"> + <button class="btn btn-primary m-auto mt-4" type="submit"> {{ 'login.log_in'|trans }} </button> </form> diff --git a/tests/functional/CapsuleControllerTest.php b/tests/functional/CapsuleControllerTest.php index 829b95310ee6c89478e601472e62bacc31dfbe87..96c813689e90cac2d62731df13d7defe7f4e2ce2 100644 --- a/tests/functional/CapsuleControllerTest.php +++ b/tests/functional/CapsuleControllerTest.php @@ -106,8 +106,6 @@ class CapsuleControllerTest extends WebTestCase $client = static::createClient(); $client->loginUser($this->verified_user); - - $client->request('GET', '/capsule/preview/' . $this->created_capsule->getLinkPath()); $this->assertResponseIsSuccessful('The preview should be allowed for none authenticated user'); } @@ -128,8 +126,11 @@ class CapsuleControllerTest extends WebTestCase $client = static::createClient(); $client->request('GET', '/capsule/edit/' . $this->created_capsule->getLinkPath()); - $this->assertResponseRedirects('/login', 302, 'An unauthenticated user ' - . 'should no access to capsule edition and should be redirected to the login page'); + $this->assertResponseRedirects( + '/login', + 302, + 'An unauthenticated user should no access to capsule edition and should be redirected to the login page' + ); } public function testLoggedUserShouldAccessToItsCapsuleEdition(): void @@ -138,7 +139,8 @@ class CapsuleControllerTest extends WebTestCase $client->loginUser($this->verified_user); $client->request('GET', '/capsule/edit/' . $this->created_capsule->getLinkPath()); - $this->assertResponseIsSuccessful('An authenticated user should be able to access to the edition of its ' - . ' capsules'); + $this->assertResponseIsSuccessful( + 'An authenticated user should be able to access to the edition of its capsules' + ); } } diff --git a/tests/functional/CapsuleEditorControllerTest.php b/tests/functional/CapsuleEditorControllerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2be2f65302c2b9a781fe24ccd080fe31dbc97f08 --- /dev/null +++ b/tests/functional/CapsuleEditorControllerTest.php @@ -0,0 +1,225 @@ +<?php + +namespace App\Tests\functional; + +use App\Entity\Capsule; +use App\Entity\User; +use App\Repository\CapsuleRepository; +use App\Repository\UserRepository; +use Doctrine\Persistence\ObjectManager; +use Symfony\Bundle\FrameworkBundle\KernelBrowser; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + +class CapsuleEditorControllerTest extends WebTestCase +{ + private KernelBrowser $client; + private ObjectManager $object_manager; + private User $user_author; + private User $editor_non_author; + private UserRepository $user_repository; /** @phpstan-ignore-line */ + private CapsuleRepository $capsule_repository; /** @phpstan-ignore-line */ + private Capsule $capsule; + + protected function setUp(): void + { + self::ensureKernelShutdown(); + + $this->client = static::createClient(); + + $this->object_manager = $this->client->getContainer() + ->get('doctrine') + ->getManager(); + + + $this->user_repository = $this->object_manager->getRepository(User::class); + $this->capsule_repository = $this->object_manager->getRepository(Capsule::class); + + $this->setUsers(); + $this->setCapsule(); + } + + protected function tearDown(): void + { + parent::tearDown(); + self::ensureKernelShutdown(); + } + + public function testAuthorShouldBeAbleToDeleteACapsule(): void + { + $this->client->loginUser($this->user_author); + $this->client->request('GET', '/capsule/delete/' . $this->capsule->getId()); + + $this->assertResponseIsSuccessful(); + } + + public function testEditorNonAuthorShouldNotBeAbleToDeleteACapsule(): void + { + $this->client->loginUser($this->editor_non_author); + $this->client->request('GET', '/capsule/delete/' . $this->capsule->getId()); + + $this->assertResponseRedirects('/my_capsules', 302); + } + + public function testAuthorShouldBeAbleToAddANewEditorForACapsule(): void + { + $uri = '/capsule/' . $this->capsule->getId() . '/editors'; + $this->client->loginUser($this->user_author); + $crawler = $this->client->request('GET', $uri); + + $this->assertResponseIsSuccessful(); + + $this->client->enableProfiler(); + $submit_button = $crawler->selectButton('Validate'); + $form = $submit_button->form(); + $form['capsule_editors_form[email]'] = $this->editor_non_author->getEmail(); + $this->client->submit($form); + + $this->assertResponseRedirects($uri, 302); + $this->client->followRedirect(); + $this->assertResponseIsSuccessful($uri); + + $capsule_refreshed = $this->capsule_repository->findOneBy(['id' => $this->capsule->getId()]); + if (! $capsule_refreshed instanceof Capsule) { + throw new \Exception("Capsule does not exist."); + } + + $editor = $this->user_repository->findOneBy(['id' => $this->editor_non_author->getId()]); + $this->assertSame($editor, $capsule_refreshed->getEditors()->last()); + } + + public function testEditorShouldBeAbleToAccessTheCapsuleEditorsPage(): void + { + $this->capsule->addEditor($this->editor_non_author); + $uri = '/capsule/' . $this->capsule->getId() . '/editors'; + $this->client->loginUser($this->editor_non_author); + + $this->client->request('GET', $uri); + + $this->assertResponseIsSuccessful(); + } + + public function testNonRegisteredUserAddedAsEditorShouldReceiveAnEmail(): void + { + $uri = '/capsule/' . $this->capsule->getId() . '/editors'; + $this->client->loginUser($this->user_author); + $crawler = $this->client->request('GET', $uri); + + $this->assertResponseIsSuccessful(); + + $this->client->enableProfiler(); + $submit_button = $crawler->selectButton('Validate'); + $non_registered_user_email = "non_registered_user@email.fr"; + $form = $submit_button->form(); + $form['capsule_editors_form[email]'] = $non_registered_user_email; + $this->client->submit($form); + + $this->assertResponseRedirects($uri, 302); + + $this->assertEmailCount(1); + $emailMessage = $this->getMailerMessage(0); + + if (null === $emailMessage) { + throw new \Exception("Email message could not be found"); + } + + $this->assertEmailAddressContains( + $emailMessage, + 'To', + $non_registered_user_email + ); + + $this->client->followRedirect(); + $this->assertResponseIsSuccessful($uri); + } + + public function testRegisteredUserShouldReceiveAnEmailWithCapsuleEditionLink(): void + { + $uri = '/capsule/' . $this->capsule->getId() . '/editors'; + $this->client->loginUser($this->user_author); + $crawler = $this->client->request('GET', $uri); + + $this->assertResponseIsSuccessful(); + + $this->client->enableProfiler(); + $submit_button = $crawler->selectButton('Validate'); + $form = $submit_button->form(); + $form['capsule_editors_form[email]'] = $this->editor_non_author->getEmail(); + $this->client->submit($form); + + $this->assertResponseRedirects($uri, 302); + + $this->assertEmailCount(1); + $emailMessage = $this->getMailerMessage(0); + + if (null === $emailMessage) { + throw new \Exception("Email message could not be found"); + } + + $this->assertEmailAddressContains( + $emailMessage, + 'To', + $this->editor_non_author->getEmail() + ); + + $this->client->followRedirect(); + $this->assertResponseIsSuccessful($uri); + } + + public function testAlreadyAddedEditorShouldNotReceiveAnEmail(): void + { + $this->capsule->addEditor($this->editor_non_author); + $this->object_manager->persist($this->capsule); + $this->object_manager->flush(); + + $this->assertContains($this->editor_non_author, $this->capsule->getEditors()->toArray()); + $uri = '/capsule/' . $this->capsule->getId() . '/editors'; + $this->client->loginUser($this->user_author); + $crawler = $this->client->request('GET', $uri); + + $this->assertResponseIsSuccessful(); + + $this->client->enableProfiler(); + $submit_button = $crawler->selectButton('Validate'); + $form = $submit_button->form(); + $form['capsule_editors_form[email]'] = $this->editor_non_author->getEmail(); + $this->client->submit($form); + + $this->assertEmailCount(0); + $this->assertResponseRedirects($uri, 302); + + $this->client->followRedirect(); + $this->assertResponseIsSuccessful($uri); + } + + private function setUsers(): void + { + $verified_user_1 = $this->user_repository + ->findOneBy(['email' => 'defaultUser@localhost.com']); + + if (! $verified_user_1 instanceof User) { + throw new \Exception("User does not exist."); + } + + $this->user_author = $verified_user_1; + + $verified_user_2 = $this->user_repository + ->findOneBy(['email' => 'defaultUser2@localhost.com']); + + if (! $verified_user_2 instanceof User) { + throw new \Exception("User does not exist."); + } + + $this->editor_non_author = $verified_user_2; + } + + private function setCapsule(): void + { + $capsule = $this->capsule_repository->findOneBy(['name' => 'Pomme']); + + if (! $capsule instanceof Capsule) { + throw new \Exception("Capsule does not exist."); + } + + $this->capsule = $capsule; + } +} diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml index b6e3029b3d13c41d03059eed90d7f7ca774c477f..7e70101b17d492bd2f9a35ec93529ef8a64f811c 100644 --- a/translations/messages.en.yaml +++ b/translations/messages.en.yaml @@ -62,13 +62,14 @@ capsule: video_url: Youtube or Vimeo video URL created_success: Capsule capsule_name was created successfully no_edition_access: No edition access - edition_not_allowed: You are not allowed to edit this capsule contact_capsule_author_for_access: Please contact the author to gain access the edition mode not_found: The project doesn't exist edit_permissions: link: Edit permissions edit: link: Edit capsule + not_allowed: You are not allowed to edit this capsule + not_found: The capsule was not found duplicate: link: Duplicate capsule title: Duplicate capsule @@ -106,4 +107,32 @@ 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 + email: + title: Invitation to edit a MemoRekall capsule + text: You have been added by %user_name% as editor of the capsule "%capsule_name%". + In order to access and edit it, you first need to register on MemoRekall. Please follow this link to + link: https://project.memorekall.com/register/ + link_name: register + user: + success: The user user_email is now an editor of the capsule capsule_name. + He will receive an email to inform him he has been added as an editor of this capsule. + already_added: The user user_email is already an editor of this capsule + email: + title: New capsule on your list + text: You have been add by %user_name% as editor of the capsule "%capsule_name%". + You can now access and edit it. You will find the capsule in your capsule list. + link: Go to capsule edition page diff --git a/translations/messages.fr.yaml b/translations/messages.fr.yaml index 9fff9516c6726f3625dc8882cd317b063f54b269..3745a308579bd51b55da0808b7aa80e92e1c7468 100644 --- a/translations/messages.fr.yaml +++ b/translations/messages.fr.yaml @@ -60,12 +60,13 @@ capsule: video_url: URL de la video Youtube ou Vimeo created_success: La capsule capsule_name a été créée no_edition_access: Pas d'accès au mode édition - edition_not_allowed: Vous n'êtes pas autorisé a modifier cette capsule contact_capsule_author_for_access: Veuillez contacter l'auteur de la capsule pour lui demander son accès en mode édition edit_permissions: link: Modifier les permissions edit: link: Modifier la capsule + not_allowed: Vous n'êtes pas autorisé a modifier cette capsule + not_found: La capsule n'existe pas duplicate: link: Dupliquer la capsule title: Dupliquer la capsule @@ -104,4 +105,32 @@ 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 + email: + title: Invitation pour éditer une capsule sur MemoRekall + text: Vous avez été ajouté par %user_name% en tant qu'éditeur de la capsule %capsule_name%. + Avant de pouvoir y accéder et la modifier, vous devez d'abord créer un compte sur MemoRekall. Veuillez suivre ce lien pour + link: https://project.memorekall.com/register/ + link_name: créer votre compte + user: + success: L'utilisateur user_email est maintenant éditeur de la capsule capsule_name. + Il recevera un e-mail l'informant qu'il a été ajouté en tant qu'éditeur de la capsule. + already_added: L'utilisateur user_email est déjà editeur de la capsule + email: + title: Nouvelle capsule dans votre liste + text: Vous avez été ajouté par %user_name% en tant qu'éditeur de la capsule "%capsule_name%". + Vous pouvez maintenant y accéder et l'éditer. Vous la retrouverez dans la liste de vos capsules. + link: Se rendre sur la page d'édition de la capsule \ No newline at end of file