From: Raphaƫl Gertz <git@rapsys.eu>
Date: Mon, 28 Dec 2020 07:15:29 +0000 (+0100)
Subject: Add snippet feature
X-Git-Tag: 0.1.7~38
X-Git-Url: https://git.rapsys.eu/airbundle/commitdiff_plain/42d2646c377a92fd92d37e09ef6b733fa9b50946

Add snippet feature
---

diff --git a/Controller/SnippetController.php b/Controller/SnippetController.php
new file mode 100644
index 0000000..6335b1f
--- /dev/null
+++ b/Controller/SnippetController.php
@@ -0,0 +1,270 @@
+<?php
+
+namespace Rapsys\AirBundle\Controller;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\RequestContext;
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+use Rapsys\AirBundle\Entity\Location;
+use Rapsys\AirBundle\Entity\Snippet;
+use Rapsys\AirBundle\Entity\User;
+
+class SnippetController extends DefaultController {
+	/**
+	 * Add snippet
+	 *
+	 * @desc Persist snippet in database
+	 *
+	 * @param Request $request The request instance
+	 *
+	 * @return Response The rendered view or redirection
+	 *
+	 * @throws \RuntimeException When user has not at least guest role
+	 */
+	public function add(Request $request) {
+		//Prevent non-guest to access here
+		$this->denyAccessUnlessGranted('ROLE_GUEST', null, $this->translator->trans('Unable to access this page without role %role%!', ['%role%' => $this->translator->trans('Guest')]));
+
+		//Create ApplicationType form
+		$form = $this->createForm('Rapsys\AirBundle\Form\SnippetType', null, [
+			//Set the action
+			'action' => $this->generateUrl('rapsys_air_snippet_add'),
+			//Set the form attribute
+			'attr' => []
+		]);
+
+		//Refill the fields in case of invalid form
+		$form->handleRequest($request);
+
+		//Prevent creating snippet for other user unless admin
+		if ($form->get('user')->getData() !== $this->getUser()) {
+			//Prevent non-admin to access here
+			$this->denyAccessUnlessGranted('ROLE_ADMIN', null, $this->translator->trans('Unable to access this page without role %role%!', ['%role%' => $this->translator->trans('Admin')]));
+		}
+
+		//Handle invalid form
+		if (!$form->isSubmitted() || !$form->isValid()) {
+			//Set section
+			$section = $this->translator->trans('Snippet add');
+
+			//Set title
+			$title = $this->translator->trans($this->config['site']['title']).' - '.$section;
+
+			//Render the view
+			return $this->render('@RapsysAir/snippet/add.html.twig', ['title' => $title, 'section' => $section, 'form' => $form->createView()]+$this->context);
+		}
+
+		//Get doctrine
+		$doctrine = $this->getDoctrine();
+
+		//Get manager
+		$manager = $doctrine->getManager();
+
+		//Get snippet
+		$snippet = $form->getData();
+
+		//Set created
+		$snippet->setCreated(new \DateTime('now'));
+
+		//Set updated
+		$snippet->setUpdated(new \DateTime('now'));
+
+		//Queue snippet save
+		$manager->persist($snippet);
+
+		//Flush to get the ids
+		$manager->flush();
+
+		//Add notice
+		$this->addFlash('notice', $this->translator->trans('Snippet in %locale% %location% for %user% created', ['%locale%' => $snippet->getLocale(), '%location%' => $this->translator->trans('at '.$snippet->getLocation()), '%user%' => $snippet->getUser()]));
+
+		//Extract and process referer
+		if ($referer = $request->headers->get('referer')) {
+			//Create referer request instance
+			$req = Request::create($referer);
+
+			//Get referer path
+			$path = $req->getPathInfo();
+
+			//Get referer query string
+			$query = $req->getQueryString();
+
+			//Remove script name
+			$path = str_replace($request->getScriptName(), '', $path);
+
+			//Try with referer path
+			try {
+				//Save old context
+				$oldContext = $this->router->getContext();
+
+				//Force clean context
+				//XXX: prevent MethodNotAllowedException because current context method is POST in onevendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php+42
+				$this->router->setContext(new RequestContext());
+
+				//Retrieve route matching path
+				$route = $this->router->match($path);
+
+				//Reset context
+				$this->router->setContext($oldContext);
+
+				//Clear old context
+				unset($oldContext);
+
+				//Extract name
+				$name = $route['_route'];
+
+				//Remove route and controller from route defaults
+				unset($route['_route'], $route['_controller']);
+
+				//Check if snippet view route
+				if ($name == 'rapsys_air_organizer_view' && !empty($route['id'])) {
+					//Replace id
+					$route['id'] = $snippet->getUser()->getId();
+				//Other routes
+				} else {
+					//Set snippet
+					$route['snippet'] = $snippet->getId();
+				}
+
+				//Generate url
+				return $this->redirectToRoute($name, $route);
+			//No route matched
+			} catch(MethodNotAllowedException|ResourceNotFoundException $e) {
+				//Unset referer to fallback to default route
+				unset($referer);
+			}
+		}
+
+		//Redirect to cleanup the form
+		return $this->redirectToRoute('rapsys_air', ['snippet' => $snippet->getId()]);
+	}
+
+	/**
+	 * Edit snippet
+	 *
+	 * @desc Persist snippet in database
+	 *
+	 * @param Request $request The request instance
+	 *
+	 * @return Response The rendered view or redirection
+	 *
+	 * @throws \RuntimeException When user has not at least guest role
+	 */
+	public function edit(Request $request, $id) {
+		//Prevent non-guest to access here
+		$this->denyAccessUnlessGranted('ROLE_GUEST', null, $this->translator->trans('Unable to access this page without role %role%!', ['%role%' => $this->translator->trans('Guest')]));
+
+		//Get doctrine
+		$doctrine = $this->getDoctrine();
+
+		//Get snippet
+		if (empty($snippet = $doctrine->getRepository(Snippet::class)->findOneById($id))) {
+			throw $this->createNotFoundException($this->translator->trans('Unable to find snippet: %id%', ['%id%' => $id]));
+		}
+
+		//Create ApplicationType form
+		$form = $this->createForm('Rapsys\AirBundle\Form\SnippetType', $snippet, [
+			//Set the action
+			'action' => $this->generateUrl('rapsys_air_snippet_edit', ['id' => $id]),
+			//Set the form attribute
+			'attr' => []
+		]);
+
+		//Refill the fields in case of invalid form
+		$form->handleRequest($request);
+
+		//Prevent creating snippet for other user unless admin
+		if ($form->get('user')->getData() !== $this->getUser()) {
+			//Prevent non-admin to access here
+			$this->denyAccessUnlessGranted('ROLE_ADMIN', null, $this->translator->trans('Unable to access this page without role %role%!', ['%role%' => $this->translator->trans('Admin')]));
+		}
+
+		//Handle invalid form
+		if (!$form->isSubmitted() || !$form->isValid()) {
+			//Set section
+			$section = $this->translator->trans('Snippet %id%', ['%id%' => $id]);
+
+			//Set title
+			$title = $this->translator->trans($this->config['site']['title']).' - '.$section;
+
+			//Render the view
+			return $this->render('@RapsysAir/snippet/edit.html.twig', ['id' => $id, 'title' => $title, 'section' => $section, 'form' => $form->createView()]+$this->context);
+		}
+
+		//Get manager
+		$manager = $doctrine->getManager();
+
+		//Set updated
+		$snippet->setUpdated(new \DateTime('now'));
+
+		//Queue snippet save
+		$manager->persist($snippet);
+
+		//Flush to get the ids
+		$manager->flush();
+
+		//Add notice
+		$this->addFlash('notice', $this->translator->trans('Snippet %id% in %locale% %location% for %user% updated', ['%id%' => $id, '%locale%' => $snippet->getLocale(), '%location%' => $this->translator->trans('at '.$snippet->getLocation()), '%user%' => $snippet->getUser()]));
+
+		//Extract and process referer
+		if ($referer = $request->headers->get('referer')) {
+			//Create referer request instance
+			$req = Request::create($referer);
+
+			//Get referer path
+			$path = $req->getPathInfo();
+
+			//Get referer query string
+			$query = $req->getQueryString();
+
+			//Remove script name
+			$path = str_replace($request->getScriptName(), '', $path);
+
+			//Try with referer path
+			try {
+				//Save old context
+				$oldContext = $this->router->getContext();
+
+				//Force clean context
+				//XXX: prevent MethodNotAllowedException because current context method is POST in onevendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php+42
+				$this->router->setContext(new RequestContext());
+
+				//Retrieve route matching path
+				$route = $this->router->match($path);
+
+				//Reset context
+				$this->router->setContext($oldContext);
+
+				//Clear old context
+				unset($oldContext);
+
+				//Extract name
+				$name = $route['_route'];
+
+				//Remove route and controller from route defaults
+				unset($route['_route'], $route['_controller']);
+
+				//Check if snippet view route
+				if ($name == 'rapsys_air_organizer_view' && !empty($route['id'])) {
+					//Replace id
+					$route['id'] = $snippet->getUser()->getId();
+				//Other routes
+				} else {
+					//Set snippet
+					$route['snippet'] = $snippet->getId();
+				}
+
+				//Generate url
+				return $this->redirectToRoute($name, $route);
+			//No route matched
+			} catch(MethodNotAllowedException|ResourceNotFoundException $e) {
+				//Unset referer to fallback to default route
+				unset($referer);
+			}
+		}
+
+		//Redirect to cleanup the form
+		return $this->redirectToRoute('rapsys_air', ['snippet' => $snippet->getId()]);
+	}
+}
diff --git a/Entity/Snippet.php b/Entity/Snippet.php
new file mode 100644
index 0000000..2aa9d3b
--- /dev/null
+++ b/Entity/Snippet.php
@@ -0,0 +1,175 @@
+<?php
+
+namespace Rapsys\AirBundle\Entity;
+
+use Rapsys\AirBundle\Entity\Location;
+use Rapsys\AirBundle\Entity\User;
+
+/**
+ * Snippet
+ */
+class Snippet {
+	/**
+	 * @var integer
+	 */
+	private $id;
+
+	/**
+	 * @var string
+	 */
+	protected $locale;
+
+	/**
+	 * @var text
+	 */
+	protected $description;
+
+	/**
+	 * @var \DateTime
+	 */
+	protected $created;
+
+	/**
+	 * @var \DateTime
+	 */
+	protected $updated;
+
+	/**
+	 * @var \Rapsys\UserBundle\Entity\Location
+	 */
+	protected $location;
+
+	/**
+	 * @var \Rapsys\UserBundle\Entity\User
+	 */
+	protected $user;
+
+	/**
+	 * Get id
+	 *
+	 * @return integer
+	 */
+	public function getId() {
+		return $this->id;
+	}
+
+	/**
+	 * Set locale
+	 *
+	 * @param string $locale
+	 *
+	 * @return Snippet
+	 */
+	public function setLocale($locale) {
+		$this->locale = $locale;
+
+		return $this;
+	}
+
+	/**
+	 * Get locale
+	 *
+	 * @return string
+	 */
+	public function getLocale() {
+		return $this->locale;
+	}
+
+	/**
+	 * Set description
+	 *
+	 * @param string $description
+	 *
+	 * @return Snippet
+	 */
+	public function setDescription($description) {
+		$this->description = $description;
+
+		return $this;
+	}
+
+	/**
+	 * Get description
+	 *
+	 * @return string
+	 */
+	public function getDescription() {
+		return $this->description;
+	}
+
+	/**
+	 * Set created
+	 *
+	 * @param \DateTime $created
+	 *
+	 * @return User
+	 */
+	public function setCreated($created) {
+		$this->created = $created;
+
+		return $this;
+	}
+
+	/**
+	 * Get created
+	 *
+	 * @return \DateTime
+	 */
+	public function getCreated() {
+		return $this->created;
+	}
+
+	/**
+	 * Set updated
+	 *
+	 * @param \DateTime $updated
+	 *
+	 * @return User
+	 */
+	public function setUpdated($updated) {
+		$this->updated = $updated;
+
+		return $this;
+	}
+
+	/**
+	 * Get updated
+	 *
+	 * @return \DateTime
+	 */
+	public function getUpdated() {
+		return $this->updated;
+	}
+
+	/**
+	 * Set location
+	 */
+	public function setLocation(Location $location) {
+		$this->location = $location;
+
+		return $this;
+	}
+
+	/**
+	 * Get location
+	 */
+	public function getLocation() {
+		return $this->location;
+	}
+
+	/**
+	 * Set user
+	 */
+	public function setUser(User $user) {
+		$this->user = $user;
+
+		return $this;
+	}
+
+	/**
+	 * Get user
+	 */
+	public function getUser() {
+		return $this->user;
+	}
+}
diff --git a/Form/SnippetType.php b/Form/SnippetType.php
new file mode 100644
index 0000000..9c21171
--- /dev/null
+++ b/Form/SnippetType.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Rapsys\AirBundle\Form;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+#use Symfony\Bridge\Doctrine\Form\Type\EntityType;
+use Symfony\Component\Form\Extension\Core\Type\HiddenType;
+use Symfony\Component\Form\Extension\Core\Type\TextType;
+use Symfony\Component\Form\Extension\Core\Type\TextareaType;
+use Symfony\Component\Form\Extension\Core\Type\SubmitType;
+use Symfony\Component\Validator\Constraints\NotBlank;
+use Rapsys\AirBundle\Form\Extension\Type\HiddenEntityType;
+use Rapsys\AirBundle\Entity\Location;
+use Rapsys\AirBundle\Entity\User;
+use Rapsys\AirBundle\Entity\Snippet;
+
+class SnippetType extends AbstractType {
+	/**
+	 * {@inheritdoc}
+	 */
+	public function buildForm(FormBuilderInterface $builder, array $options) {
+		return $builder
+			->add('locale', HiddenType::class, ['required' => true])
+			->add('location', HiddenEntityType::class, ['required' => true])
+			->add('user', HiddenEntityType::class, ['required' => true])
+			->add('description', TextareaType::class, ['attr' => ['placeholder' => 'Your description', 'cols' => 50, 'rows' => 15], 'constraints' => [new NotBlank(['message' => 'Please provide your description'])], 'required' => true])
+			->add('submit', SubmitType::class, ['label' => 'Send', 'attr' => ['class' => 'submit']]);
+			#->add('delete', SubmitType::class, ['label' => 'Remove', 'attr' => ['class' => 'submit']]);
+	}
+
+	/**
+	 * {@inheritdoc}
+	 */
+	public function configureOptions(OptionsResolver $resolver) {
+		$resolver->setDefaults(['data_class' => Snippet::class, 'error_bubbling' => true, 'location' => null, 'user' => null]);
+		$resolver->setAllowedTypes('location', [Location::class, 'null']);
+		$resolver->setAllowedTypes('user', [User::class, 'null']);
+	}
+
+	/**
+	 * {@inheritdoc}
+	 */
+	public function getName() {
+		return 'snippet_form';
+	}
+}
diff --git a/Repository/SnippetRepository.php b/Repository/SnippetRepository.php
new file mode 100644
index 0000000..a1d696b
--- /dev/null
+++ b/Repository/SnippetRepository.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Rapsys\AirBundle\Repository;
+
+use Symfony\Component\Translation\TranslatorInterface;
+use Doctrine\ORM\Query\ResultSetMapping;
+
+/**
+ * SnippetRepository
+ */
+class SnippetRepository extends \Doctrine\ORM\EntityRepository {
+	/**
+	 * Find snippets by locale and user id
+	 *
+	 * @param string $locale The locale
+	 * @param User|int $user The user
+	 * @return array The snippets or empty array
+	 */
+	public function findByLocaleUserId($locale, $user) {
+		//Fetch snippets
+		$ret = $this->getEntityManager()
+			->createQuery('SELECT s FROM RapsysAirBundle:Snippet s WHERE s.locale = :locale and s.user = :user')
+			->setParameter('locale', $locale)
+			->setParameter('user', $user)
+			->getResult();
+
+		//Send result
+		return $ret;
+	}
+}
diff --git a/Resources/config/doctrine/Snippet.orm.yml b/Resources/config/doctrine/Snippet.orm.yml
new file mode 100644
index 0000000..a6e9165
--- /dev/null
+++ b/Resources/config/doctrine/Snippet.orm.yml
@@ -0,0 +1,35 @@
+Rapsys\AirBundle\Entity\Snippet:
+    type: entity
+    repositoryClass: Rapsys\AirBundle\Repository\SnippetRepository
+    table: snippets
+    id:
+        id:
+            type: integer
+            generator:
+                strategy: AUTO
+            options:
+                unsigned: true
+    fields:
+        locale:
+            type: string
+            length: 2
+        description:
+            type: text
+        created:
+            type: datetime
+        updated:
+            type: datetime
+    manyToOne:
+        location:
+            targetEntity: Rapsys\AirBundle\Entity\Location
+            inversedBy: snippets
+        user:
+            targetEntity: Rapsys\AirBundle\Entity\User
+            inversedBy: snippets
+    indexes:
+        #XXX: may be used in SnippetRepository::findByLocaleUserId
+        locale_user:
+            columns: [ locale, user_id ]
+    uniqueConstraints:
+        locale_location_user:
+            columns: [ locale, location_id, user_id ]
diff --git a/Resources/views/snippet/add.html.twig b/Resources/views/snippet/add.html.twig
new file mode 100644
index 0000000..75bf356
--- /dev/null
+++ b/Resources/views/snippet/add.html.twig
@@ -0,0 +1,20 @@
+{% extends '@RapsysAir/body.html.twig' %}
+{% block content %}
+	<section id="form">
+		<h2>{{ section }}</h2>
+		{{ form_start(form) }}
+			<div>
+				{{ form_row(form.description) }}
+
+				{{ form_row(form.submit) }}
+
+				{% if form.delete is defined %}
+					{{ form_row(form.delete) }}
+				{% endif %}
+			</div>
+
+			{# render csrf token etc .#}
+			<footer style="display:none">{{ form_rest(form) }}</footer>
+		{{ form_end(form) }}
+	</section>
+{% endblock %}
diff --git a/Resources/views/snippet/edit.html.twig b/Resources/views/snippet/edit.html.twig
new file mode 100644
index 0000000..75bf356
--- /dev/null
+++ b/Resources/views/snippet/edit.html.twig
@@ -0,0 +1,20 @@
+{% extends '@RapsysAir/body.html.twig' %}
+{% block content %}
+	<section id="form">
+		<h2>{{ section }}</h2>
+		{{ form_start(form) }}
+			<div>
+				{{ form_row(form.description) }}
+
+				{{ form_row(form.submit) }}
+
+				{% if form.delete is defined %}
+					{{ form_row(form.delete) }}
+				{% endif %}
+			</div>
+
+			{# render csrf token etc .#}
+			<footer style="display:none">{{ form_rest(form) }}</footer>
+		{{ form_end(form) }}
+	</section>
+{% endblock %}