diff --git a/migrations/mysql/Version20210122203139.php b/migrations/mysql/Version20210122203139.php new file mode 100644 index 0000000..c5e14bf --- /dev/null +++ b/migrations/mysql/Version20210122203139.php @@ -0,0 +1,33 @@ +addSql('ALTER TABLE user_class DROP FOREIGN KEY FK_F89E2C7AA23F6C8'); + $this->addSql('ALTER TABLE user_class ADD CONSTRAINT FK_F89E2C7AA23F6C8 FOREIGN KEY (next_id) REFERENCES user_class (id) ON DELETE CASCADE'); + } + + public function down(Schema $schema) : void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE user_class DROP FOREIGN KEY FK_F89E2C7AA23F6C8'); + $this->addSql('ALTER TABLE user_class ADD CONSTRAINT FK_F89E2C7AA23F6C8 FOREIGN KEY (next_id) REFERENCES user_class (id)'); + } +} diff --git a/src/Controller/UserClassController.php b/src/Controller/UserClassController.php index 9cfe227..1f9ed5d 100644 --- a/src/Controller/UserClassController.php +++ b/src/Controller/UserClassController.php @@ -32,6 +32,8 @@ use App\Repository\UserClassRepository; use App\Security\Voter\UserClassVoter; use DateTime; +use Doctrine\DBAL\ConnectionException; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -44,7 +46,7 @@ class UserClassController extends AbstractController */ public function index(UserClassRepository $userClassRepository): Response { - $userClasses = $userClassRepository->findBy([], ['rank' => 'ASC']); + $userClasses = $userClassRepository->findAll(); return $this->render('user_class/index.html.twig', [ 'userClass' => new UserClass(), @@ -61,13 +63,12 @@ public function show(UserClass $userClass): Response return $this->render('user_class/show.html.twig', [ 'userClass' => $userClass, ]); - } /** * @Route("/userclass/{id}/delete", name="userclass_delete", requirements={"id"="\d+"}) */ - public function delete(UserClass $userClass, Request $request): Response + public function delete(UserClass $userClass, Request $request, EntityManagerInterface $em): Response { $this->denyAccessUnlessGranted(UserClassVoter::DELETE, $userClass); @@ -75,13 +76,42 @@ public function delete(UserClass $userClass, Request $request): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $userClass = $form->getData(); - - $entityManager = $this->getDoctrine()->getManager(); - $entityManager->remove($userClass); - $entityManager->flush(); - - $this->redirectToRoute('userclass_show', ['id' => $userClass->getId()]); + if (!$userClass->getSharables()->isEmpty()) { + $sharableTarget = $form->get('target')->getData(); + assert($sharableTarget instanceof UserClass); + foreach ($userClass->getSharables() as $sharable) { + $sharable->setVisibleBy($sharableTarget); + $em->persist($sharable); + $em->flush(); + } + } + if (!$userClass->getUsers()->isEmpty()) { + $userTarget = is_null($userClass->getPrev()) ? $userClass->getNext() : $userClass->getPrev(); + foreach ($userClass->getUsers() as $user) { + $user->setUserClass($userTarget); + $em->persist($user); + $em->flush(); + } + } + + $em->getConnection()->beginTransaction(); + try { + $prev = $userClass->getPrev(); + if (!is_null($prev)) { + $prev->setNext(null); + $em->persist($prev); + $em->flush(); + $prev->setNext($userClass->getNext()); + } + $em->remove($userClass); + $em->flush(); + $em->getConnection()->commit(); + } catch (ConnectionException $e) { + $em->getConnection()->rollBack(); + throw $e; + } + + return $this->redirectToRoute('userclass_show', ['id' => $userClass->getId()]); } return $this->render('user_class/delete.html.twig', [ @@ -102,13 +132,15 @@ public function edit(UserClass $userClass, Request $request): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $userClass = $form->getData(); + assert($userClass instanceof UserClass); + $userClass->setLastEditedAt(new DateTime()); - $entityManager = $this->getDoctrine()->getManager(); - $entityManager->persist($userClass); - $entityManager->flush(); + $em = $this->getDoctrine()->getManager(); + $em->persist($userClass); + $em->flush(); - $this->redirectToRoute('userclass_show', ['id' => $userClass->getId()]); + return $this->redirectToRoute('userclass_show', ['id' => $userClass->getId()]); } return $this->render('user_class/edit.html.twig', [ @@ -120,8 +152,11 @@ public function edit(UserClass $userClass, Request $request): Response /** * @Route("/userclass/new", name="userclass_new") */ - public function new(Request $request): Response - { + public function new( + Request $request, + UserClassRepository $userClassRepository, + EntityManagerInterface $em + ): Response { $userClass = new UserClass(); $this->denyAccessUnlessGranted(UserClassVoter::CREATE, $userClass); @@ -130,12 +165,25 @@ public function new(Request $request): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $userClass = $form->getData(); - - $entityManager = $this->getDoctrine()->getManager(); - $entityManager->persist($userClass); - $entityManager->flush(); - - $this->redirectToRoute('userclass_show', ['id' => $userClass->getId()]); + assert($userClass instanceof UserClass); + $replaced = $userClassRepository->findOneBy(['next' => $userClass->getNext()]); + $em->getConnection()->beginTransaction(); + try { + if (!is_null($replaced)) { + $replaced->setNext(null); + $em->persist($replaced); + $em->flush(); + $replaced->setNext($userClass); + } + $em->persist($userClass); + $em->flush(); + $em->getConnection()->commit(); + } catch (ConnectionException $e) { + $em->getConnection()->rollBack(); + throw $e; + } + + return $this->redirectToRoute('userclass_show', ['id' => $userClass->getId()]); } return $this->render('user_class/new.html.twig', [ diff --git a/src/Entity/Sharable.php b/src/Entity/Sharable.php index f1176a0..9fd34bc 100644 --- a/src/Entity/Sharable.php +++ b/src/Entity/Sharable.php @@ -114,7 +114,7 @@ public static function interestedOptionsValues() private $description; /** - * @ORM\ManyToOne(targetEntity=UserClass::class) + * @ORM\ManyToOne(targetEntity=UserClass::class, inversedBy="sharables") */ private $visibleBy; diff --git a/src/Entity/UserClass.php b/src/Entity/UserClass.php index 350ffc4..01675aa 100644 --- a/src/Entity/UserClass.php +++ b/src/Entity/UserClass.php @@ -34,7 +34,6 @@ use App\Security\Voter\UserVoter; use DateTime; use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Validator\Context\ExecutionContextInterface; /** * @ORM\Entity(repositoryClass=UserClassRepository::class) @@ -135,6 +134,7 @@ class UserClass /** * @ORM\OneToOne(targetEntity=UserClass::class, inversedBy="prev", cascade={"persist", "remove"}) + * @ORM\JoinColumn(onDelete="cascade") */ private $next; @@ -143,6 +143,11 @@ class UserClass */ private $prev; + /** + * @ORM\OneToMany(targetEntity=Sharable::class, mappedBy="visibleBy") + */ + private $sharables; + public function __construct() { $this->users = new ArrayCollection(); @@ -386,4 +391,34 @@ public function setPrev(?self $prev): self return $this; } + + /** + * @return Collection|Sharable[] + */ + public function getSharables(): Collection + { + return $this->sharables; + } + + public function addSharable(Sharable $sharable): self + { + if (!$this->sharables->contains($sharable)) { + $this->sharables[] = $sharable; + $sharable->setVisibleBy($this); + } + + return $this; + } + + public function removeSharable(Sharable $sharable): self + { + if ($this->sharables->removeElement($sharable)) { + // set the owning side to null (unless already changed) + if ($sharable->getVisibleBy() === $this) { + $sharable->setVisibleBy(null); + } + } + + return $this; + } } diff --git a/src/Form/UserClassDeleteType.php b/src/Form/UserClassDeleteType.php index 41a72dd..1e868f3 100644 --- a/src/Form/UserClassDeleteType.php +++ b/src/Form/UserClassDeleteType.php @@ -27,7 +27,10 @@ namespace App\Form; use App\Entity\UserClass; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -35,21 +38,42 @@ class UserClassDeleteType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { + $choices = []; + $userClass = $builder->getData(); + assert($userClass instanceof UserClass); + + if (!$userClass->getSharables()->isEmpty()) { + if (!is_null($userClass->getPrev())) { + $choices['previous'] = [$userClass->getPrev()]; + $data = $userClass->getPrev(); + } + if (!is_null($userClass->getNext())) { + $choices['next'] = [$userClass->getNext()]; + $data = $userClass->getNext(); + } + + $builder + ->add('target', EntityType::class, [ + 'class' => UserClass::class, + 'mapped' => false, + 'choices' => $choices, + 'required' => true, + 'label' => 'Target user class for sharables', + 'help' => 'Select the new user class for sharables that where accessible by this user class' + ]) + ; + } $builder - ->add('rank') - ->add('name') - ->add('share') - ->add('access') - ->add('canInvite') - ->add('maxParanoia') - ->add('inviteFrequency') - ->add('shareScoreReq') - ->add('accountAgeReq') - ->add('validatedReq') - ->add('verifiedReq') - ->add('createdAt') - ->add('lastEditedAt') + ->add('delete', SubmitType::class, [ + 'label' => 'delete user class', + 'attr' => [ + 'class' => 'btn-danger', + ] + ]) ; + if (isset($data)) { + $builder->get('target')->setData($data); + } } public function configureOptions(OptionsResolver $resolver) diff --git a/src/Form/UserClassType.php b/src/Form/UserClassType.php index 8775000..bbd337f 100644 --- a/src/Form/UserClassType.php +++ b/src/Form/UserClassType.php @@ -27,8 +27,10 @@ namespace App\Form; use App\Entity\UserClass; +use App\Repository\UserClassRepository; use App\Security\Voter\UserVoter; use App\Validator\Rank; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\IntegerType; @@ -42,8 +44,30 @@ class UserClassType extends AbstractType { + private UserClassRepository $userClassRepository; + + public function __construct(UserClassRepository $userClassRepository) + { + $this->userClassRepository = $userClassRepository; + } + public function buildForm(FormBuilderInterface $builder, array $options) { + $userClass = $builder->getData(); + assert($userClass instanceof UserClass); + + // If The it's a new user class + if (is_null($userClass->getId())) { + $builder->add('next', EntityType::class, [ + 'class' => UserClass::class, + 'label' => 'Next user class', + 'choices' => $this->userClassRepository->findAll(), + 'placeholder' => 'none', + 'help' => 'Your user class will be placed before the selected one.', + 'required' => false, + ]); + } + $builder ->add('rank', IntegerType::class, [ 'label' => 'Rank', @@ -76,6 +100,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) assert($userClass instanceof UserClass); $form = $event->getForm(); + + if ($userClass->getId() === null) { $form->add('create', SubmitType::class); } else { diff --git a/src/Repository/UserClassRepository.php b/src/Repository/UserClassRepository.php index 642797d..1c0178f 100644 --- a/src/Repository/UserClassRepository.php +++ b/src/Repository/UserClassRepository.php @@ -119,6 +119,15 @@ public function findFirst(): ?UserClass return $query->getOneOrNullResult(); } + public function findLast(): ?UserClass + { + $qb = $this->createQueryBuilder('u'); + return $qb->andWhere($qb->expr()->isNull('u.next')) + ->getQuery() + ->getOneOrNullResult() + ; + } + /** * Recursive SQL raw query to find a list of user class between two others. * @@ -156,6 +165,15 @@ public function findBetween(UserClass $from, ?UserClass $to = null): array return $result; } + /** + * @return UserClass[] All the user classes sorted from first to last + */ + public function findAll(): array + { + $first = $this->findFirst(); + return $this->findBetween($first); + } + /* public function findOneBySomeField($value): ?UserClass { diff --git a/templates/user_class/delete.html.twig b/templates/user_class/delete.html.twig index eb6358a..8154f91 100644 --- a/templates/user_class/delete.html.twig +++ b/templates/user_class/delete.html.twig @@ -14,11 +14,44 @@ ]) }} -

Delete

+

{{ userClass.name }}

-{{ form(form) }} +
+
+ +
+

delete

+
+
+ +
+ + {% if userClass.users|length %} +
+ {% if userClass.prev %} + {{ userClass.users|length }} users will be downgraded to closest previous user class : {{ userClass.prev.name }}. + {% else %} + {{ userClass.users|length }} users will be upgraded to closest next user class : {{ userClass.next.name }} + {% endif %} +
+ {% endif %} + + {% if userClass.sharables|length %} +
{{ userClass.sharables|length }} sharable(s) are set as visible by this user class. It's recommend to move them to upper class, but you can select what you prefer.
+ {% endif %} + +

+ +

+ + + {{ form(form) }} + + +
+
{% endblock %} \ No newline at end of file diff --git a/templates/user_class/show.html.twig b/templates/user_class/show.html.twig index 4e2d3bc..6b5db1a 100644 --- a/templates/user_class/show.html.twig +++ b/templates/user_class/show.html.twig @@ -44,7 +44,6 @@
-