]> Raphaël G. Git Repositories - airbundle/blobdiff - Controller/DefaultController.php
Cleanup
[airbundle] / Controller / DefaultController.php
index 4f1fbf4a5c08130f9d83a79a202f7833d643c1c5..647baf844bb85af78275f564262555467072e5dd 100644 (file)
-<?php
+<?php declare(strict_types=1);
+
+/*
+ * This file is part of the Rapsys AirBundle package.
+ *
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
 
 namespace Rapsys\AirBundle\Controller;
 
 use Symfony\Bridge\Twig\Mime\TemplatedEmail;
-use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
-use Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait;
-use Symfony\Component\Asset\Packages;
 use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
 use Symfony\Component\Filesystem\Filesystem;
-use Symfony\Component\DependencyInjection\ContainerAwareTrait;
-use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\Form\FormError;
 use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\RequestStack;
 use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
-use Symfony\Component\Mailer\MailerInterface;
 use Symfony\Component\Mime\Address;
-use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
-use Symfony\Component\Routing\RouterInterface;
-use Symfony\Component\Translation\TranslatorInterface;
+use Symfony\Component\Security\Core\Exception\AccessDeniedException;
 
-use Rapsys\AirBundle\Entity\Application;
+use Rapsys\AirBundle\Entity\Dance;
 use Rapsys\AirBundle\Entity\Location;
 use Rapsys\AirBundle\Entity\Session;
-use Rapsys\AirBundle\Entity\Slot;
+use Rapsys\AirBundle\Entity\Snippet;
 use Rapsys\AirBundle\Entity\User;
-use Rapsys\AirBundle\Pdf\DisputePdf;
-use Rapsys\UserBundle\Utils\Slugger;
-
-
-class DefaultController {
-       use ControllerTrait {
-               //Rename render as _render
-               render as protected _render;
-       }
-
-       ///Config array
-       protected $config;
-
-       ///Context array
-       protected $context;
-
-       ///Router instance
-       protected $router;
-
-       ///Translator instance
-       protected $translator;
-
-       ///Packages instance
-       protected $asset;
-
-       ///RequestStack instance
-       protected $stack;
-
-       /**
-        * @var ContainerInterface
-        */
-       protected $container;
-
-       ///Facebook image array
-       protected $facebookImage = [];
+use Rapsys\AirBundle\Token\AnonymousToken;
 
+/**
+ * {@inheritdoc}
+ */
+class DefaultController extends AbstractController {
        /**
-        * Inject container and translator interface
+        * The about page
+        *
+        * Display the about informations
         *
-        * @param ContainerInterface $container The container instance
-        * @param RouterInterface $router The router instance
-        * @param RequestStack $stack The request stack
-        * @param TranslatorInterface $translator The translator instance
+        * @param Request $request The request instance
+        * @return Response The rendered view
         */
-       public function __construct(ContainerInterface $container, RouterInterface $router, RequestStack $stack, TranslatorInterface $translator, Packages $asset) {
-               //Retrieve config
-               $this->config = $container->getParameter($this->getAlias());
-
-               //Set the container
-               $this->container = $container;
-
-               //Set the router
-               $this->router = $router;
-
-               //Set the translator
-               $this->translator = $translator;
-
-               //Set the asset
-               $this->asset = $asset;
-
-               //Set the request stack
-               $this->stack = $stack;
-
-               //Set the context
-               $this->context = [
-                       'copy' => [
-                               'by' => $translator->trans($this->config['copy']['by']),
-                               'link' => $this->config['copy']['link'],
-                               'long' => $translator->trans($this->config['copy']['long']),
-                               'short' => $translator->trans($this->config['copy']['short']),
-                               'title' => $this->config['copy']['title']
-                       ],
-                       'page' => [
-                               'description' => null,
-                               'section' => null,
-                               'title' => null
-                       ],
-                       'site' => [
-                               'ico' => $this->config['site']['ico'],
-                               'logo' => $this->config['site']['logo'],
-                               'png' => $this->config['site']['png'],
-                               'svg' => $this->config['site']['svg'],
-                               'title' => $translator->trans($this->config['site']['title']),
-                               'url' => $router->generate($this->config['site']['url']),
-                       ],
-                       'canonical' => null,
-                       'alternates' => [],
-                       'ogps' => [
-                               'type' => 'article',
-                               'site_name' => $this->translator->trans($this->config['site']['title'])
-                       ],
-                       'facebooks' => [
-                               #'admins' => $this->config['facebook']['admins'],
-                               'app_id' => $this->config['facebook']['apps']
-                       ],
-                       'forms' => []
-               ];
-
-               //Get current request
-               $currentRequest = $stack->getCurrentRequest();
-
-               //Get current locale
-               #$currentLocale = $router->getContext()->getParameters()['_locale'];
-               $currentLocale = $currentRequest->getLocale();
+       public function about(Request $request): Response {
+               //Set page
+               $this->context['title']['page'] = $this->translator->trans('About');
 
-               //Set translator locale
-               //XXX: allow LocaleSubscriber on the fly locale change for first page
-               $this->translator->setLocale($currentLocale);
+               //Set description
+               $this->context['description'] = $this->translator->trans('Libre Air about');
 
-               //Iterate on locales excluding current one
-               foreach($this->config['locales'] as $locale) {
-                       //Set titles
-                       $titles = [];
+               //Set keywords
+               $this->context['keywords'] = [
+                       $this->translator->trans('about'),
+                       $this->translator->trans('Libre Air')
+               ];
 
-                       //Iterate on other locales
-                       foreach(array_diff($this->config['locales'], [$locale]) as $other) {
-                               $titles[$other] = $translator->trans($this->config['languages'][$locale], [], null, $other);
-                       }
+               //Render template
+               $response = $this->render('@RapsysAir/default/about.html.twig', $this->context);
+               $response->setEtag(md5($response->getContent()));
+               $response->setPublic();
+               $response->isNotModified($request);
 
-                       //Get context path
-                       $path = $router->getContext()->getPathInfo();
-
-                       //Retrieve route matching path
-                       $route = $router->match($path);
-
-                       //Get route name
-                       $name = $route['_route'];
-
-                       //Unset route name
-                       unset($route['_route']);
-
-                       //With current locale
-                       if ($locale == $currentLocale) {
-                               //Set locale locales context
-                               $this->context['canonical'] = $router->generate($name, ['_locale' => $locale]+$route, UrlGeneratorInterface::ABSOLUTE_URL);
-                       } else {
-                               //Set locale locales context
-                               $this->context['alternates'][] = [
-                                       'lang' => $locale,
-                                       'absolute' => $router->generate($name, ['_locale' => $locale]+$route, UrlGeneratorInterface::ABSOLUTE_URL),
-                                       'relative' => $router->generate($name, ['_locale' => $locale]+$route),
-                                       'title' => implode('/', $titles),
-                                       'translated' => $translator->trans($this->config['languages'][$locale], [], null, $locale)
-                               ];
-                       }
-               }
+               //Return response
+               return $response;
        }
 
        /**
         * The contact page
         *
-        * @desc Send a contact mail to configured contact
+        * Send a contact mail to configured contact
         *
         * @param Request $request The request instance
-        * @param MailerInterface $mailer The mailer instance
         *
         * @return Response The rendered view or redirection
         */
-       public function contact(Request $request, MailerInterface $mailer): Response {
+       public function contact(Request $request): Response {
                //Set page
-               $this->context['page']['title'] = $this->translator->trans('Contact');
+               $this->context['title']['page'] = $this->translator->trans('Contact');
 
                //Set description
-               $this->context['page']['description'] = $this->translator->trans('Contact Libre Air');
+               $this->context['description'] = $this->translator->trans('Contact Libre Air');
 
                //Set keywords
                $this->context['keywords'] = [
@@ -200,10 +89,22 @@ class DefaultController {
                        $this->translator->trans('calendar')
                ];
 
+               //Set data
+               $data = [];
+
+               //With user
+               if ($user = $this->security->getUser()) {
+                       //Set data
+                       $data = [
+                               'name' => $user->getRecipientName(),
+                               'mail' => $user->getMail()
+                       ];
+               }
+
                //Create the form according to the FormType created previously.
                //And give the proper parameters
-               $form = $this->createForm('Rapsys\AirBundle\Form\ContactType', null, [
-                       'action' => $this->generateUrl('rapsys_air_contact'),
+               $form = $this->factory->create('Rapsys\AirBundle\Form\ContactType', $data, [
+                       'action' => $this->generateUrl('rapsysair_contact'),
                        'method' => 'POST'
                ]);
 
@@ -211,7 +112,7 @@ class DefaultController {
                        // Refill the fields in case the form is not valid.
                        $form->handleRequest($request);
 
-                       if ($form->isValid()) {
+                       if ($form->isSubmitted() && $form->isValid()) {
                                //Get data
                                $data = $form->getData();
 
@@ -220,8 +121,7 @@ class DefaultController {
                                        //Set sender
                                        ->from(new Address($data['mail'], $data['name']))
                                        //Set recipient
-                                       //XXX: remove the debug set in vendor/symfony/mime/Address.php +46
-                                       ->to(new Address($this->config['contact']['mail'], $this->config['contact']['name']))
+                                       ->to(new Address($this->context['contact']['address'], $this->context['contact']['name']))
                                        //Set subject
                                        ->subject($data['subject'])
 
@@ -241,7 +141,7 @@ class DefaultController {
                                //XXX: mail delivery may silently fail
                                try {
                                        //Send message
-                                       $mailer->send($message);
+                                       $this->mailer->send($message);
 
                                        //Redirect on the same route with sent=1 to cleanup form
                                        return $this->redirectToRoute($request->get('_route'), ['sent' => 1]+$request->get('_route_params'));
@@ -249,10 +149,10 @@ class DefaultController {
                                } catch(TransportExceptionInterface $e) {
                                        if ($message = $e->getMessage()) {
                                                //Add error message mail unreachable
-                                               $form->get('mail')->addError(new FormError($this->translator->trans('Unable to contact: %mail%: %message%', ['%mail%' => $this->config['contact']['mail'], '%message%' => $this->translator->trans($message)])));
+                                               $form->get('mail')->addError(new FormError($this->translator->trans('Unable to contact: %mail%: %message%', ['%mail%' => $this->context['contact']['address'], '%message%' => $this->translator->trans($message)])));
                                        } else {
                                                //Add error message mail unreachable
-                                               $form->get('mail')->addError(new FormError($this->translator->trans('Unable to contact: %mail%', ['%mail%' => $this->config['contact']['mail']])));
+                                               $form->get('mail')->addError(new FormError($this->translator->trans('Unable to contact: %mail%', ['%mail%' => $this->context['contact']['address']])));
                                        }
                                }
                        }
@@ -263,224 +163,133 @@ class DefaultController {
        }
 
        /**
-        * The dispute page
+        * The index page
         *
-        * @desc Generate a dispute document
+        * Display session calendar
         *
         * @param Request $request The request instance
-        * @param MailerInterface $mailer The mailer instance
-        *
-        * @return Response The rendered view or redirection
+        * @return Response The rendered view
         */
-       public function dispute(Request $request, MailerInterface $mailer): Response {
-               //Prevent non-guest to access here
-               $this->denyAccessUnlessGranted('ROLE_USER', null, $this->translator->trans('Unable to access this page without role %role%!', ['%role%' => $this->translator->trans('User')]));
-               //Set section
-               $section = $this->translator->trans('Dispute');
+       public function index(Request $request): Response {
+               //Add cities
+               $this->context['cities'] = $this->doctrine->getRepository(Location::class)->findCitiesAsArray($this->period);
 
-               //Set description
-               $this->context['description'] = $this->translator->trans('Libre Air dispute');
+               //Add calendar
+               $this->context['calendar'] = $this->doctrine->getRepository(Session::class)->findAllByPeriodAsCalendarArray($this->period, !$this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED'));
 
-               //Set keywords
-               $this->context['keywords'] = [
-                       $this->translator->trans('dispute'),
-                       $this->translator->trans('Libre Air'),
-                       $this->translator->trans('outdoor'),
-                       $this->translator->trans('Argentine Tango'),
-                       $this->translator->trans('calendar')
-               ];
+               //Add dances
+               $this->context['dances'] = $this->doctrine->getRepository(Dance::class)->findNamesAsArray();
 
-               //Set title
-               $title = $this->translator->trans($this->config['site']['title']).' - '.$section;
+               //Set modified
+               $this->modified = max(array_map(function ($v) { return $v['modified']; }, array_merge($this->context['calendar'], $this->context['cities'], $this->context['dances'])));
 
-               //Create the form according to the FormType created previously.
-               //And give the proper parameters
-               $form = $this->createForm('Rapsys\AirBundle\Form\DisputeType', ['court' => 'Paris', 'abstract' => 'Pour constater cette prétendue infraction, les agents verbalisateurs ont pénétré dans un jardin privatif, sans visibilité depuis la voie publique, situé derrière un batiment privé, pour ce faire ils ont franchi au moins un grillage de chantier ou des potteaux métalliques séparant le terrain privé de la voie publique de l\'autre côté du batiment.'], [
-                       'action' => $this->generateUrl('rapsys_air_dispute'),
-                       'method' => 'POST'
-               ]);
+               //Create response
+               $response = new Response();
 
-               if ($request->isMethod('POST')) {
-                       // Refill the fields in case the form is not valid.
-                       $form->handleRequest($request);
+               //With logged user
+               if ($this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
+                       //Set last modified
+                       $response->setLastModified(new \DateTime('-1 year'));
 
-                       if ($form->isValid()) {
-                               //Get data
-                               $data = $form->getData();
+                       //Set as private
+                       $response->setPrivate();
+               //Without logged user
+               } else {
+                       //Set etag
+                       //XXX: only for public to force revalidation by last modified
+                       $response->setEtag(md5(serialize(array_merge($this->context['calendar'], $this->context['cities'], $this->context['dances']))));
 
-                               //Gathering offense
-                               if (!empty($data['offense']) && $data['offense'] == 'gathering') {
-                                       //Add gathering
-                                       $output = DisputePdf::genGathering($data['court'], $data['notice'], $data['agent'], $data['service'], $data['abstract'], $this->translator->trans($this->getUser()->getCivility()->getTitle()), $this->getUser()->getForename(), $this->getUser()->getSurname());
-                               //Traffic offense
-                               } elseif (!empty($data['offense'] && $data['offense'] == 'traffic')) {
-                                       //Add traffic
-                                       $output = DisputePdf::genTraffic($data['court'], $data['notice'], $data['agent'], $data['service'], $data['abstract'], $this->translator->trans($this->getUser()->getCivility()->getTitle()), $this->getUser()->getForename(), $this->getUser()->getSurname());
-                               //Unsupported offense
-                               } else {
-                                       header('Content-Type: text/plain');
-                                       die('TODO');
-                                       exit;
-                               }
+                       //Set last modified
+                       $response->setLastModified($this->modified);
 
-                               //Send common headers
-                               header('Content-Type: application/pdf');
-
-                               //Send remaining headers
-                               header('Cache-Control: private, max-age=0, must-revalidate');
-                               header('Pragma: public');
-
-                               //Send content-length
-                               header('Content-Length: '.strlen($output));
-
-                               //Display the pdf
-                               echo $output;
-
-                               //Die for now
-                               exit;
-
-#                              //Create message
-#                              $message = (new TemplatedEmail())
-#                                      //Set sender
-#                                      ->from(new Address($data['mail'], $data['name']))
-#                                      //Set recipient
-#                                      //XXX: remove the debug set in vendor/symfony/mime/Address.php +46
-#                                      ->to(new Address($this->config['contact']['mail'], $this->config['contact']['name']))
-#                                      //Set subject
-#                                      ->subject($data['subject'])
-#
-#                                      //Set path to twig templates
-#                                      ->htmlTemplate('@RapsysAir/mail/contact.html.twig')
-#                                      ->textTemplate('@RapsysAir/mail/contact.text.twig')
-#
-#                                      //Set context
-#                                      ->context(
-#                                              [
-#                                                      'subject' => $data['subject'],
-#                                                      'message' => strip_tags($data['message']),
-#                                              ]+$this->context
-#                                      );
-#
-#                              //Try sending message
-#                              //XXX: mail delivery may silently fail
-#                              try {
-#                                      //Send message
-#                                      $mailer->send($message);
-#
-#                                      //Redirect on the same route with sent=1 to cleanup form
-#                                      return $this->redirectToRoute($request->get('_route'), ['sent' => 1]+$request->get('_route_params'));
-#                              //Catch obvious transport exception
-#                              } catch(TransportExceptionInterface $e) {
-#                                      if ($message = $e->getMessage()) {
-#                                              //Add error message mail unreachable
-#                                              $form->get('mail')->addError(new FormError($this->translator->trans('Unable to contact: %mail%: %message%', ['%mail%' => $this->config['contact']['mail'], '%message%' => $this->translator->trans($message)])));
-#                                      } else {
-#                                              //Add error message mail unreachable
-#                                              $form->get('mail')->addError(new FormError($this->translator->trans('Unable to contact: %mail%', ['%mail%' => $this->config['contact']['mail']])));
-#                                      }
-#                              }
+                       //Set as public
+                       $response->setPublic();
+
+                       //Without role and modification
+                       if ($response->isNotModified($request)) {
+                               //Return 304 response
+                               return $response;
                        }
                }
 
-               //Render template
-               return $this->render('@RapsysAir/default/dispute.html.twig', ['title' => $title, 'section' => $section, 'form' => $form->createView(), 'sent' => $request->query->get('sent', 0)]+$this->context);
-       }
-
-       /**
-        * The index page
-        *
-        * @desc Display all granted sessions with an application or login form
-        *
-        * @param Request $request The request instance
-        *
-        * @return Response The rendered view
-        */
-       public function index(Request $request): Response {
-               //Fetch doctrine
-               $doctrine = $this->getDoctrine();
+               //With cities
+               if (!empty($this->context['cities'])) {
+                       //Set locations
+                       $locations = [];
+
+                       //Iterate on each cities
+                       foreach($this->context['cities'] as $city) {
+                               //Iterate on each locations
+                               foreach($city['locations'] as $location) {
+                                       //Add location
+                                       $locations[$location['id']] = $location;
+                               }
+                       }
 
-               //Set page
-               $this->context['page']['title'] = $this->translator->trans('Argentine Tango in Paris');
+                       //Add multi
+                       $this->context['multimap'] = $this->map->getMultiMap($this->translator->trans('Libre Air cities sector map'), $this->modified->getTimestamp(), $locations);
 
-               //Set description
-               $this->context['page']['description'] = $this->translator->trans('Outdoor Argentine Tango session calendar in Paris');
+                       //Set cities
+                       $cities = array_map(function ($v) { return $v['in']; }, $this->context['cities']);
 
-               //Set keywords
-               $this->context['keywords'] = [
-                       $this->translator->trans('Argentine Tango'),
-                       $this->translator->trans('Paris'),
-                       $this->translator->trans('outdoor'),
-                       $this->translator->trans('calendar'),
-                       $this->translator->trans('Libre Air')
-               ];
+                       //Set dances
+                       $dances = array_map(function ($v) { return $v['name']; }, $this->context['dances']);
+               } else {
+                       //Set cities
+                       $cities = [];
 
-               //Set type
-               //XXX: only valid for home page
-               $this->context['ogps']['type'] = 'website';
-
-               //Set facebook image
-               $this->facebookImage = [
-                       //XXX: format facebook/<controller>/<action>.<locale>.jpeg
-                       'destination' => 'facebook/default/index.'.$request->getLocale().'.jpeg',
-                       'texts' => [
-                               $this->context['site']['title'] => [
-                                       'font' => 'irishgrover',
-                                       'size' => 110
-                               ],
-                               $this->context['page']['title'] => [
-                                       'align' => 'left'
-                               ]/*,
-                               $this->context['canonical'] => [
-                                       'align' => 'right',
-                                       'font' => 'labelleaurore',
-                                       'size' => 75
-                               ]*/
-                       ]
-               ];
+                       //Set dances
+                       $dances = [];
+               }
 
-               //Compute period
-               $period = new \DatePeriod(
-                       //Start from first monday of week
-                       new \DateTime('Monday this week'),
-                       //Iterate on each day
-                       new \DateInterval('P1D'),
-                       //End with next sunday and 4 weeks
-                       new \DateTime(
-                               $this->isGranted('IS_AUTHENTICATED_REMEMBERED')?'Monday this week + 3 week':'Monday this week + 2 week'
+               //Set keywords
+               //TODO: use splice instead of that shit !!!
+               //TODO: handle smartly indoor and outdoor !!!
+               $this->context['keywords'] = array_values(
+                       array_merge(
+                               $dances,
+                               $cities,
+                               [
+                                       $this->translator->trans('indoor'),
+                                       $this->translator->trans('outdoor'),
+                                       $this->translator->trans('calendar'),
+                                       $this->translator->trans('Libre Air')
+                               ]
                        )
                );
 
-               //Fetch calendar
-               $calendar = $doctrine->getRepository(Session::class)->fetchCalendarByDatePeriod($this->translator, $period, null, $request->get('session'), !$this->isGranted('IS_AUTHENTICATED_REMEMBERED'));
+               //Get textual cities
+               $cities = implode($this->translator->trans(' and '), array_filter(array_merge([implode(', ', array_slice($cities, 0, -1))], array_slice($cities, -1)), 'strlen'));
 
-               //Fetch locations
-               //XXX: we want to display all active locations anyway
-               $locations = $doctrine->getRepository(Location::class)->findTranslatedSortedByPeriod($this->translator, $period);
+               //Get textual dances
+               $dances = implode($this->translator->trans(' and '), array_filter(array_merge([implode(', ', array_slice($dances, 0, -1))], array_slice($dances, -1)), 'strlen'));
 
-               //Render the view
-               return $this->render('@RapsysAir/default/index.html.twig', ['calendar' => $calendar, 'locations' => $locations]+$this->context);
+               //Set title
+               $this->context['title']['page'] = $this->translator->trans('%dances% %cities%', ['%dances%' => $dances, '%cities%' => $cities]);
 
-               //Set Cache-Control must-revalidate directive
-               //TODO: add a javascript forced refresh after 1h ? or header refresh ?
-               #$response->setPublic(true);
-               #$response->setMaxAge(300);
-               #$response->mustRevalidate();
-               ##$response->setCache(['public' => true, 'max_age' => 300]);
+               //Set description
+               //TODO: handle french translation when city start with a A, change à in en !
+               $this->context['description'] = $this->translator->trans('%dances% indoor and outdoor calendar %cities%', ['%dances%' => $dances, '%cities%' => $cities]);
 
-               //Return the response
-               #return $response;
+               //Set facebook type
+               //XXX: only valid for home page
+               $this->context['facebook']['metas']['og:type'] = 'website';
+
+               //Render the view
+               return $this->render('@RapsysAir/default/index.html.twig', $this->context, $response);
        }
 
        /**
         * The organizer regulation page
         *
-        * @desc Display the organizer regulation policy
+        * Display the organizer regulation policy
         *
+        * @param Request $request The request instance
         * @return Response The rendered view
         */
-       public function organizerRegulation(): Response {
-               //Set section
-               $section = $this->translator->trans('Organizer regulation');
+       public function organizerRegulation(Request $request): Response {
+               //Set page
+               $this->context['title']['page'] = $this->translator->trans('Organizer regulation');
 
                //Set description
                $this->context['description'] = $this->translator->trans('Libre Air organizer regulation');
@@ -491,23 +300,29 @@ class DefaultController {
                        $this->translator->trans('Libre Air')
                ];
 
-               //Set title
-               $title = $this->translator->trans($this->config['site']['title']).' - '.$section;
-
                //Render template
-               return $this->render('@RapsysAir/default/organizer_regulation.html.twig', ['title' => $title, 'section' => $section]+$this->context);
+               $response = $this->render('@RapsysAir/default/organizer_regulation.html.twig', $this->context);
+
+               //Set as cachable
+               $response->setEtag(md5($response->getContent()));
+               $response->setPublic();
+               $response->isNotModified($request);
+
+               //Return response
+               return $response;
        }
 
        /**
         * The terms of service page
         *
-        * @desc Display the terms of service policy
+        * Display the terms of service policy
         *
+        * @param Request $request The request instance
         * @return Response The rendered view
         */
-       public function termsOfService(): Response {
-               //Set section
-               $section = $this->translator->trans('Terms of service');
+       public function termsOfService(Request $request): Response {
+               //Set page
+               $this->context['title']['page'] = $this->translator->trans('Terms of service');
 
                //Set description
                $this->context['description'] = $this->translator->trans('Libre Air terms of service');
@@ -518,23 +333,29 @@ class DefaultController {
                        $this->translator->trans('Libre Air')
                ];
 
-               //Set title
-               $title = $this->translator->trans($this->config['site']['title']).' - '.$section;
-
                //Render template
-               return $this->render('@RapsysAir/default/terms_of_service.html.twig', ['title' => $title, 'section' => $section]+$this->context);
+               $response = $this->render('@RapsysAir/default/terms_of_service.html.twig', $this->context);
+
+               //Set as cachable
+               $response->setEtag(md5($response->getContent()));
+               $response->setPublic();
+               $response->isNotModified($request);
+
+               //Return response
+               return $response;
        }
 
        /**
         * The frequently asked questions page
         *
-        * @desc Display the frequently asked questions
+        * Display the frequently asked questions
         *
+        * @param Request $request The request instance
         * @return Response The rendered view
         */
-       public function frequentlyAskedQuestions(): Response {
-               //Set section
-               $section = $this->translator->trans('Frequently asked questions');
+       public function frequentlyAskedQuestions(Request $request): Response {
+               //Set page
+               $this->context['title']['page'] = $this->translator->trans('Frequently asked questions');
 
                //Set description
                $this->context['description'] = $this->translator->trans('Libre Air frequently asked questions');
@@ -546,401 +367,407 @@ class DefaultController {
                        $this->translator->trans('Libre Air')
                ];
 
-               //Set title
-               $title = $this->translator->trans($this->config['site']['title']).' - '.$section;
-
                //Render template
-               return $this->render('@RapsysAir/default/frequently_asked_questions.html.twig', ['title' => $title, 'section' => $section]+$this->context);
+               $response = $this->render('@RapsysAir/default/frequently_asked_questions.html.twig', $this->context);
+
+               //Set as cachable
+               $response->setEtag(md5($response->getContent()));
+               $response->setPublic();
+               $response->isNotModified($request);
+
+               //Return response
+               return $response;
        }
 
        /**
-        * Return the bundle alias
+        * List all users
+        *
+        * Display all user with a group listed as users
+        *
+        * @param Request $request The request instance
         *
-        * {@inheritdoc}
+        * @return Response The rendered view
         */
-       public function getAlias(): string {
-               return 'rapsys_air';
+       public function userIndex(Request $request): Response {
+               //With admin role
+               if ($this->checker->isGranted('ROLE_ADMIN')) {
+                       //Set title
+                       $this->context['title']['page'] = $this->translator->trans('Libre Air user list');
+
+                       //Set section
+                       $this->context['title']['section'] = $this->translator->trans('User');
+
+                       //Set description
+                       $this->context['description'] = $this->translator->trans('Lists Libre air users');
+               //Without admin role
+               } else {
+                       //Set title
+                       $this->context['title']['page'] = $this->translator->trans('Libre Air organizer list');
+
+                       //Set section
+                       $this->context['title']['section'] = $this->translator->trans('Organizer');
+
+                       //Set description
+                       $this->context['description'] = $this->translator->trans('Lists Libre air organizers');
+               }
+
+               //Set keywords
+               $this->context['keywords'] = [
+                       $this->translator->trans('users'),
+                       $this->translator->trans('user list'),
+                       $this->translator->trans('listing'),
+                       $this->translator->trans('Libre Air')
+               ];
+
+               //Fetch users
+               $users = $this->doctrine->getRepository(User::class)->findIndexByGroupId();
+
+               //With admin role
+               if ($this->checker->isGranted('ROLE_ADMIN')) {
+                       //Display all users
+                       $this->context['groups'] = $users;
+               //Without admin role
+               } else {
+                       //Only display senior organizers
+                       $this->context['users'] = $users[$this->translator->trans('Senior')];
+               }
+
+               //Render the view
+               return $this->render('@RapsysAir/user/index.html.twig', $this->context);
        }
 
        /**
-        * Return the facebook image
+        * List all sessions for the user
         *
-        * @desc Generate image in jpeg format or load it from cache
+        * Display all sessions for the user with an application or login form
         *
-        * @return array The image array
+        * @param Request $request The request instance
+        * @param int $id The user id
+        *
+        * @return Response The rendered view
         */
-       protected function getFacebookImage(): array {
-               //Set texts
-               $texts = $this->facebookImage['texts'] ?? [];
-
-               //Set default source
-               $source = $this->facebookImage['source'] ?? 'png/facebook.png';
-
-               //Set default source
-               $updated = $this->facebookImage['updated'] ?? strtotime('last week');
-
-               //Set source path
-               $src = $this->config['path']['public'].'/'.$source;
-
-               //Set cache path
-               //XXX: remove extension and store as png anyway
-               $cache = $this->config['path']['cache'].'/facebook/'.substr($source, 0, strrpos($source, '.')).'.'.$this->config['facebook']['width'].'x'.$this->config['facebook']['height'].'.png';
-
-               //Set destination path
-               $dest = $this->config['path']['public'].'/'.$this->facebookImage['destination'];
-
-               //Set asset
-               $asset = '@RapsysAir/'.$this->facebookImage['destination'];
-
-               //With up to date generated image
-               if (
-                       is_file($dest) &&
-                       ($stat = stat($dest)) &&
-                       $stat['mtime'] >= $updated
-               ) {
-                       //Get image size
-                       //TODO: see if it works every time
-                       list ($width, $height) = getimagesize($dest);
-
-                       //Return image data
-                       return [
-                               //TODO: see if it works every time
-                               'image' => $this->stack->getCurrentRequest()->getUriForPath($this->asset->getUrl($asset), true),#.'?fbrefresh='.$stat['mtime'],
-                               'image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
-                               'image:height' => $height,
-                               'image:width' => $width
-                       ];
-               //With image candidate
-               } elseif (is_file($src)) {
-                       //Create image object
-                       $image = new \Imagick();
-
-                       //With cache image
-                       if (is_file($cache)) {
-                               //Read image
-                               $image->readImage($cache);
-                       //Without we generate it
-                       } else {
-                               //Check target directory
-                               if (!is_dir($dir = dirname($cache))) {
-                                       //Create filesystem object
-                                       $filesystem = new Filesystem();
-
-                                       try {
-                                               //Create dir
-                                               //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
-                                               $filesystem->mkdir($dir, 0775);
-                                       } catch (IOExceptionInterface $e) {
-                                               //Throw error
-                                               throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
-                                       }
-                               }
-
-                               //Read image
-                               $image->readImage($src);
-
-                               //Crop using aspect ratio
-                               //XXX: for better result upload image directly in aspect ratio :)
-                               $image->cropThumbnailImage($this->config['facebook']['width'], $this->config['facebook']['height']);
+       public function userView(Request $request, int $id, ?string $user): Response {
+               //Get user
+               if (empty($this->context['user'] = $this->doctrine->getRepository(User::class)->findOneByIdAsArray($id, $this->locale))) {
+                       //Throw not found
+                       throw new NotFoundHttpException($this->translator->trans('Unable to find user: %id%', ['%id%' => $id]));
+               }
 
-                               //Strip image exif data and properties
-                               $image->stripImage();
+               //Create token
+               $token = new AnonymousToken($this->context['user']['roles']);
 
-                               //Save cache image
-                               if (!$image->writeImage($cache)) {
-                                       //Throw error
-                                       throw new \Exception(sprintf('Unable to write image "%s"', $cache));
-                               }
-                       }
-                       //Check target directory
-                       if (!is_dir($dir = dirname($dest))) {
-                               //Create filesystem object
-                               $filesystem = new Filesystem();
+               //Prevent access when not admin, user is not guest and not currently logged user
+               if (!($isAdmin = $this->checker->isGranted('ROLE_ADMIN')) && !($isGuest = $this->decision->decide($token, ['ROLE_GUEST']))) {
+                       //Throw access denied
+                       throw new AccessDeniedException($this->translator->trans('Unable to access user: %id%', ['%id%' => $id]));
+               }
 
-                               try {
-                                       //Create dir
-                                       //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
-                                       $filesystem->mkdir($dir, 0775);
-                               } catch (IOExceptionInterface $e) {
-                                       //Throw error
-                                       throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
-                               }
-                       }
+               //With invalid user slug
+               if ($this->context['user']['slug'] !== $user) {
+                       //Redirect to cleaned url
+                       return $this->redirectToRoute('rapsysair_user_view', ['id' => $id, 'user' => $this->context['user']['slug']]);
+               }
 
-                       //Get image width
-                       $width = $image->getImageWidth();
+               //Fetch calendar
+               $this->context['calendar'] = $this->doctrine->getRepository(Session::class)->findAllByPeriodAsCalendarArray($this->period, !$this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED'), null, null, $id);
 
-                       //Get image height
-                       $height = $image->getImageHeight();
+               //Get locations at less than 2 km
+               $this->context['locations'] = $this->doctrine->getRepository(Location::class)->findAllByUserIdAsArray($id, $this->period, 2);
 
-                       //Create draw
-                       $draw = new \ImagickDraw();
+               //Set ats
+               $ats = [];
 
-                       //Set stroke antialias
-                       $draw->setStrokeAntialias(true);
+               //Set dances
+               $dances = [];
 
-                       //Set text antialias
-                       $draw->setTextAntialias(true);
+               //Set indoors
+               $indoors = [];
 
-                       //Set stroke width
-                       //TODO: configure that ?
-                       $draw->setStrokeWidth(15);
+               //Set ins
+               $ins = [];
 
-                       //Set font aliases
-                       $fonts = [
-                               'irishgrover' => $this->config['path']['public'].'/ttf/irishgrover.v10.ttf',
-                               'droidsans' => $this->config['path']['public'].'/ttf/droidsans.regular.ttf',
-                               'dejavusans' => $this->config['path']['public'].'/ttf/dejavusans.2.37.ttf',
-                               'labelleaurore' => $this->config['path']['public'].'/ttf/labelleaurore.v10.ttf'
-                       ];
+               //Set insides
+               $insides = [];
 
-                       //Set align aliases
-                       $aligns = [
-                               'left' => \Imagick::ALIGN_LEFT,
-                               'center' => \Imagick::ALIGN_CENTER,
-                               'right' => \Imagick::ALIGN_RIGHT
-                       ];
+               //Set locations
+               $locations = [];
 
-                       //Set default font
-                       $defaultFont = 'dejavusans';
+               //Set types
+               $types = [];
 
-                       //Set default align
-                       $defaultAlign = 'center';
+               //Iterate on each calendar
+               foreach($this->context['calendar'] as $date => $calendar) {
+                       //Iterate on each session
+                       foreach($calendar['sessions'] as $sessionId => $session) {
+                               //Add dance
+                               $dances[$session['application']['dance']['name']] = $session['application']['dance']['name'];
 
-                       //Set default size
-                       $defaultSize = 60;
+                               //Add types
+                               $types[$session['application']['dance']['type']] = lcfirst($session['application']['dance']['type']);
 
-                       //Set default stroke
-                       $defaultStroke = '#00c3f9';
+                               //Add indoors
+                               $indoors[$session['location']['indoor']?'indoor':'outdoor'] = $this->translator->trans($session['location']['indoor']?'indoor':'outdoor');
 
-                       //Set default fill
-                       $defaultFill = 'white';
+                               //Add insides
+                               $insides[$session['location']['indoor']?'inside':'outside'] = $this->translator->trans($session['location']['indoor']?'inside':'outside');
 
-                       //Init counter
-                       $i = 1;
+                               //Add ats
+                               $ats[$session['location']['id']] = $session['location']['at'];
 
-                       //Set text count
-                       $count = count($texts);
+                               //Add ins
+                               $ins[$session['location']['id']] = $session['location']['in'];
 
-                       //Draw each text stroke
-                       foreach($texts as $text => $data) {
-                               //Set font
-                               $draw->setFont($fonts[$data['font']??$defaultFont]);
+                               //Session with application user id
+                               if (!empty($session['application']['user']['id']) && $session['application']['user']['id'] == $id) {
+                                       //Add location
+                                       $locations[$session['location']['id']] = $session['location'];
+                               }
+                       }
+               }
 
-                               //Set font size
-                               $draw->setFontSize($data['size']??$defaultSize);
+               //Set modified
+               //XXX: dance modified is already computed inside calendar modified
+               $this->modified = max(array_merge([$this->context['user']['modified']], array_map(function ($v) { return $v['modified']; }, array_merge($this->context['calendar'], $this->context['locations']))));
 
-                               //Set text alignment
-                               $draw->setTextAlignment($align = ($aligns[$data['align']??$defaultAlign]));
+               //Create response
+               $response = new Response();
 
-                               //Get font metrics
-                               $metrics = $image->queryFontMetrics($draw, $text);
+               //With logged user
+               if ($this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
+                       //Set last modified
+                       $response->setLastModified(new \DateTime('-1 year'));
 
-                               //Without y
-                               if (empty($data['y'])) {
-                                       //Position verticaly each text evenly
-                                       $texts[$text]['y'] = $data['y'] = (($height + 100) / (count($texts) + 1) * $i) - 50;
-                               }
+                       //Set as private
+                       $response->setPrivate();
+               //Without logged user
+               } else {
+                       //Set etag
+                       //XXX: only for public to force revalidation by last modified
+                       $response->setEtag(md5(serialize(array_merge($this->context['user'], $this->context['calendar'], $this->context['locations']))));
 
-                               //Without x
-                               if (empty($data['x'])) {
-                                       if ($align == \Imagick::ALIGN_CENTER) {
-                                               $texts[$text]['x'] = $data['x'] = $width/2;
-                                       } elseif ($align == \Imagick::ALIGN_LEFT) {
-                                               $texts[$text]['x'] = $data['x'] = 50;
-                                       } elseif ($align == \Imagick::ALIGN_RIGHT) {
-                                               $texts[$text]['x'] = $data['x'] = $width - 50;
-                                       }
-                               }
+                       //Set last modified
+                       $response->setLastModified($this->modified);
 
-                               //Center verticaly
-                               //XXX: add ascender part then center it back by half of textHeight
-                               //TODO: maybe add a boundingbox ???
-                               $texts[$text]['y'] = $data['y'] += $metrics['ascender'] - $metrics['textHeight']/2;
+                       //Set as public
+                       $response->setPublic();
 
-                               //Set stroke color
-                               $draw->setStrokeColor(new \ImagickPixel($data['stroke']??$defaultStroke));
+                       //Without role and modification
+                       if ($response->isNotModified($request)) {
+                               //Return 304 response
+                               return $response;
+                       }
+               }
 
-                               //Set fill color
-                               $draw->setFillColor(new \ImagickPixel($data['stroke']??$defaultStroke));
+               //Add multi map
+               $this->context['multimap'] = $this->map->getMultiMap($this->context['user']['multimap'], $this->modified->getTimestamp(), $this->context['locations']);
 
-                               //Add annotation
-                               $draw->annotation($data['x'], $data['y'], $text);
+               //Set keywords
+               $this->context['keywords'] = [
+                       $this->context['user']['pseudonym'],
+                       $this->translator->trans('calendar'),
+                       $this->translator->trans('Libre Air')
+               ];
 
-                               //Increase counter
-                               $i++;
-                       }
+               //Set cities
+               $cities = array_unique(array_map(function ($v) { return $v['city']; }, $locations));
 
-                       //Create stroke object
-                       $stroke = new \Imagick();
+               //Set titles
+               $titles = array_map(function ($v) { return $v['title']; }, $locations);
 
-                       //Add new image
-                       $stroke->newImage($width, $height, new \ImagickPixel('transparent'));
+               //Insert dances in keywords
+               array_splice($this->context['keywords'], 1, 0, array_merge($types, $dances, $indoors, $insides, $titles, $cities));
 
-                       //Draw on image
-                       $stroke->drawImage($draw);
+               //Deduplicate ins
+               $ins = array_unique($ins);
 
-                       //Blur image
-                       //XXX: blur the stroke canvas only
-                       $stroke->blurImage(5,3);
+               //Get textual dances
+               $dances = implode($this->translator->trans(' and '), array_filter(array_merge([implode(', ', array_slice($dances, 0, -1))], array_slice($dances, -1)), 'strlen'));
 
-                       //Set opacity to 0.5
-                       //XXX: see https://www.php.net/manual/en/image.evaluateimage.php
-                       $stroke->evaluateImage(\Imagick::EVALUATE_DIVIDE, 1.5, \Imagick::CHANNEL_ALPHA);
+               //Get textual types
+               $types = implode($this->translator->trans(' and '), array_filter(array_merge([implode(', ', array_slice($types, 0, -1))], array_slice($types, -1)), 'strlen'));
 
-                       //Compose image
-                       $image->compositeImage($stroke, \Imagick::COMPOSITE_OVER, 0, 0);
+               //Get textual indoors
+               $indoors = implode($this->translator->trans(' and '), array_filter(array_merge([implode(', ', array_slice($indoors, 0, -1))], array_slice($indoors, -1)), 'strlen'));
 
-                       //Clear stroke
-                       $stroke->clear();
+               //Get textual ats
+               $ats = implode($this->translator->trans(' and '), array_filter(array_merge([implode(', ', array_slice($ats, 0, -1))], array_slice($ats, -1)), 'strlen'));
 
-                       //Destroy stroke
-                       unset($stroke);
+               //Get textual ins
+               $ins = implode($this->translator->trans(' and '), array_filter(array_merge([implode(', ', array_slice($ins, 0, -1))], array_slice($ins, -1)), 'strlen'));
 
-                       //Clear draw
-                       $draw->clear();
+               //Set title
+               $this->context['title']['page'] = $this->translator->trans('%pseudonym% organizer', ['%pseudonym%' => $this->context['user']['pseudonym']]);
 
-                       //Set text antialias
-                       $draw->setTextAntialias(true);
+               //Set section
+               $this->context['title']['section'] = $this->translator->trans('User');
+
+               //With locations
+               if (!empty($locations)) {
+                       //Set description
+                       $this->context['description'] = ucfirst($this->translator->trans('%dances% %types% %indoors% calendar %ats% %ins% %pseudonym%', ['%dances%' => $dances, '%types%' => $types, '%indoors%' => $indoors, '%ats%' => $ats, '%ins%' => $ins, '%pseudonym%' => $this->translator->trans('by %pseudonym%', ['%pseudonym%' => $this->context['user']['pseudonym']])]));
+               //Without locations
+               } else {
+                       //Set description
+                       $this->context['description'] = $this->translator->trans('%pseudonym% calendar', ['%pseudonym%' => $this->context['user']['pseudonym']]);
+               }
 
-                       //Draw each text
-                       foreach($texts as $text => $data) {
-                               //Set font
-                               $draw->setFont($fonts[$data['font']??$defaultFont]);
+               //Set user description
+               $this->context['locations_description'] = $this->translator->trans('Libre Air %pseudonym% location list', ['%pseudonym%' => $this->translator->trans('by %pseudonym%', ['%pseudonym%' => $this->context['user']['pseudonym']])]);
 
-                               //Set font size
-                               $draw->setFontSize($data['size']??$defaultSize);
+               //Set alternates
+               $this->context['alternates'] += $this->context['user']['alternates'];
 
-                               //Set text alignment
-                               $draw->setTextAlignment($aligns[$data['align']??$defaultAlign]);
+               //Create snippet forms for role_guest
+               //TODO: optimize this call
+               if ($isAdmin || $isGuest && $this->security->getUser() && $this->context['user']['id'] == $this->security->getUser()->getId()) {
+                       //Fetch all user snippet
+                       $snippets = $this->doctrine->getRepository(Snippet::class)->findByUserIdLocaleIndexByLocationId($id, $this->locale);
 
-                               //Set fill color
-                               $draw->setFillColor(new \ImagickPixel($data['fill']??$defaultFill));
+                       //Get user
+                       $user = $this->doctrine->getRepository(User::class)->findOneById($id);
 
-                               //Add annotation
-                               $draw->annotation($data['x'], $data['y'], $text);
-                       }
+                       //Iterate on locations
+                       foreach($this->context['locations'] as $locationId => $location) {
+                               //With existing snippet
+                               if (isset($snippets[$location['id']])) {
+                                       //Set existing in current
+                                       $current = $snippets[$location['id']];
+                               //Without existing snippet
+                               } else {
+                                       //Init snippet
+                                       $current = new Snippet($this->locale, $this->doctrine->getRepository(Location::class)->findOneById($location['id']), $user);
+                               }
 
-                       //Draw on image
-                       $image->drawImage($draw);
+                               //Create SnippetType form
+                               $form = $this->factory->createNamed(
+                                       //Set form id
+                                       'snippet_'.$locationId.'_'.$id.'_'.$this->locale,
+                                       //Set form type
+                                       'Rapsys\AirBundle\Form\SnippetType',
+                                       //Set form data
+                                       $current
+                               );
 
-                       //Strip image exif data and properties
-                       $image->stripImage();
+                               //Refill the fields in case of invalid form
+                               $form->handleRequest($request);
 
-                       //Set image format
-                       $image->setImageFormat('jpeg');
+                               //Handle submitted and valid form
+                               //TODO: add a delete snippet ?
+                               if ($form->isSubmitted() && $form->isValid()) {
+                                       //Get snippet
+                                       $snippet = $form->getData();
 
-                       //Save image
-                       if (!$image->writeImage($dest)) {
-                               //Throw error
-                               throw new \Exception(sprintf('Unable to write image "%s"', $dest));
-                       }
+                                       //Queue snippet save
+                                       $this->manager->persist($snippet);
 
-                       //Get dest stat
-                       //TODO: see if it works every time
-                       $stat = stat($dest);
-
-                       //Return image data
-                       return [
-                               //TODO: see if it works every time
-                               'image' => $this->stack->getCurrentRequest()->getUriForPath($this->asset->getUrl($asset), true),#.'?fbrefresh='.$stat['mtime'],
-                               'image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
-                               'image:height' => $height,
-                               'image:width' => $width
-                       ];
-               }
+                                       //Flush to get the ids
+                                       $this->manager->flush();
 
-               //Return empty array without image
-               return [];
-       }
+                                       //Add notice
+                                       $this->addFlash('notice', $this->translator->trans('Snippet for %user% %location% updated', ['%location%' => $location['at'], '%user%' => $this->context['user']['pseudonym']]));
 
-       /**
-        * Renders a view
-        *
-        * {@inheritdoc}
-        */
-       protected function render(string $view, array $parameters = [], Response $response = null): Response {
-               //Create application form for role_guest
-               if ($this->isGranted('ROLE_GUEST')) {
-                       //Without application form
-                       if (empty($parameters['forms']['application'])) {
-                               //Fetch doctrine
-                               $doctrine = $this->getDoctrine();
-
-                               //Create ApplicationType form
-                               $application = $this->createForm('Rapsys\AirBundle\Form\ApplicationType', null, [
-                                       //Set the action
-                                       'action' => $this->generateUrl('rapsys_air_application_add'),
-                                       //Set the form attribute
-                                       'attr' => [ 'class' => 'col' ],
-                                       //Set admin
-                                       'admin' => $this->isGranted('ROLE_ADMIN'),
-                                       //Set default user to current
-                                       'user' => $this->getUser()->getId(),
-                                       //Set default slot to evening
-                                       //XXX: default to Evening (3)
-                                       'slot' => $doctrine->getRepository(Slot::class)->findOneById(3)
-                               ]);
+                                       //Redirect to cleaned url
+                                       return $this->redirectToRoute('rapsysair_user_view', ['id' => $id, 'user' => $this->context['user']['slug']]);
+                               }
 
                                //Add form to context
-                               $parameters['forms']['application'] = $application->createView();
-                       }
-               //Create login form for anonymous
-               } elseif (!$this->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
-                       //Create ApplicationType form
-                       $login = $this->createForm('Rapsys\UserBundle\Form\LoginType', null, [
-                               //Set the action
-                               'action' => $this->generateUrl('rapsys_user_login'),
-                               //Set the form attribute
-                               'attr' => [ 'class' => 'col' ]
-                       ]);
-
-                       //Add form to context
-                       $parameters['forms']['login'] = $login->createView();
-               }
+                               $this->context['forms']['snippets'][$locationId] = $form->createView();
 
-               /*
-               //TODO: set here or in constructor the controller and action name
-               //XXX: used to autogenerate the facebookimage dest
-               //XXX: with just page title and canonical we may generate miniatures automaticaly
-               if ($_SERVER['REMOTE_ADDR'] == '89.3.147.209') {
-                       header('Content-Type: text/plain');
-                       #var_dump($this->getModuleName());
-                       #var_dump($this->getController());
-                       exit;
-                       #var_dump(__CLASS__);
-                       #var_dump($router);
-                       exit;
-                       var_dump($currentRequest->attributes->get('_controller'));
-                       exit;
-               }*/
-
-
-               //With canonical
-               if (!empty($parameters['canonical'])) {
-                       //Set facebook url
-                       $parameters['ogps']['url'] = $parameters['canonical'];
-               }
+                               //With location user source image
+                               if (($isFile = is_file($source = $this->config['path'].'/location/'.$location['id'].'/'.$id.'.png')) && ($mtime = stat($source)['mtime'])) {
+                                       //Set location image
+                                       $this->context['locations'][$locationId]['image'] = $this->image->getThumb($location['miniature'], $mtime, $source);
+                               }
 
-               //With page title
-               if (!empty($parameters['page']['title'])) {
-                       //Set facebook title
-                       $parameters['ogps']['title'] = $parameters['page']['title'];
-               }
+                               //Create ImageType form
+                               $form = $this->factory->createNamed(
+                                       //Set form id
+                                       'image_'.$locationId.'_'.$id,
+                                       //Set form type
+                                       'Rapsys\AirBundle\Form\ImageType',
+                                       //Set form data
+                                       [
+                                               //Set location
+                                               'location' => $location['id'],
+                                               //Set user
+                                               'user' => $id
+                                       ],
+                                       //Set form attributes
+                                       [
+                                               //Enable delete with image
+                                               'delete' => isset($this->context['locations'][$locationId]['image'])
+                                       ]
+                               );
+
+                               //Refill the fields in case of invalid form
+                               $form->handleRequest($request);
+
+                               //Handle submitted and valid form
+                               if ($form->isSubmitted() && $form->isValid()) {
+                                       //With delete
+                                       if ($form->has('delete') && $form->get('delete')->isClicked()) {
+                                               //With source and mtime
+                                               if ($isFile && !empty($source) && !empty($mtime)) {
+                                                       //Clear thumb
+                                                       $this->image->remove($mtime, $source);
+
+                                                       //Unlink file
+                                                       unlink($this->config['path'].'/location/'.$location['id'].'/'.$id.'.png');
+
+                                                       //Add notice
+                                                       $this->addFlash('notice', $this->translator->trans('Image for %user% %location% deleted', ['%location%' => $location['at'], '%user%' => $this->context['user']['pseudonym']]));
+
+                                                       //Redirect to cleaned url
+                                                       return $this->redirectToRoute('rapsysair_user_view', ['id' => $id, 'user' => $this->context['user']['slug']]);
+                                               }
+                                       }
 
-               //With page description
-               if (!empty($parameters['page']['description'])) {
-                       //Set facebook description
-                       $parameters['ogps']['description'] = $parameters['page']['description'];
-               }
+                                       //With image
+                                       if ($image = $form->get('image')->getData()) {
+                                               //Check source path
+                                               if (!is_dir($dir = dirname($source))) {
+                                                       //Create filesystem object
+                                                       $filesystem = new Filesystem();
+
+                                                       try {
+                                                               //Create dir
+                                                               //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
+                                                               $filesystem->mkdir($dir, 0775);
+                                                       } catch (IOExceptionInterface $e) {
+                                                               //Throw error
+                                                               throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
+                                                       }
+                                               }
+
+                                               //Set source
+                                               $source = realpath($dir).'/'.basename($source);
+
+                                               //Create imagick object
+                                               $imagick = new \Imagick();
+
+                                               //Read image
+                                               $imagick->readImage($image->getRealPath());
+
+                                               //Save image
+                                               if (!$imagick->writeImage($source)) {
+                                                       //Throw error
+                                                       throw new \Exception(sprintf('Unable to write image "%s"', $source));
+                                               }
+
+                                               //Add notice
+                                               $this->addFlash('notice', $this->translator->trans('Image for %user% %location% updated', ['%location%' => $location['at'], '%user%' => $this->context['user']['pseudonym']]));
+
+                                               //Redirect to cleaned url
+                                               return $this->redirectToRoute('rapsysair_user_view', ['id' => $id, 'user' => $this->context['user']['slug']]);
+                                       }
+                               }
 
-               //With facebook image defined
-               if (!empty($this->facebookImage)) {
-                       //Get facebook image
-                       $parameters['ogps'] += $this->getFacebookImage();
+                               //Add form to context
+                               $this->context['forms']['images'][$locationId] = $form->createView();
+                       }
                }
 
-               //Call parent method
-               return $this->_render($view, $parameters, $response);
+               //Render the view
+               return $this->render('@RapsysAir/user/view.html.twig', ['id' => $id]+$this->context);
        }
 }