]> Raphaël G. Git Repositories - airbundle/commitdiff
Rename rapsysair:calendar2 command to rapsysair:calendar master 0.5.0
authorRaphaël Gertz <git@rapsys.eu>
Tue, 9 Apr 2024 13:44:41 +0000 (15:44 +0200)
committerRaphaël Gertz <git@rapsys.eu>
Tue, 9 Apr 2024 13:44:41 +0000 (15:44 +0200)
141 files changed:
Command.php [new file with mode: 0644]
Command/AttributeCommand.php
Command/CalendarCommand.php
Command/RekeyCommand.php
Command/WeatherCommand.php
Controller/AbstractController.php
Controller/ApplicationController.php
Controller/CalendarController.php
Controller/DanceController.php [new file with mode: 0644]
Controller/DefaultController.php
Controller/LocationController.php
Controller/SessionController.php
Controller/SnippetController.php
Controller/UserController.php
DataFixtures/AirFixtures.php
DependencyInjection/Configuration.php
DependencyInjection/RapsysAirExtension.php
Entity/Application.php
Entity/Civility.php
Entity/Country.php [new file with mode: 0644]
Entity/Dance.php
Entity/GoogleCalendar.php [new file with mode: 0644]
Entity/GoogleToken.php [new file with mode: 0644]
Entity/Group.php
Entity/Location.php
Entity/Session.php
Entity/Slot.php
Entity/Snippet.php
Entity/User.php
EventSubscriber/FacebookSubscriber.php [deleted file]
Factory.php [new file with mode: 0644]
Form/ApplicationType.php
Form/CalendarType.php
Form/ContactType.php
Form/Extension/Type/HiddenEntityType.php
Form/ImageType.php [new file with mode: 0644]
Form/LocationType.php
Form/RegisterType.php
Form/SessionType.php
Form/SnippetType.php
Handler/AccessDeniedHandler.php
Pdf/DisputePdf.php [deleted file]
RapsysAirBundle.php
Repository.php [new file with mode: 0644]
Repository/ApplicationRepository.php
Repository/DanceRepository.php
Repository/GoogleTokenRepository.php [new file with mode: 0644]
Repository/LocationRepository.php
Repository/SessionRepository.php
Repository/SlotRepository.php
Repository/SnippetRepository.php
Repository/UserRepository.php
Resources/config/doctrine/Country.orm.yml [new file with mode: 0644]
Resources/config/doctrine/Dance.orm.yml
Resources/config/doctrine/GoogleCalendar.orm.yml [new file with mode: 0644]
Resources/config/doctrine/GoogleToken.orm.yml [new file with mode: 0644]
Resources/config/doctrine/Location.orm.yml
Resources/config/doctrine/Session.orm.yml
Resources/config/doctrine/Slot.orm.yml
Resources/config/doctrine/User.orm.yml
Resources/config/packages/rapsys_air.yaml [deleted file]
Resources/config/packages/rapsysair.yaml [new file with mode: 0644]
Resources/config/routes/rapsysair.yaml [moved from Resources/config/routes/rapsys_air.yaml with 55% similarity]
Resources/public/css/droidsans.css
Resources/public/css/lemon.css [new file with mode: 0644]
Resources/public/css/notoemoji.css [new file with mode: 0644]
Resources/public/css/screen.css
Resources/public/facebook/.keep [deleted file]
Resources/public/jpeg/calendar.jpeg [new file with mode: 0644]
Resources/public/jpeg/facebook.1200x1200.jpeg [new file with mode: 0644]
Resources/public/jpeg/logo.en.jpeg [new file with mode: 0644]
Resources/public/jpeg/logo.fr.jpeg [new file with mode: 0644]
Resources/public/png/calendar.png [new file with mode: 0644]
Resources/public/png/facebook.1200x1200.png [new file with mode: 0644]
Resources/public/png/logo.171.png [new file with mode: 0644]
Resources/public/png/logo.orig.png [new file with mode: 0644]
Resources/public/png/logo.png
Resources/public/svg/calendar.svg [new file with mode: 0644]
Resources/public/svg/logo.orig.svg [new file with mode: 0644]
Resources/public/svg/logo.svg [new file with mode: 0644]
Resources/public/ttf/default.ttf [new symlink]
Resources/public/ttf/lemon.ttf [new file with mode: 0644]
Resources/public/ttf/notoemoji.ttf [new file with mode: 0644]
Resources/public/woff2/lemon.woff2 [new file with mode: 0644]
Resources/public/woff2/notoemoji.woff2 [new file with mode: 0644]
Resources/translations/messages.en_gb.yaml
Resources/translations/messages.fr_fr.yaml
Resources/views/admin/index.html.twig
Resources/views/application/add.html.twig
Resources/views/base.html.twig
Resources/views/body.html.twig [deleted file]
Resources/views/calendar/callback.html.twig
Resources/views/calendar/index.html.twig
Resources/views/default/_city.html.twig [new file with mode: 0644]
Resources/views/default/_location.html.twig
Resources/views/default/about.html.twig
Resources/views/default/dispute.html.twig [deleted file]
Resources/views/default/frequently_asked_questions.html.twig
Resources/views/default/index.html.twig
Resources/views/default/organizer_regulation.html.twig
Resources/views/default/regulation.html.twig
Resources/views/default/terms_of_service.html.twig
Resources/views/error.html.twig
Resources/views/form/_application.html.twig
Resources/views/form/_location.html.twig
Resources/views/form/_login.html.twig
Resources/views/form/_register.html.twig
Resources/views/form/_session.html.twig
Resources/views/form/contact.html.twig
Resources/views/form/edit.html.twig
Resources/views/form/form_div_layout.html.twig
Resources/views/form/login.html.twig
Resources/views/form/recover.html.twig
Resources/views/form/recover_mail.html.twig
Resources/views/form/register.html.twig
Resources/views/location/add.html.twig [deleted file]
Resources/views/location/cities.html.twig [new file with mode: 0644]
Resources/views/location/city.html.twig [new file with mode: 0644]
Resources/views/location/index.html.twig
Resources/views/location/view.html.twig
Resources/views/mail/base.html.twig [moved from Resources/views/mail/body.html.twig with 81% similarity]
Resources/views/mail/contact.html.twig
Resources/views/mail/recover.html.twig
Resources/views/mail/recover.text.twig
Resources/views/mail/recover_mail.html.twig
Resources/views/mail/recover_mail.text.twig
Resources/views/mail/register.html.twig
Resources/views/mail/register.text.twig
Resources/views/security/denied.html.twig
Resources/views/session/edit.html.twig
Resources/views/session/index.html.twig
Resources/views/session/view.html.twig
Resources/views/snippet/add.html.twig
Resources/views/snippet/edit.html.twig
Resources/views/user/edit.html.twig [deleted file]
Resources/views/user/index.html.twig
Resources/views/user/view.html.twig
Token/AnonymousToken.php [new file with mode: 0644]
Transformer/DanceTransformer.php [new file with mode: 0644]
Transformer/SubscriptionTransformer.php [new file with mode: 0644]
composer.json

diff --git a/Command.php b/Command.php
new file mode 100644 (file)
index 0000000..c775499
--- /dev/null
@@ -0,0 +1,96 @@
+<?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;
+
+use Doctrine\Persistence\ManagerRegistry;
+
+use Symfony\Component\Console\Command\Command as BaseCommand;
+use Symfony\Component\Routing\RouterInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
+
+use Rapsys\AirBundle\RapsysAirBundle;
+
+use Rapsys\PackBundle\Util\SluggerUtil;
+
+/**
+ * {@inheritdoc}
+ */
+class Command extends BaseCommand {
+       /**
+        * {@inheritdoc}
+        *
+        * Creates new command
+        *
+        * @param ManagerRegistry $doctrine The doctrine instance
+        * @param RouterInterface $router The router instance
+        * @param SluggerUtil $slugger The slugger instance
+        * @param TranslatorInterface $translator The translator instance
+        * @param string $locale The default locale
+        */
+       public function __construct(protected ManagerRegistry $doctrine, protected string $locale, protected RouterInterface $router, protected SluggerUtil $slugger, protected TranslatorInterface $translator, protected ?string $name = null) {
+               //Fix name
+               $this->name = $this->name ?? static::getName();
+
+               //Call parent constructor
+               parent::__construct($this->name);
+
+               //With description
+               if (!empty($this->description)) {
+                       //Set description
+                       $this->setDescription($this->description);
+               }
+
+               //With help
+               if (!empty($this->help)) {
+                       //Set help
+                       $this->setHelp($this->help);
+               }
+
+               //Get router context
+               $context = $this->router->getContext();
+
+               //Set hostname
+               $context->setHost($_ENV['RAPSYSAIR_HOSTNAME']);
+
+               //Set scheme
+               $context->setScheme($_ENV['RAPSYSAIR_SCHEME'] ?? 'https');
+       }
+
+       /**
+        * {@inheritdoc}
+        *
+        * Return the command name
+        */
+       public function getName(): string {
+               //With namespace
+               if ($npos = strrpos(static::class, '\\')) {
+                       //Set name pos
+                       $npos++;
+               //Without namespace
+               } else {
+                       $npos = 0;
+               }
+
+               //With trailing command
+               if (substr(static::class, -strlen('Command'), strlen('Command')) === 'Command') {
+                       //Set bundle pos
+                       $bpos = strlen(static::class) - $npos - strlen('Command');
+               //Without bundle
+               } else {
+                       //Set bundle pos
+                       $bpos = strlen(static::class) - $npos;
+               }
+
+               //Return command alias
+               return RapsysAirBundle::getAlias().':'.strtolower(substr(static::class, $npos, $bpos));
+       }
+}
index 8f26e26dd8f51d5c01d701dd0cec97a7a1071534..f4fa13368564d3453f6b94f9d19c6632447ce8a3 100644 (file)
@@ -1,10 +1,21 @@
-<?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\Command;
 
 use Doctrine\Bundle\DoctrineBundle\Command\DoctrineCommand;
+
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
+
 use Rapsys\AirBundle\Entity\Session;
 
 class AttributeCommand extends DoctrineCommand {
@@ -27,7 +38,7 @@ class AttributeCommand extends DoctrineCommand {
        }
 
        ///Process the attribution
-       protected function execute(InputInterface $input, OutputInterface $output) {
+       protected function execute(InputInterface $input, OutputInterface $output): int {
                //Fetch doctrine
                $doctrine = $this->getDoctrine();
 
index 07e537b620bcea562cf19e5d435a8c51ec69ed5c..f722aaed4cfa842eeb2bf74cedd6ee93a68ea99f 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\Command;
 
 use Doctrine\Persistence\ManagerRegistry;
-use Symfony\Component\Cache\Adapter\FilesystemAdapter;
-use Symfony\Component\Console\Command\Command;
+
+use Google\Client;
+use Google\Service\Calendar;
+use Google\Service\Calendar\Event;
+use Google\Service\Calendar\EventExtendedProperties;
+use Google\Service\Calendar\EventSource;
+
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 use Symfony\Component\Routing\RouterInterface;
-use Symfony\Component\Translation\TranslatorInterface;
+use Symfony\Contracts\Cache\CacheInterface;
+use Symfony\Contracts\Cache\ItemInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
+
 use Twig\Extra\Markdown\DefaultMarkdown;
 
+use Rapsys\AirBundle\Command;
+use Rapsys\AirBundle\Entity\GoogleCalendar;
+use Rapsys\AirBundle\Entity\GoogleToken;
 use Rapsys\AirBundle\Entity\Session;
 
+use Rapsys\PackBundle\Util\SluggerUtil;
+
+/**
+ * {@inheritdoc}
+ *
+ * Synchronize sessions in users' google calendar
+ */
 class CalendarCommand extends Command {
-       //Set failure constant
-       const FAILURE = 1;
+       /**
+        * Set description
+        *
+        * Shown with bin/console list
+        */
+       protected string $description = 'Synchronize sessions in users\' calendar';
 
-       ///Set success constant
-       const SUCCESS = 0;
+       /**
+        * Set help
+        *
+        * Shown with bin/console --help rapsysair:calendar
+        */
+       protected string $help = 'This command synchronize sessions in users\' google calendar';
 
-       ///Config array
-       protected $config;
+       /**
+        * Set domain
+        */
+       protected string $domain;
 
        /**
-        * Doctrine instance
+        * Set item
         *
-        * @var ManagerRegistry
+        * Cache item instance
         */
-       protected $doctrine;
+       protected ItemInterface $item;
 
-       ///Locale
-       protected $locale;
+       /**
+        * Set prefix
+        */
+       protected string $prefix;
 
-       ///Translator instance
-       protected $translator;
+       /**
+        * Set service
+        *
+        * Google calendar instance
+        */
+       protected Calendar $service;
 
        /**
-        * Inject doctrine, container and translator interface
+        * {@inheritdoc}
         *
-        * @param ContainerInterface $container The container instance
-        * @param ManagerRegistry $doctrine The doctrine instance
-        * @param RouterInterface $router The router instance
-        * @param TranslatorInterface $translator The translator instance
+        * @param CacheInterface $cache The cache instance
+        * @param Client $google The google client instance
+        * @param DefaultMarkdown $markdown The markdown instance
         */
-    public function __construct(ContainerInterface $container, ManagerRegistry $doctrine, RouterInterface $router, TranslatorInterface $translator) {
+       public function __construct(protected ManagerRegistry $doctrine, protected string $locale, protected RouterInterface $router, protected SluggerUtil $slugger, protected TranslatorInterface $translator, protected CacheInterface $cache, protected Client $google, protected DefaultMarkdown $markdown) {
                //Call parent constructor
-               parent::__construct();
+               parent::__construct($this->doctrine, $this->locale, $this->router, $this->slugger, $this->translator);
 
-               //Retrieve config
-               $this->config = $container->getParameter($this->getAlias());
+               //Replace google client redirect uri
+               $this->google->setRedirectUri($this->router->generate($this->google->getRedirectUri(), [], UrlGeneratorInterface::ABSOLUTE_URL));
+       }
 
-               //Retrieve locale
-               $this->locale = $container->getParameter('kernel.default_locale');
+       /**
+        * Process the attribution
+        */
+       protected function execute(InputInterface $input, OutputInterface $output): int {
+               //Get domain
+               $this->domain = $this->router->getContext()->getHost();
 
-               //Store doctrine
-        $this->doctrine = $doctrine;
+               //Get manager
+               $manager = $this->doctrine->getManager();
 
-               //Store router
-        $this->router = $router;
+               //Convert from any to latin, then to ascii and lowercase
+               $trans = \Transliterator::create('Any-Latin; Latin-ASCII; Lower()');
 
-               //Get router context
-               $context = $this->router->getContext();
+               //Replace every non alphanumeric character by dash then trim dash
+               $this->prefix = preg_replace(['/(\.[a-z0-9]+)+$/', '/[^a-v0-9]+/'], '', $trans->transliterate($this->domain));
 
-               //Set host
-               $context->setHost('airlibre.eu');
+               //With too short prefix
+               if ($this->prefix === null || strlen($this->prefix) < 4) {
+                       //Throw domain exception
+                       throw new \DomainException('Prefix too short: '.$this->prefix);
+               }
 
-               //Set scheme
-               $context->setScheme('https');
+               //Iterate on google tokens
+               foreach($tokens = $this->doctrine->getRepository(GoogleToken::class)->findAllIndexed() as $tid => $token) {
+                       //Clear client cache before changing access token
+                       //TODO: set a per token cache ?
+                       $this->google->getCache()->clear();
+
+                       //Set access token
+                       $this->google->setAccessToken(
+                               [
+                                       'access_token' => $token['access'],
+                                       'refresh_token' => $token['refresh'],
+                                       'created' => $token['created']->getTimestamp(),
+                                       'expires_in' => $token['expired']->getTimestamp() - (new \DateTime('now'))->getTimestamp()
+                               ]
+                       );
+
+                       //With expired token
+                       if ($this->google->isAccessTokenExpired()) {
+                               //Refresh token
+                               if (($gRefresh = $this->google->getRefreshToken()) && ($gToken = $this->google->fetchAccessTokenWithRefreshToken($gRefresh)) && empty($gToken['error'])) {
+                                       //Get google token
+                                       $googleToken = $this->doctrine->getRepository(GoogleToken::class)->findOneById($token['id']);
+
+                                       //Set access token
+                                       $googleToken->setAccess($gToken['access_token']);
+
+                                       //Set expires
+                                       $googleToken->setExpired(new \DateTime('+'.$gToken['expires_in'].' second'));
+
+                                       //Set refresh
+                                       $googleToken->setRefresh($gToken['refresh_token']);
+
+                                       //Queue google token save
+                                       $manager->persist($googleToken);
+
+                                       //Flush to get the ids
+                                       $manager->flush();
+                               //Refresh failed
+                               } else {
+                                       //Show error
+                                       fprintf(STDERR, 'Unable to refresh token %d: %s', $token['id'], $gToken['error']?:'');
+
+                                       //TODO: warn user by mail ?
+
+                                       //Skip to next token
+                                       continue;
+                               }
+                       }
 
-               //Set the translator
-               $this->translator = $translator;
-       }
+                       //Get google calendar service
+                       $this->service = new Calendar($this->google);
 
-       ///Configure attribute command
-       protected function configure() {
-               //Configure the class
-               $this
-                       //Set name
-                       ->setName('rapsysair:calendar')
-                       //Set description shown with bin/console list
-                       ->setDescription('Synchronize sessions in calendar')
-                       //Set description shown with bin/console --help airlibre:attribute
-                       ->setHelp('This command synchronize sessions in google calendar');
-       }
+                       //Iterate on google calendars
+                       foreach($calendars = $token['calendars'] as $cid => $calendar) {
+                               //Set start
+                               $synchronized = null;
 
-       ///Process the attribution
-       protected function execute(InputInterface $input, OutputInterface $output) {
-               //Compute period
-               $period = new \DatePeriod(
-                       //Start from last week
-                       new \DateTime('-1 week'),
-                       //Iterate on each day
-                       new \DateInterval('P1D'),
-                       //End with next 2 week
-                       new \DateTime('+2 week')
-               );
+                               //Set cache key
+                               $cacheKey = 'command.calendar.'.$this->slugger->short($calendar['mail']);
 
-               //Retrieve events to update
-               $sessions = $this->doctrine->getRepository(Session::class)->fetchAllByDatePeriod($period, $this->locale);
+                               //XXX: TODO: remove DEBUG
+                               #$this->cache->delete($cacheKey);
 
-               //Markdown converted instance
-               $markdown = new DefaultMarkdown;
+                               //Retrieve calendar events
+                               try {
+                                       //Get events
+                                       $events = $this->cache->get(
+                                               //Cache key
+                                               //XXX: set to command.calendar.$mail
+                                               $cacheKey,
+                                               //Fetch mail calendar event list
+                                               function (ItemInterface $item) use ($calendar, &$synchronized): array {
+                                                       //Expire after 1h
+                                                       $item->expiresAfter(3600);
+
+                                                       //Set synchronized
+                                                       $synchronized = new \DateTime('now');
+
+                                                       //Init events
+                                                       $events = [];
+
+                                                       //Set filters
+                                                       //TODO: add a filter to only retrieve
+                                                       $filters = [
+                                                               //XXX: every event even deleted one to be able to update them
+                                                               'showDeleted' => true,
+                                                               //XXX: every instances
+                                                               'singleEvents' => false,
+                                                               //XXX: select only domain events
+                                                               'privateExtendedProperty' => 'domain='.$this->domain
+                                                               #TODO: restrict events even more by time or updated datetime ? (-1 week to +2 week)
+                                                               //TODO: fetch events one day before and one day after to avoid triggering double insert duplicate key 409 errors :=) on google
+                                                               #'timeMin' => $period->getStartDate()->format(\DateTime::ISO8601),
+                                                               #'timeMax' => $period->getEndDate()->format(\DateTime::ISO8601)
+                                                               /*, 'iCalUID' => 'airlibre/?????'*//*'orderBy' => 'startTime', */
+                                                               //updatedMin => new \DateTime('-1 week') ?
+                                                       ];
+
+                                                       //Set page token
+                                                       $pageToken = null;
+
+                                                       //Iterate until next page token is null
+                                                       do {
+                                                               //Get calendar events list
+                                                               //XXX: see vendor/google/apiclient-services/src/Calendar/Resource/Events.php +289
+                                                               $eventList = $this->service->events->listEvents($calendar['mail'], ['pageToken' => $pageToken]+$filters);
+
+                                                               //Iterate on items
+                                                               foreach($eventList->getItems() as $event) {
+                                                                       //With extended properties
+                                                                       if (($properties = $event->getExtendedProperties()) && ($private = $properties->getPrivate()) && isset($private['id']) && ($id = $private['id']) && isset($private['domain']) && $private['domain'] == $this->domain) {
+                                                                               //Add event
+                                                                               $events[$id] = $event;
+                                                                       //XXX: 3rd party events without matching prefix and id are skipped
+                                                                       #} else {
+                                                                       #       #echo 'Skipping '.$event->getId().':'.$event->getSummary()."\n";
+                                                                       #       echo 'Skipping '.$id.':'.$event->getSummary()."\n";
+                                                                       }
+                                                               }
+                                                       } while ($pageToken = $eventList->getNextPageToken());
+
+                                                       //Return events
+                                                       return $events;
+                                               }
+                                       );
+                               //Catch exception
+                               } catch(\Google\Service\Exception $e) {
+                                       //With 401 or code
+                                       //XXX: see https://cloud.google.com/apis/design/errors
+                                       if ($e->getCode() == 401 || $e->getCode() == 403) {
+                                               //Show error
+                                               fprintf(STDERR, 'Unable to list calendar %d events: %s', $calendar['id'], $e->getMessage()?:'');
 
-               //Retrieve cache object
-               //XXX: by default stored in /tmp/symfony-cache/@/W/3/6SEhFfeIW4UMDlAII+Dg
-               //XXX: stored in %kernel.project_dir%/var/cache/airlibre/0/P/IA20X0K4dkMd9-+Ohp9Q
-               $cache = new FilesystemAdapter($this->config['cache']['namespace'], $this->config['cache']['lifetime'], $this->config['path']['cache']);
+                                               //TODO: warn user by mail ?
 
-               //Retrieve calendars
-               $cacheCalendars = $cache->getItem('calendars');
+                                               //Skip to next token
+                                               continue;
+                                       }
 
-               //Without calendars
-               if (!$cacheCalendars->isHit()) {
-                       //Return failure
-                       return self::FAILURE;
-               }
+                                       //Throw error
+                                       throw new \LogicException('Calendar event list failed', 0, $e);
+                               }
 
-               //Retrieve calendars
-               $calendars = $cacheCalendars->get();
-
-               //Check expired token
-               foreach($calendars as $clientId => $client) {
-                       //Get google client
-                       $googleClient = new \Google\Client(['application_name' => $client['project'], 'client_id' => $clientId, 'client_secret' => $client['secret'], 'redirect_uri' => $client['redirect']]);
-
-                       //Iterate on each tokens
-                       foreach($client['tokens'] as $tokenId => $token) {
-                               //Set token
-                               $googleClient->setAccessToken(
-                                       [
-                                               'access_token' => $tokenId,
-                                               'refresh_token' => $token['refresh'],
-                                               'expires_in' => $token['expire'],
-                                               'scope' => $token['scope'],
-                                               'token_type' => $token['type'],
-                                               'created' => $token['created']
-                                       ]
-                               );
-
-                               //With expired token
-                               if ($exp = $googleClient->isAccessTokenExpired()) {
-                                       //Refresh token
-                                       if (($refreshToken = $googleClient->getRefreshToken()) && ($googleToken = $googleClient->fetchAccessTokenWithRefreshToken($refreshToken)) && empty($googleToken['error'])) {
-                                               //Add refreshed token
-                                               $calendars[$clientId]['tokens'][$googleToken['access_token']] = [
-                                                       'calendar' => $token['calendar'],
-                                                       'prefix' => $token['prefix'],
-                                                       'refresh' => $googleToken['refresh_token'],
-                                                       'expire' => $googleToken['expires_in'],
-                                                       'scope' => $googleToken['scope'],
-                                                       'type' => $googleToken['token_type'],
-                                                       'created' => $googleToken['created']
-                                               ];
-
-                                               //Remove old token
-                                               unset($calendars[$clientId]['tokens'][$tokenId]);
-                                       } else {
-                                               //Drop token
-                                               unset($calendars[$clientId]['tokens'][$tokenId]);
-
-                                               //Without tokens
-                                               if (empty($calendars[$clientId]['tokens'])) {
-                                                       //Drop client
-                                                       unset($calendars[$clientId]);
+                               //Store cache item
+                               $this->item = $this->cache->getItem($cacheKey);
+
+                               //Iterate on sessions to update
+                               foreach($this->doctrine->getRepository(Session::class)->findAllByUserIdSynchronized($token['uid'], $calendar['synchronized']) as $session) {
+                                       //Start exception catching
+                                       try {
+                                               //Without event
+                                               if (!isset($events[$session['id']])) {
+                                                       //Insert event
+                                                       $this->insert($calendar['mail'], $session);
+                                               //With locked session
+                                               } elseif (($event = $events[$session['id']]) && !empty($session['locked'])) {
+                                                       //Delete event
+                                                       $sid = $this->delete($calendar['mail'], $event);
+
+                                                       //Drop from events array
+                                                       unset($events[$sid]);
+                                               //With event to update
+                                               } elseif ($session['modified'] > (new \DateTime($event->getUpdated()))) {
+                                                       //Update event
+                                                       $sid = $this->update($calendar['mail'], $event, $session);
+
+                                                       //Drop from events array
+                                                       unset($events[$sid]);
                                                }
+                                       //Catch exception
+                                       } catch(\Google\Service\Exception $e) {
+                                               //Identifier already exists
+                                               if ($e->getCode() == 409) {
+                                                       //Get calendar event
+                                                       //XXX: see vendor/google/apiclient-services/src/Calendar/Resource/Events.php +81
+                                                       $event = $this->service->events->get($calendar['mail'], $this->prefix.$session['id']);
+
+                                                       //Update required
+                                                       if ($session['modified'] > (new \DateTime($event->getUpdated()))) {
+                                                               //Update event
+                                                               $sid = $this->update($calendar['mail'], $event, $session);
+
+                                                               //Drop from events array
+                                                               unset($events[$sid]);
+                                                       }
+                                               //TODO: handle other codes gracefully ? (503 & co)
+                                               //Other errors
+                                               } else {
+                                                       //Throw error
+                                                       throw new \LogicException(sprintf('Calendar %s event %s operation failed', $calendar, $this->prefix.$session['id']), 0, $e);
+                                               }
+                                       }
+                               }
 
-                                               //Save calendars
-                                               $cacheCalendars->set($calendars);
+                               //Get all sessions
+                               $sessions = $this->doctrine->getRepository(Session::class)->findAllByUserIdSynchronized($token['uid']);
 
-                                               //Save calendar
-                                               $cache->save($cacheCalendars);
+                               //Remaining events to drop
+                               foreach($events as $eid => $event) {
+                                       //With events updated since last synchronized
+                                       if ($event->getStatus() == 'confirmed' && (new \DateTime($event->getUpdated())) > $calendar['synchronized']) {
+                                               //TODO: Add a try/catch here to handle error codes gracefully (503 & co) ?
+                                               //With event to update
+                                               if (isset($sessions[$eid]) && ($session = $sessions[$eid]) && empty($session['locked'])) {
+                                                       //Update event
+                                                       $sid = $this->update($calendar['mail'], $event, $session);
+
+                                                       //Drop from events array
+                                                       unset($events[$sid]);
+                                               //With locked or unknown session
+                                               } else {
+                                                       //Delete event
+                                                       $sid = $this->delete($calendar['mail'], $event);
+
+                                                       //Drop from events array
+                                                       unset($events[$sid]);
+                                               }
+                                       }
+                               }
 
-                                               //Drop token and report
-                                               //XXX: submit app to avoid expiration
-                                               //XXX: see https://console.cloud.google.com/apis/credentials/consent?project=calendar-317315
-                                               echo 'Token '.$tokenId.' for calendar '.$token['calendar'].' has expired and is not refreshable'."\n";
+                               //Persist cache item
+                               $this->cache->commit();
 
-                                               //Return failure
-                                               //XXX: we want that mail and stop here
-                                               return self::FAILURE;
-                                       }
+                               //With synchronized
+                               //XXX: only store synchronized on run without caching
+                               if ($synchronized) {
+                                       //Get google calendar
+                                       $googleCalendar = $this->doctrine->getRepository(GoogleCalendar::class)->findOneById($calendar['id']);
+
+                                       //Set synchronized
+                                       $googleCalendar->setSynchronized($synchronized);
+
+                                       //Queue google calendar save
+                                       $manager->persist($googleCalendar);
+
+                                       //Flush to get the ids
+                                       $manager->flush();
                                }
                        }
                }
 
-               //Save calendars
-               $cacheCalendars->set($calendars);
-
-               //Save calendar
-               $cache->save($cacheCalendars);
-
-               //Iterate on each calendar client
-               foreach($calendars as $clientId => $client) {
-                       //Get google client
-                       $googleClient = new \Google\Client(['application_name' => $client['project'], 'client_id' => $clientId, 'client_secret' => $client['secret'], 'redirect_uri' => $client['redirect']]);
-
-                       //Iterate on each tokens
-                       foreach($client['tokens'] as $tokenId => $token) {
-                               //Set token
-                               $googleClient->setAccessToken(
-                                       [
-                                               'access_token' => $tokenId,
-                                               'refresh_token' => $token['refresh'],
-                                               'expires_in' => $token['expire'],
-                                               'scope' => $token['scope'],
-                                               'token_type' => $token['type'],
-                                               'created' => $token['created']
-                                       ]
-                               );
+               //Return success
+               return self::SUCCESS;
+       }
 
-                               //With expired token
-                               if ($exp = $googleClient->isAccessTokenExpired()) {
-                                       //Last chance to skip this run
-                                       continue;
-                               }
+       /**
+        * Delete event
+        *
+        * @param string $calendar The calendar mail
+        * @param Event $event The google event instance
+        * @return void
+        */
+       function delete(string $calendar, Event $event): int {
+               //Get cache events
+               $cacheEvents = $this->item->get();
 
-                               //Get google calendar
-                               $googleCalendar = new \Google\Service\Calendar($googleClient);
+               //Get event id
+               $eid = $event->getId();
 
-                               //Retrieve calendar
-                               try {
-                                       $calendar = $googleCalendar->calendars->get($token['calendar']);
-                               //Catch exception
-                               } catch(\Google\Service\Exception $e) {
-                                       //Display exception
-                                       //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
-                                       echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
-                                       echo $e->getTraceAsString()."\n";
+               //Delete the event
+               $this->service->events->delete($calendar, $eid);
 
-                                       //Return failure
-                                       return self::FAILURE;
-                               }
+               //Set sid
+               $sid = intval(substr($event->getId(), strlen($this->prefix)));
 
-                               //Init events
-                               $events = [];
-
-                               //Set filters
-                               $filters = [
-                                       //XXX: show even deleted event to be able to update them
-                                       'showDeleted' => true,
-                                       //TODO: fetch events one day before and one day after to avoid triggering double insert duplicate key 409 errors :=) on google
-                                       'timeMin' => $period->getStartDate()->format(\DateTime::ISO8601),
-                                       'timeMax' => $period->getEndDate()->format(\DateTime::ISO8601)
-                                       /*, 'iCalUID' => 'airlibre/?????'*//*'orderBy' => 'startTime', */
-                               ];
-
-                               //Retrieve event collection
-                               $googleEvents = $googleCalendar->events->listEvents($token['calendar'], $filters);
-
-                               //Iterate until reached end
-                               while (true) {
-                                       //Iterate on each event
-                                       foreach ($googleEvents->getItems() as $event) {
-                                               //Store event by id
-                                               if (preg_match('/^'.$token['prefix'].'([0-9]+)$/', $id = $event->getId(), $matches)) {
-                                                       $events[$matches[1]] = $event;
-                                               //XXX: 3rd party events with id not matching prefix are skipped
-                                               #} else {
-                                               #       echo 'Skipping '.$event->getId().':'.$event->getSummary()."\n";*/
-                                               }
-                                       }
+               //Remove from events and cache events
+               unset($cacheEvents[$sid]);
 
-                                       //Get page token
-                                       $pageToken = $googleEvents->getNextPageToken();
+               //Set cache events
+               $this->item->set($cacheEvents);
 
-                                       //Handle next page
-                                       if ($pageToken) {
-                                               //Replace collection with next one
-                                               $googleEvents = $service->events->listEvents($token['calendar'], $filters+['pageToken' => $pageToken]);
-                                       } else {
-                                               break;
-                                       }
-                               }
+               //Save cache item
+               $this->cache->saveDeferred($this->item);
 
-                               //Iterate on each session to sync
-                               foreach($sessions as $sessionId => $session) {
-                                       //Init shared properties
-                                       //TODO: validate for constraints here ??? https://developers.google.com/calendar/api/guides/extended-properties
-                                       //TODO: drop shared as unused ???
-                                       $shared = [
-                                               'gps' => $session['l_latitude'].','.$session['l_longitude']
-                                       ];
-
-                                       //Init source
-                                       $source = [
-                                               'title' => $this->translator->trans('Session %id% by %pseudonym%', ['%id%' => $sessionId, '%pseudonym%' => $session['au_pseudonym']]).' '.$this->translator->trans('at '.$session['l_title']),
-                                               'url' => $this->router->generate('rapsys_air_session_view', ['id' => $sessionId], UrlGeneratorInterface::ABSOLUTE_URL)
-                                       ];
-
-                                       //Init description
-                                       $description = 'Description :'."\n".strip_tags(preg_replace('!<a href="([^"]+)"(?: title="[^"]+")?'.'>([^<]+)</a>!', '\1', $markdown->convert(strip_tags($session['p_description']))));
-                                       $shared['description'] = $markdown->convert(strip_tags($session['p_description']));
-
-                                       //Add class when available
-                                       if (!empty($session['p_class'])) {
-                                               $shared['class'] = $session['p_class'];
-                                               $description .= "\n\n".'Classe :'."\n".$session['p_class'];
-                                       }
+               //Return session id
+               return $sid;
+       }
 
-                                       //Add contact when available
-                                       if (!empty($session['p_contact'])) {
-                                               $shared['contact'] = $session['p_contact'];
-                                               $description .= "\n\n".'Contact :'."\n".$session['p_contact'];
-                                       }
+       /**
+        * Fill event
+        *
+        * TODO: add domain based/calendar mail specific templates ?
+        *
+        * @param array $session The session instance
+        * @param ?Event $event The event instance
+        * @return Event The filled event
+        */
+       function fill(array $session, ?Event $event = null): Event {
+               //Init private properties
+               $private = [
+                       'id' => $session['id'],
+                       'domain' => $this->domain,
+                       'updated' => $session['modified']->format(\DateTime::ISO8601)
+               ];
+
+               //Init shared properties
+               //TODO: validate for constraints here ??? https://developers.google.com/calendar/api/guides/extended-properties
+               //TODO: drop shared as unused ???
+               $shared = [
+                       'gps' => $session['l_latitude'].','.$session['l_longitude']
+               ];
+
+               //Init source
+               $source = new EventSource(
+                       [
+                               'title' => $this->translator->trans('%dance% %id% by %pseudonym%', ['%id%' => $session['id'], '%dance%' => $this->translator->trans($session['ad_name'].' '.lcfirst($session['ad_type'])), '%pseudonym%' => $session['au_pseudonym']]).' '.$this->translator->trans('at '.$session['l_title']),
+                               'url' => $this->router->generate('rapsysair_session_view', ['id' => $session['id'], 'location' => $this->slugger->slug($this->translator->trans($session['l_title'])), 'dance' => $this->slugger->slug($this->translator->trans($session['ad_name'].' '.lcfirst($session['ad_type']))), 'user' => $this->slugger->slug($session['au_pseudonym'])], UrlGeneratorInterface::ABSOLUTE_URL)
+                       ]
+               );
 
-                                       //Add donate when available
-                                       if (!empty($session['p_donate'])) {
-                                               $shared['donate'] = $session['p_donate'];
-                                               $description .= "\n\n".'Contribuer :'."\n".$session['p_donate'];
-                                       }
+               //Init location
+               $description = 'Emplacement :'."\n".$this->translator->trans($session['l_description']);
+               $shared['location'] = strip_tags($this->translator->trans($session['l_description']));
 
-                                       //Add link when available
-                                       if (!empty($session['p_link'])) {
-                                               $shared['link'] = $session['p_link'];
-                                               $description .= "\n\n".'Site :'."\n".$session['p_link'];
-                                       }
+               //Add description when available
+               if(!empty($session['p_description'])) {
+                       $description .= "\n\n".'Description :'."\n".strip_tags(preg_replace('!<a href="([^"]+)"(?: title="[^"]+")?'.'>([^<]+)</a>!', '\1', $this->markdown->convert(strip_tags($session['p_description']))));
+                       $shared['description'] = $this->markdown->convert(strip_tags($session['p_description']));
+               }
 
-                                       //Add profile when available
-                                       if (!empty($session['p_profile'])) {
-                                               $shared['profile'] = $session['p_profile'];
-                                               $description .= "\n\n".'Réseau social :'."\n".$session['p_profile'];
-                                       }
+               //Add class when available
+               if (!empty($session['p_class'])) {
+                       $description .= "\n\n".'Classe :'."\n".$session['p_class'];
+                       $shared['class'] = $session['p_class'];
+               }
 
-                                       //Locked session
-                                       if (!empty($session['locked']) && $events[$sessionId]) {
-                                               //With events
-                                               if (!empty($event = $events[$sessionId])) {
-                                                       try {
-                                                               //Delete the event
-                                                               $googleCalendar->events->delete($token['calendar'], $event->getId());
-                                                       //Catch exception
-                                                       } catch(\Google\Service\Exception $e) {
-                                                               //Display exception
-                                                               //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
-                                                               echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
-                                                               echo $e->getTraceAsString()."\n";
-
-                                                               //Return failure
-                                                               return self::FAILURE;
-                                                       }
-                                               }
-                                       //Without event
-                                       } elseif (empty($events[$sessionId])) {
-                                               //Init event
-                                               $event = new \Google\Service\Calendar\Event(
-                                                       [
-                                                               //TODO: replace 'airlibre' with $this->config['calendar']['prefix'] when possible with prefix validating [a-v0-9]{5,}
-                                                               //XXX: see https://developers.google.com/calendar/api/v3/reference/events/insert#id
-                                                               'id' => $token['prefix'].$sessionId,
-                                                               'summary' => $session['au_pseudonym'].' '.$this->translator->trans('at '.$session['l_title']),
-                                                               #'description' => $markdown->convert(strip_tags($session['p_description'])),
-                                                               'description' => $description,
-                                                               'status' => empty($session['a_canceled'])?'confirmed':'cancelled',
-                                                               'location' => implode(' ', [$session['l_address'], $session['l_zipcode'], $session['l_city']]),
-                                                               'source' => $source,
-                                                               'extendedProperties' => [
-                                                                       'shared' => $shared
-                                                               ],
-                                                               //TODO: colorId ?
-                                                               //TODO: attendees[] ?
-                                                               'start' => [
-                                                                       'dateTime' => $session['start']->format(\DateTime::ISO8601)
-                                                               ],
-                                                               'end' => [
-                                                                       'dateTime' => $session['stop']->format(\DateTime::ISO8601)
-                                                               ]
-                                                       ]
-                                               );
-
-                                               try {
-                                                       //Insert the event
-                                                       $googleCalendar->events->insert($token['calendar'], $event);
-                                               //Catch exception
-                                               } catch(\Google\Service\Exception $e) {
-                                                       //Display exception
-                                                       //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
-                                                       echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
-                                                       echo $e->getTraceAsString()."\n";
-
-                                                       //Return failure
-                                                       return self::FAILURE;
-                                               }
-                                       // With event
-                                       } else {
-                                               //Set event
-                                               $event = $events[$sessionId];
+               //Add contact when available
+               if (!empty($session['p_contact'])) {
+                       $description .= "\n\n".'Contact :'."\n".$session['p_contact'];
+                       $shared['contact'] = $session['p_contact'];
+               }
+
+               //Add donate when available
+               if (!empty($session['p_donate'])) {
+                       $description .= "\n\n".'Contribuer :'."\n".$session['p_donate'];
+                       $shared['donate'] = $session['p_donate'];
+               }
+
+               //Add link when available
+               if (!empty($session['p_link'])) {
+                       $description .= "\n\n".'Site :'."\n".$session['p_link'];
+                       $shared['link'] = $session['p_link'];
+               }
 
-                                               //With updated event
-                                               if ($session['updated'] >= (new \DateTime($event->getUpdated()))) {
-                                                       //Set summary
-                                                       $event->setSummary($session['au_pseudonym'].' '.$this->translator->trans('at '.$session['l_title']));
+               //Add profile when available
+               if (!empty($session['p_profile'])) {
+                       $description .= "\n\n".'Réseau social :'."\n".$session['p_profile'];
+                       $shared['profile'] = $session['p_profile'];
+               }
 
-                                                       //Set description
-                                                       $event->setDescription($description);
+               //Set properties
+               $properties = new EventExtendedProperties(
+                       [
+                               //Set private property
+                               'private' => $private,
+                               //Set shared property
+                               'shared' => $shared
+                       ]
+               );
 
-                                                       //Set status
-                                                       $event->setStatus(empty($session['a_canceled'])?'confirmed':'cancelled');
+               //Without event
+               if ($event === null) {
+                       //Init event
+                       $event = new Event(
+                               [
+                                       //Id must match /^[a-v0-9]{5,}$/
+                                       //XXX: see https://developers.google.com/calendar/api/v3/reference/events/insert#id
+                                       'id' => $this->prefix.$session['id'],
+                                       'summary' => $source->getTitle(),
+                                       'description' => $description,
+                                       'status' => empty($session['a_canceled'])?'confirmed':'cancelled',
+                                       'location' => implode(' ', [$session['l_address'], $session['l_zipcode'], $session['l_city']]),
+                                       'source' => $source,
+                                       'extendedProperties' => $properties,
+                                       //TODO: colorId ?
+                                       //TODO: attendees[] ?
+                                       'start' => [
+                                               'dateTime' => $session['start']->format(\DateTime::ISO8601)
+                                       ],
+                                       'end' => [
+                                               'dateTime' => $session['stop']->format(\DateTime::ISO8601)
+                                       ]
+                               ]
+                       );
+               //With event
+               } else {
+                       //Set summary
+                       $event->setSummary($source->getTitle());
 
-                                                       //Set location
-                                                       $event->setLocation(implode(' ', [$session['l_address'], $session['l_zipcode'], $session['l_city']]));
+                       //Set description
+                       $event->setDescription($description);
 
-                                                       //Get source
-                                                       $eventSource = $event->getSource();
+                       //Set status
+                       $event->setStatus(empty($session['a_canceled'])?'confirmed':'cancelled');
 
-                                                       //Update source title
-                                                       $eventSource->setTitle($source['title']);
+                       //Set location
+                       $event->setLocation(implode(' ', [$session['l_address'], $session['l_zipcode'], $session['l_city']]));
 
-                                                       //Update source url
-                                                       $eventSource->setUrl($source['url']);
+                       //Get source
+                       #$eventSource = $event->getSource();
 
-                                                       //Set source
-                                                       #$event->setSource($source);
+                       //Update source title
+                       #$eventSource->setTitle($source->getTitle());
 
-                                                       //Get extended properties
-                                                       $extendedProperties = $event->getExtendedProperties();
+                       //Update source url
+                       #$eventSource->setUrl($source->getUrl());
 
-                                                       //Update shared
-                                                       $extendedProperties->setShared($shared);
+                       //Set source
+                       $event->setSource($source);
 
-                                                       //TODO: colorId ?
-                                                       //TODO: attendees[] ?
+                       //Get extended properties
+                       #$extendedProperties = $event->getExtendedProperties();
 
-                                                       //Set start
-                                                       $start = $event->getStart();
+                       //Update private
+                       #$extendedProperties->setPrivate($properties->getPrivate());
 
-                                                       //Update start datetime
-                                                       $start->setDateTime($session['start']->format(\DateTime::ISO8601));
+                       //Update shared
+                       #$extendedProperties->setShared($properties->getShared());
 
-                                                       //Set end
-                                                       $end = $event->getEnd();
+                       //Set properties
+                       $event->setExtendedProperties($properties);
 
-                                                       //Update stop datetime
-                                                       $end->setDateTime($session['stop']->format(\DateTime::ISO8601));
+                       //TODO: colorId ?
+                       //TODO: attendees[] ?
 
-                                                       try {
-                                                               //Update the event
-                                                               $updatedEvent = $googleCalendar->events->update($token['calendar'], $event->getId(), $event);
-                                                       //Catch exception
-                                                       } catch(\Google\Service\Exception $e) {
-                                                               //Display exception
-                                                               //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
-                                                               echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
-                                                               echo $e->getTraceAsString()."\n";
+                       //Set start
+                       $start = $event->getStart();
 
-                                                               //Return failure
-                                                               return self::FAILURE;
-                                                       }
-                                               }
+                       //Update start datetime
+                       $start->setDateTime($session['start']->format(\DateTime::ISO8601));
 
-                                               //Drop from events array
-                                               unset($events[$sessionId]);
-                                       }
-                               }
+                       //Set end
+                       $end = $event->getEnd();
 
-                               //Remaining events to drop
-                               foreach($events as $eventId => $event) {
-                                       //Non canceled events
-                                       if ($event->getStatus() == 'confirmed') {
-                                               try {
-                                                       //Delete the event
-                                                       $googleCalendar->events->delete($token['calendar'], $event->getId());
-                                               //Catch exception
-                                               } catch(\Google\Service\Exception $e) {
-                                                       //Display exception
-                                                       //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
-                                                       echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
-                                                       echo $e->getTraceAsString()."\n";
-
-                                                       //Return failure
-                                                       return self::FAILURE;
-                                               }
-                                       }
-                               }
-                       }
+                       //Update stop datetime
+                       $end->setDateTime($session['stop']->format(\DateTime::ISO8601));
                }
 
-               //Return success
-               return self::SUCCESS;
+               //Return event
+               return $event;
        }
 
        /**
-        * Return the bundle alias
+        * Insert event
         *
-        * {@inheritdoc}
+        * @param string $calendar The calendar mail
+        * @param array $session The session instance
+        * @return void
         */
-       public function getAlias(): string {
-               return 'rapsys_air';
+       function insert(string $calendar, array $session): void {
+               //Get event
+               $event = $this->fill($session);
+
+               //Get cache events
+               $cacheEvents = $this->item->get();
+
+               //Insert in cache event
+               $cacheEvents[$session['id']] = $this->service->events->insert($calendar, $event);
+
+               //Set cache events
+               $this->item->set($cacheEvents);
+
+               //Save cache item
+               $this->cache->saveDeferred($this->item);
+       }
+
+       /**
+        * Update event
+        *
+        * @param string $calendar The calendar mail
+        * @param Event $event The google event instance
+        * @param array $session The session instance
+        * @return int The session id
+        */
+       function update(string $calendar, Event $event, array $session): int {
+               //Get event
+               $event = $this->fill($session, $event);
+
+               //Get cache events
+               $cacheEvents = $this->item->get();
+
+               //Update in cache events
+               $cacheEvents[$session['id']] = $this->service->events->update($calendar, $event->getId(), $event);
+
+               //Set cache events
+               $this->item->set($cacheEvents);
+
+               //Save cache item
+               $this->cache->saveDeferred($this->item);
+
+               //Return session id
+               return $session['id'];
        }
 }
index 91d7967de249a60beb2449c992dd2f03f6291261..4ad8dfcd14ffb0593016857ee9a585b55131b3c0 100644 (file)
@@ -1,4 +1,13 @@
-<?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\Command;
 
@@ -6,16 +15,16 @@ use Doctrine\Bundle\DoctrineBundle\Command\DoctrineCommand;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
 
+use Rapsys\AirBundle\Command;
 use Rapsys\AirBundle\Entity\Session;
 
+/**
+ * {@inheritdoc}
+ */
 class RekeyCommand extends DoctrineCommand {
-       //Set failure constant
-       const FAILURE = 1;
-
-       ///Set success constant
-       const SUCCESS = 0;
-
-       ///Configure attribute command
+       /**
+        * Configure attribute command
+        */
        protected function configure() {
                //Configure the class
                $this
@@ -27,8 +36,10 @@ class RekeyCommand extends DoctrineCommand {
                        ->setHelp('This command rekey sessions in chronological order');
        }
 
-       ///Process the attribution
-       protected function execute(InputInterface $input, OutputInterface $output) {
+       /**
+        * Process the attribution
+        */
+       protected function execute(InputInterface $input, OutputInterface $output): int {
                //Fetch doctrine
                $doctrine = $this->getDoctrine();
 
@@ -41,13 +52,4 @@ class RekeyCommand extends DoctrineCommand {
                //Return success
                return self::SUCCESS;
        }
-
-       /**
-        * Return the bundle alias
-        *
-        * {@inheritdoc}
-        */
-       public function getAlias(): string {
-               return 'rapsys_air';
-       }
 }
index c2aae61dc4451ffcb5a8e0e3b1411745bd40168d..b499390e0912b2f135a08acdf07530b1bb75f588 100644 (file)
@@ -1,12 +1,24 @@
-<?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\Command;
 
 use Doctrine\Bundle\DoctrineBundle\Command\DoctrineCommand;
+use Doctrine\Persistence\ManagerRegistry;
+
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
+use Symfony\Component\Filesystem\Exception\IOException;
 use Symfony\Component\Filesystem\Filesystem;
+
 use Rapsys\AirBundle\Entity\Session;
 
 class WeatherCommand extends DoctrineCommand {
@@ -35,8 +47,11 @@ class WeatherCommand extends DoctrineCommand {
                        75001 => 'https://www.accuweather.com/en/fr/paris-01-louvre/75001/hourly-weather-forecast/179142_pc?day=',
                        75004 => 'https://www.accuweather.com/en/fr/paris-04-hotel-de-ville/75004/hourly-weather-forecast/179145_pc?day=',
                        75005 => 'https://www.accuweather.com/en/fr/paris-05-pantheon/75005/hourly-weather-forecast/179146_pc?day=',
+                       75006 => 'https://www.accuweather.com/fr/fr/paris-06-luxembourg/75006/hourly-weather-forecast/179147_pc?day=',
                        75007 => 'https://www.accuweather.com/en/fr/paris-07-palais-bourbon/75007/hourly-weather-forecast/179148_pc?day=',
                        75009 => 'https://www.accuweather.com/en/fr/paris-09-opera/75009/hourly-weather-forecast/179150_pc?day=',
+                       75010 => 'https://www.accuweather.com/en/fr/paris-10-entrepot/75010/hourly-weather-forecast/179151_pc?day=',
+                       75012 => 'https://www.accuweather.com/en/fr/paris-12-reuilly/75012/hourly-weather-forecast/179153_pc?day=',
                        75013 => 'https://www.accuweather.com/en/fr/paris-13-gobelins/75013/hourly-weather-forecast/179154_pc?day=',
                        75015 => 'https://www.accuweather.com/en/fr/paris-15-vaugirard/75015/hourly-weather-forecast/179156_pc?day=',
                        75019 => 'https://www.accuweather.com/en/fr/paris-19-buttes-chaumont/75019/hourly-weather-forecast/179160_pc?day=',
@@ -47,8 +62,11 @@ class WeatherCommand extends DoctrineCommand {
                        75001 => 'https://www.accuweather.com/en/fr/paris-01-louvre/75001/daily-weather-forecast/179142_pc',
                        75004 => 'https://www.accuweather.com/en/fr/paris-04-hotel-de-ville/75004/daily-weather-forecast/179145_pc',
                        75005 => 'https://www.accuweather.com/en/fr/paris-05-pantheon/75005/daily-weather-forecast/179146_pc',
+                       75006 => 'https://www.accuweather.com/fr/fr/paris-06-luxembourg/75006/daily-weather-forecast/179147_pc',
                        75007 => 'https://www.accuweather.com/en/fr/paris-07-palais-bourbon/75007/daily-weather-forecast/179148_pc',
                        75009 => 'https://www.accuweather.com/en/fr/paris-09-opera/75009/daily-weather-forecast/179150_pc',
+                       75010 => 'https://www.accuweather.com/en/fr/paris-10-entrepot/75010/daily-weather-forecast/179151_pc',
+                       75012 => 'https://www.accuweather.com/en/fr/paris-12-reuilly/75012/daily-weather-forecast/179153_pc',
                        75013 => 'https://www.accuweather.com/en/fr/paris-13-gobelins/75013/daily-weather-forecast/179154_pc',
                        75015 => 'https://www.accuweather.com/en/fr/paris-15-vaugirard/75015/daily-weather-forecast/179156_pc',
                        75019 => 'https://www.accuweather.com/en/fr/paris-19-buttes-chaumont/75019/daily-weather-forecast/179160_pc',
@@ -59,6 +77,23 @@ class WeatherCommand extends DoctrineCommand {
        ///Set curl handler
        private $ch = null;
 
+       ///Set manager registry
+       private $doctrine;
+
+       ///Set filesystem
+       private $filesystem;
+
+       ///Weather command constructor
+       public function __construct(ManagerRegistry $doctrine, Filesystem $filesystem) {
+               parent::__construct($doctrine);
+
+               //Set entity manager
+               $this->doctrine = $doctrine;
+
+               //Set filesystem
+               $this->filesystem = $filesystem;
+       }
+
        ///Configure attribute command
        protected function configure() {
                //Configure the class
@@ -74,12 +109,30 @@ class WeatherCommand extends DoctrineCommand {
        }
 
        ///Process the attribution
-       protected function execute(InputInterface $input, OutputInterface $output) {
-               //Fetch doctrine
-               $doctrine = $this->getDoctrine();
+       protected function execute(InputInterface $input, OutputInterface $output): int {
+               //Kernel object
+               $kernel = $this->getApplication()->getKernel();
 
-               //Get manager
-               $manager = $doctrine->getManager();
+               //Tmp directory
+               $tmpdir = $kernel->getContainer()->getParameter('kernel.project_dir').'/var/cache/weather';
+
+               //Set tmpdir
+               //XXX: worst case scenario we have 3 files per zipcode plus daily
+               if (!is_dir($tmpdir)) {
+                       try {
+                               //Create dir
+                               $this->filesystem->mkdir($tmpdir, 0775);
+                       } catch (IOException $exception) {
+                               //Display error
+                               echo 'Create dir '.$exception->getPath().' failed'."\n";
+
+                               //Exit with failure
+                               exit(self::FAILURE);
+                       }
+               }
+
+               //Cleanup kernel
+               unset($kernel);
 
                //Tidy object
                $tidy = new \tidy();
@@ -93,7 +146,7 @@ class WeatherCommand extends DoctrineCommand {
                //Process hourly accuweather
                if (($command = $input->getFirstArgument()) == 'rapsysair:weather:hourly' || $command == 'rapsysair:weather') {
                        //Fetch hourly sessions to attribute
-                       $types['hourly'] = $doctrine->getRepository(Session::class)->findAllPendingHourlyWeather();
+                       $types['hourly'] = $this->doctrine->getRepository(Session::class)->findAllPendingHourlyWeather();
 
                        //Iterate on each session
                        foreach($types['hourly'] as $sessionId => $session) {
@@ -136,7 +189,7 @@ class WeatherCommand extends DoctrineCommand {
                //Process daily accuweather
                if ($command == 'rapsysair:weather:daily' || $command == 'rapsysair:weather') {
                        //Fetch daily sessions to attribute
-                       $types['daily'] = $doctrine->getRepository(Session::class)->findAllPendingDailyWeather();
+                       $types['daily'] = $this->doctrine->getRepository(Session::class)->findAllPendingDailyWeather();
 
                        //Iterate on each session
                        foreach($types['daily'] as $sessionId => $session) {
@@ -163,24 +216,6 @@ class WeatherCommand extends DoctrineCommand {
                        }
                }
 
-               //Get filesystem
-               $filesystem = new Filesystem();
-
-               //Set tmpdir
-               //XXX: worst case scenario we have 3 files per zipcode
-               if (!is_dir($tmpdir = sys_get_temp_dir().'/accuweather')) {
-                       try {
-                               //Create dir
-                           $filesystem->mkdir($tmpdir, 0775);
-                       } catch (IOExceptionInterface $exception) {
-                               //Display error
-                               echo 'Create dir '.$exception->getPath().' failed'."\n";
-
-                               //Exit with failure
-                               exit(self::FAILURE);
-                       }
-               }
-
                //Init curl
                $this->curl_init();
 
@@ -223,20 +258,20 @@ class WeatherCommand extends DoctrineCommand {
 
                                //Load simplexml
                                //XXX: trash all xmlns= broken tags
-                               $sx = new \SimpleXMLElement(str_replace(['xmlns=', 'xlink:href='], ['xns=', 'href='], $tidy));
+                               $sx = new \SimpleXMLElement(str_replace(['xmlns=', 'xlink:href='], ['xns=', 'href='], (string)$tidy));
 
                                //Process daily
                                if ($day == 'daily') {
                                        //Iterate on each link containing data
-                                       foreach($sx->xpath('//a[@class="daily-forecast-card"]') as $node) {
+                                       foreach($sx->xpath('//a[contains(@class,"daily-forecast-card")]') as $node) {
                                                //Get date
-                                               $dsm = trim($node->div[0]->h2[0]->span[1]);
+                                               $dsm = trim((string)$node->div[0]->h2[0]->span[1]);
 
                                                //Get temperature
-                                               $temperature = str_replace('°', '', $node->div[0]->div[0]->span[0]);
+                                               $temperature = str_replace('°', '', (string)$node->div[0]->div[0]->span[0]);
 
                                                //Get rainrisk
-                                               $rainrisk = str_replace('%', '', trim($node->div[2]))/100;
+                                               $rainrisk = trim(str_replace('%', '', (string)$node->div[1]))/100;
 
                                                //Store data
                                                $data[$zipcode][$dsm]['daily'] = [
@@ -251,29 +286,30 @@ class WeatherCommand extends DoctrineCommand {
                                        #/html/body/div[1]/div[5]/div[1]/div[1]/div[1]/div[1]/div[1]/div/h2/span[1]
                                        foreach($sx->xpath('//div[@data-shared="false"]') as $node) {
                                                //Get hour
-                                               $hour = trim($node->div[0]->div[0]->h2[0]->span[0]);
+                                               $hour = trim(str_replace(' h', '', (string)$node->div[0]->div[0]->div[0]->div[0]->div[0]->h2[0]));
 
-                                               //Get dsm
-                                               $dsm = trim($node->div[0]->div[0]->h2[0]->span[1]);
+                                               //Compute dsm from day (1=d,2=d+1,3=d+2)
+                                               $dsm = (new \DateTime('+'.($day - 1).' day'))->format('d/m');
 
                                                //Get temperature
-                                               $temperature = str_replace('°', '', $node->div[0]->div[0]->div[0]);
+                                               $temperature = str_replace('°', '', (string)$node->div[0]->div[0]->div[0]->div[0]->div[1]);
 
                                                //Get realfeel
-                                               $realfeel = str_replace(['RealFeel® ', '°'], '', trim($node->div[0]->div[0]->span[0]));
+                                               $realfeel = trim(str_replace(['RealFeel®', '°'], '', (string)$node->div[0]->div[0]->div[0]->div[1]->div[0]->div[0]->div[0]));
 
                                                //Get rainrisk
-                                               $rainrisk = str_replace('%', '', trim($node->div[0]->div[0]->div[1]))/100;
+                                               $rainrisk = floatval(str_replace('%', '', trim((string)$node->div[0]->div[0]->div[0]->div[2]->div[0]))/100);
 
                                                //Set rainfall to 0 (mm)
                                                $rainfall = 0;
 
                                                //Iterate on each entry
-                                               foreach($node->div[1]->div[0]->div[0]->div[1]->p as $p) {
+                                               //TODO: wind and other infos are present in $node->div[1]->div[0]->div[1]->div[0]->p
+                                               foreach($node->div[1]->div[0]->div[1]->div[0]->p as $p) {
                                                        //Lookup for rain entry if present
-                                                       if (trim($p) == 'Rain') {
+                                                       if (in_array(trim((string)$p), ['Rain', 'Pluie'])) {
                                                                //Get rainfall
-                                                               $rainfall = floatval(str_replace(' mm', '', $p->span[0]));
+                                                               $rainfall = floatval(str_replace(' mm', '', (string)$p->span[0]));
                                                        }
                                                }
 
@@ -417,7 +453,6 @@ class WeatherCommand extends DoctrineCommand {
                                        //Check if realfeel differ
                                        if ($session->getRealfeel() !== $realfeel) {
                                                //Set average realfeel
-                                               #$meteo['realfeel'] = array_sum($meteo['realfeel'])/count($meteo['realfeel']);
                                                $session->setRealfeel($realfeel);
                                        }
 
@@ -442,7 +477,6 @@ class WeatherCommand extends DoctrineCommand {
                                        //Check if temperature differ
                                        if ($session->getTemperature() !== $temperature) {
                                                //Set average temperature
-                                               #$meteo['temperature'] = array_sum($meteo['temperature'])/count($meteo['temperature']);
                                                $session->setTemperature($temperature);
                                        }
 
@@ -462,7 +496,7 @@ class WeatherCommand extends DoctrineCommand {
                }
 
                //Flush to get the ids
-               $manager->flush();
+               $this->doctrine->getManager()->flush();
 
                //Close curl handler
                $this->curl_close();
index 05a928d77ebbc295824b293a8e372cc9b6aa76e3..73e84f63993fa7e27567493da5ebe4d1bab067fd 100644 (file)
 
 namespace Rapsys\AirBundle\Controller;
 
+use Doctrine\ORM\EntityManagerInterface;
 use Doctrine\Persistence\ManagerRegistry;
+
+use Psr\Container\ContainerInterface;
 use Psr\Log\LoggerInterface;
+
 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as BaseAbstractController;
-use Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait;
+use Symfony\Bundle\SecurityBundle\Security;
 use Symfony\Component\Asset\PackageInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
 use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\Form\FormFactoryInterface;
+use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\RequestStack;
 use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Mailer\MailerInterface;
 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 use Symfony\Component\Routing\RouterInterface;
-use Symfony\Component\Translation\TranslatorInterface;
+use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
+use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
 use Symfony\Contracts\Service\ServiceSubscriberInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
+
+use Twig\Environment;
 
 use Rapsys\AirBundle\Entity\Dance;
 use Rapsys\AirBundle\Entity\Location;
@@ -32,62 +42,137 @@ use Rapsys\AirBundle\Entity\Slot;
 use Rapsys\AirBundle\Entity\User;
 use Rapsys\AirBundle\RapsysAirBundle;
 
+use Rapsys\PackBundle\Util\FacebookUtil;
+use Rapsys\PackBundle\Util\ImageUtil;
+use Rapsys\PackBundle\Util\MapUtil;
+use Rapsys\PackBundle\Util\SluggerUtil;
+
 /**
  * Provides common features needed in controllers.
  *
  * {@inheritdoc}
  */
 abstract class AbstractController extends BaseAbstractController implements ServiceSubscriberInterface {
-       use ControllerTrait {
-               //Rename render as baseRender
-               render as protected baseRender;
-       }
+       /**
+        * Config array
+        */
+       protected array $config;
 
-       ///Config array
-       protected $config;
+       /**
+        * Context array
+        */
+       protected array $context;
 
-       ///ContainerInterface instance
-       protected $container;
+       /**
+        * Locale string
+        */
+       protected string $locale;
 
-       ///Context array
-       protected $context;
+       /**
+        * Modified DateTime
+        */
+       protected \DateTime $modified;
+
+       /**
+        * DatePeriod instance
+        */
+       protected \DatePeriod $period;
 
-       ///Router instance
-       protected $router;
+       /**
+        * Request instance
+        */
+       protected Request $request;
 
-       ///Translator instance
-       protected $translator;
+       /**
+        * Route string
+        */
+       protected string $route;
 
        /**
-        * Common constructor
-        *
-        * Stores container, router and translator interfaces
-        * Stores config
-        * Prepares context tree
+        * Route params array
+        */
+       protected array $routeParams;
+
+       /**
+        * Abstract constructor
         *
+        * @param AuthorizationCheckerInterface $checker The container instance
         * @param ContainerInterface $container The container instance
+        * @param AccessDecisionManagerInterface $decision The decision instance
+        * @param ManagerRegistry $doctrine The doctrine instance
+        * @param FacebookUtil $facebook The facebook instance
+        * @param FormFactoryInterface $factory The factory instance
+        * @param ImageUtil $image The image instance
+        * @param MailerInterface $mailer The mailer instance
+        * @param EntityManagerInterface $manager The manager instance
+        * @param MapUtil $map The map instance
+        * @param PackageInterface $package The package instance
+        * @param RouterInterface $router The router instance
+        * @param Security $security The security instance
+        * @param SluggerUtil $slugger The slugger instance
+        * @param RequestStack $stack The stack instance
+        * @param TranslatorInterface $translator The translator instance
+        * @param Environment $twig The twig environment instance
+        *
+        * @TODO move all that stuff to setSlugger('@slugger') setters with a calls: [ setSlugger: [ '@slugger' ] ] to unbload classes ???
+        * @TODO add a calls: [ ..., prepare: ['@???'] ] that do all the logic that can't be done in constructor because various things are not available
         */
-       public function __construct(ContainerInterface $container) {
+       public function __construct(protected AuthorizationCheckerInterface $checker, protected ContainerInterface $container, protected AccessDecisionManagerInterface $decision, protected ManagerRegistry $doctrine, protected FacebookUtil $facebook, protected FormFactoryInterface $factory, protected ImageUtil $image, protected MailerInterface $mailer, protected EntityManagerInterface $manager, protected MapUtil $map, protected PackageInterface $package, protected RouterInterface $router, protected Security $security, protected SluggerUtil $slugger, protected RequestStack $stack, protected TranslatorInterface $translator, protected Environment $twig, protected int $limit = 5) {
                //Retrieve config
                $this->config = $container->getParameter(RapsysAirBundle::getAlias());
 
-               //Set the container
-               $this->container = $container;
+               //Set period
+               $this->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
+                       //XXX: we can't use isGranted here as AuthenticatedVoter deny access because user is likely not authenticated yet :'(
+                       new \DateTime('Monday this week + 2 week')
+               );
+
+               //Get main request
+               $this->request = $this->stack->getMainRequest();
+
+               //Get current locale
+               $this->locale = $this->request->getLocale();
+
+               //Set canonical
+               $canonical = null;
+
+               //Set alternates
+               $alternates = [];
+
+               //Set route
+               //TODO: default to not found route ???
+               //TODO: pour une url not found, cet attribut n'est pas défini, comment on fait ???
+               //XXX: on génère une route bidon par défaut ???
+               $this->route = $this->request->attributes->get('_route');
 
-               //Set the router
-               $this->router = $container->get('router');
+               //Set route params
+               $this->routeParams = $this->request->attributes->get('_route_params');
 
-               //Set the translator
-               $this->translator = $container->get('translator');
+               //With route and routeParams
+               if ($this->route !== null && $this->routeParams !== null) {
+                       //Set canonical
+                       $canonical = $this->router->generate($this->route, $this->routeParams, UrlGeneratorInterface::ABSOLUTE_URL);
+
+                       //Set alternates
+                       $alternates = [
+                               substr($this->locale, 0, 2) => [
+                                       'absolute' => $canonical
+                               ]
+                       ];
+               }
 
                //Set the context
                $this->context = [
-                       'description' => null,
-                       'section' => null,
-                       'title' => null,
+                       'alternates' => $alternates,
+                       'canonical' => $canonical,
                        'contact' => [
-                               'title' => $this->translator->trans($this->config['contact']['title']),
-                               'mail' => $this->config['contact']['mail']
+                               'address' => $this->config['contact']['address'],
+                               'name' => $this->translator->trans($this->config['contact']['name'])
                        ],
                        'copy' => [
                                'by' => $this->translator->trans($this->config['copy']['by']),
@@ -96,436 +181,70 @@ abstract class AbstractController extends BaseAbstractController implements Serv
                                'short' => $this->translator->trans($this->config['copy']['short']),
                                'title' => $this->config['copy']['title']
                        ],
-                       'site' => [
-                               'donate' => $this->config['site']['donate'],
-                               'ico' => $this->config['site']['ico'],
-                               'logo' => $this->config['site']['logo'],
-                               'png' => $this->config['site']['png'],
-                               'svg' => $this->config['site']['svg'],
-                               'title' => $this->translator->trans($this->config['site']['title']),
-                               'url' => $this->router->generate($this->config['site']['url'])
-                       ],
-                       'canonical' => null,
-                       'alternates' => [],
+                       'description' => null,
+                       'donate' => $this->config['donate'],
                        'facebook' => [
-                               'prefixes' => [
-                                       'og' => 'http://ogp.me/ns#',
-                                       'fb' => 'http://ogp.me/ns/fb#'
-                               ],
-                               'metas' => [
-                                       'og:type' => 'article',
-                                       'og:site_name' => $this->translator->trans($this->config['site']['title']),
-                                       #'fb:admins' => $this->config['facebook']['admins'],
-                                       'fb:app_id' => $this->config['facebook']['apps']
-                               ],
-                               'texts' => []
+                               'og:type' => 'article',
+                               'og:site_name' => $title = $this->translator->trans($this->config['title']),
+                               'og:url' => $canonical,
+                               #'fb:admins' => $this->config['facebook']['admins'],
+                               'fb:app_id' => $this->config['facebook']['apps']
+                       ],
+                       //XXX: TODO: only generate it when fb robot request the url ???
+                       'fbimage' => [
+                               'texts' => [
+                                       $title => [
+                                               'font' => 'irishgrover',
+                                               'size' => 110
+                                       ]
+                               ]
                        ],
-                       'forms' => []
+                       'icon' => $this->config['icon'],
+                       'keywords' => null,
+                       'locale' => str_replace('_', '-', $this->locale),
+                       'logo' => $this->config['logo'],
+                       'forms' => [],
+                       'root' => $this->router->generate($this->config['root']),
+                       'title' => [
+                               'page' => null,
+                               'section' => null,
+                               'site' => $title
+                       ]
                ];
        }
 
-       /**
-        * Return the facebook image
-        *
-        * @desc Generate image in jpeg format or load it from cache
-        *
-        * @param string $pathInfo The request path info
-        * @param array $parameters The image parameters
-        * @return array The image array
-        */
-       protected function getFacebookImage(string $pathInfo, array $parameters = []): array {
-               //Get asset package
-               //XXX: require asset package to be public
-               $package = $this->container->get('rapsys_pack.path_package');
-
-               //Set texts
-               $texts = $parameters['texts'] ?? [];
-
-               //Set default source
-               $source = $parameters['source'] ?? 'png/facebook.png';
-
-               //Set default source
-               $updated = $parameters['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
-               //XXX: format <public>/facebook<pathinfo>.jpeg
-               //XXX: was <public>/facebook/<controller>/<action>.<locale>.jpeg
-               $dest = $this->config['path']['public'].'/facebook'.$pathInfo.'.jpeg';
-
-               //With up to date generated image
-               if (
-                       is_file($dest) &&
-                       ($stat = stat($dest)) &&
-                       $stat['mtime'] >= $updated
-               ) {
-                       //Get image size
-                       list ($width, $height) = getimagesize($dest);
-
-                       //Iterate each text
-                       foreach($texts as $text => $data) {
-                               //With canonical text
-                               if (!empty($data['canonical'])) {
-                                       //Prevent canonical to finish in alt
-                                       unset($texts[$text]);
-                               }
-                       }
-
-                       //Return image data
-                       return [
-                               'og:image' => $package->getAbsoluteUrl('@RapsysAir/facebook/'.$stat['mtime'].$pathInfo.'.jpeg'),
-                               'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
-                               'og:image:height' => $height,
-                               'og: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']);
-
-                               //Strip image exif data and properties
-                               $image->stripImage();
-
-                               //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();
-
-                               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);
-                               }
-                       }
-
-                       //Get image width
-                       $width = $image->getImageWidth();
-
-                       //Get image height
-                       $height = $image->getImageHeight();
-
-                       //Create draw
-                       $draw = new \ImagickDraw();
-
-                       //Set stroke antialias
-                       $draw->setStrokeAntialias(true);
-
-                       //Set text antialias
-                       $draw->setTextAntialias(true);
-
-                       //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 align aliases
-                       $aligns = [
-                               'left' => \Imagick::ALIGN_LEFT,
-                               'center' => \Imagick::ALIGN_CENTER,
-                               'right' => \Imagick::ALIGN_RIGHT
-                       ];
-
-                       //Set default font
-                       $defaultFont = 'dejavusans';
-
-                       //Set default align
-                       $defaultAlign = 'center';
-
-                       //Set default size
-                       $defaultSize = 60;
-
-                       //Set default stroke
-                       $defaultStroke = '#00c3f9';
-
-                       //Set default width
-                       $defaultWidth = 15;
-
-                       //Set default fill
-                       $defaultFill = 'white';
-
-                       //Init counter
-                       $i = 1;
-
-                       //Set text count
-                       $count = count($texts);
-
-                       //Draw each text stroke
-                       foreach($texts as $text => $data) {
-                               //Set font
-                               $draw->setFont($fonts[$data['font']??$defaultFont]);
-
-                               //Set font size
-                               $draw->setFontSize($data['size']??$defaultSize);
-
-                               //Set stroke width
-                               $draw->setStrokeWidth($data['width']??$defaultWidth);
-
-                               //Set text alignment
-                               $draw->setTextAlignment($align = ($aligns[$data['align']??$defaultAlign]));
-
-                               //Get font metrics
-                               $metrics = $image->queryFontMetrics($draw, $text);
-
-                               //Without y
-                               if (empty($data['y'])) {
-                                       //Position verticaly each text evenly
-                                       $texts[$text]['y'] = $data['y'] = (($height + 100) / (count($texts) + 1) * $i) - 50;
-                               }
-
-                               //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;
-                                       }
-                               }
-
-                               //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 stroke color
-                               $draw->setStrokeColor(new \ImagickPixel($data['stroke']??$defaultStroke));
-
-                               //Set fill color
-                               $draw->setFillColor(new \ImagickPixel($data['stroke']??$defaultStroke));
-
-                               //Add annotation
-                               $draw->annotation($data['x'], $data['y'], $text);
-
-                               //Increase counter
-                               $i++;
-                       }
-
-                       //Create stroke object
-                       $stroke = new \Imagick();
-
-                       //Add new image
-                       $stroke->newImage($width, $height, new \ImagickPixel('transparent'));
-
-                       //Draw on image
-                       $stroke->drawImage($draw);
-
-                       //Blur image
-                       //XXX: blur the stroke canvas only
-                       $stroke->blurImage(5,3);
-
-                       //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);
-
-                       //Compose image
-                       $image->compositeImage($stroke, \Imagick::COMPOSITE_OVER, 0, 0);
-
-                       //Clear stroke
-                       $stroke->clear();
-
-                       //Destroy stroke
-                       unset($stroke);
-
-                       //Clear draw
-                       $draw->clear();
-
-                       //Set text antialias
-                       $draw->setTextAntialias(true);
-
-                       //Draw each text
-                       foreach($texts as $text => $data) {
-                               //Set font
-                               $draw->setFont($fonts[$data['font']??$defaultFont]);
-
-                               //Set font size
-                               $draw->setFontSize($data['size']??$defaultSize);
-
-                               //Set text alignment
-                               $draw->setTextAlignment($aligns[$data['align']??$defaultAlign]);
-
-                               //Set fill color
-                               $draw->setFillColor(new \ImagickPixel($data['fill']??$defaultFill));
-
-                               //Add annotation
-                               $draw->annotation($data['x'], $data['y'], $text);
-
-                               //With canonical text
-                               if (!empty($data['canonical'])) {
-                                       //Prevent canonical to finish in alt
-                                       unset($texts[$text]);
-                               }
-                       }
-
-                       //Draw on image
-                       $image->drawImage($draw);
-
-                       //Strip image exif data and properties
-                       $image->stripImage();
-
-                       //Set image format
-                       $image->setImageFormat('jpeg');
-
-                       //Save image
-                       if (!$image->writeImage($dest)) {
-                               //Throw error
-                               throw new \Exception(sprintf('Unable to write image "%s"', $dest));
-                       }
-
-                       //Get dest stat
-                       $stat = stat($dest);
-
-                       //Return image data
-                       return [
-                               'og:image' => $package->getAbsoluteUrl('@RapsysAir/facebook/'.$stat['mtime'].$pathInfo.'.jpeg'),
-                               'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
-                               'og:image:height' => $height,
-                               'og:image:width' => $width
-                       ];
-               }
-
-               //Return empty array without image
-               return [];
-       }
-
        /**
         * Renders a view
         *
         * {@inheritdoc}
         */
        protected function render(string $view, array $parameters = [], Response $response = null): Response {
-               //Get request stack
-               $stack = $this->container->get('request_stack');
-
-               //Get current request
-               $request = $stack->getCurrentRequest();
-
-               //Get current locale
-               $locale = $request->getLocale();
-
-               //Set locale
-               $parameters['locale'] = str_replace('_', '-', $locale);
-
-               //Get context path
-               $pathInfo = $this->router->getContext()->getPathInfo();
-
-               //Iterate on locales excluding current one
-               foreach($this->config['locales'] as $current) {
-                       //Set titles
-                       $titles = [];
-
-                       //Iterate on other locales
-                       foreach(array_diff($this->config['locales'], [$current]) as $other) {
-                               $titles[$other] = $this->translator->trans($this->config['languages'][$current], [], null, $other);
-                       }
-
-                       //Retrieve route matching path
-                       $route = $this->router->match($pathInfo);
-
-                       //Get route name
-                       $name = $route['_route'];
-
-                       //Unset route name
-                       unset($route['_route']);
-
-                       //With current locale
-                       if ($current == $locale) {
-                               //Set locale locales context
-                               $parameters['canonical'] = $this->router->generate($name, ['_locale' => $current]+$route, UrlGeneratorInterface::ABSOLUTE_URL);
-                       } else {
-                               //Set locale locales context
-                               $parameters['alternates'][str_replace('_', '-', $current)] = [
-                                       'absolute' => $this->router->generate($name, ['_locale' => $current]+$route, UrlGeneratorInterface::ABSOLUTE_URL),
-                                       'relative' => $this->router->generate($name, ['_locale' => $current]+$route),
-                                       'title' => implode('/', $titles),
-                                       'translated' => $this->translator->trans($this->config['languages'][$current], [], null, $current)
-                               ];
-                       }
-
-                       //Add shorter locale
-                       if (empty($parameters['alternates'][$shortCurrent = substr($current, 0, 2)])) {
-                               //Set locale locales context
-                               $parameters['alternates'][$shortCurrent] = [
-                                       'absolute' => $this->router->generate($name, ['_locale' => $current]+$route, UrlGeneratorInterface::ABSOLUTE_URL),
-                                       'relative' => $this->router->generate($name, ['_locale' => $current]+$route),
-                                       'title' => implode('/', $titles),
-                                       'translated' => $this->translator->trans($this->config['languages'][$current], [], null, $current)
-                               ];
-                       }
-               }
+               //Create response when null
+               $response ??= new Response();
 
                //Create application form for role_guest
-               if ($this->isGranted('ROLE_GUEST')) {
+               if ($this->checker->isGranted('ROLE_GUEST')) {
                        //Without application form
                        if (empty($parameters['forms']['application'])) {
-                               //Fetch doctrine
-                               $doctrine = $this->get('doctrine');
-
                                //Get favorites dances
-                               $danceFavorites = $doctrine->getRepository(Dance::class)->findByUserId($this->getUser()->getId());
+                               $danceFavorites = $this->doctrine->getRepository(Dance::class)->findByUserId($this->security->getUser()->getId());
 
                                //Set dance default
                                $danceDefault = !empty($danceFavorites)?current($danceFavorites):null;
 
                                //Get favorites locations
-                               $locationFavorites = $doctrine->getRepository(Location::class)->findByUserId($this->getUser()->getId());
+                               $locationFavorites = $this->doctrine->getRepository(Location::class)->findByUserId($this->security->getUser()->getId());
 
                                //Set location default
                                $locationDefault = !empty($locationFavorites)?current($locationFavorites):null;
 
                                //With admin
-                               if ($this->isGranted('ROLE_ADMIN')) {
+                               if ($this->checker->isGranted('ROLE_ADMIN')) {
                                        //Get dances
-                                       $dances = $doctrine->getRepository(Dance::class)->findAll();
+                                       $dances = $this->doctrine->getRepository(Dance::class)->findAll();
 
                                        //Get locations
-                                       $locations = $doctrine->getRepository(Location::class)->findAll();
+                                       $locations = $this->doctrine->getRepository(Location::class)->findAll();
                                //Without admin
                                } else {
                                        //Restrict to favorite dances
@@ -541,8 +260,24 @@ abstract class AbstractController extends BaseAbstractController implements Serv
                                        $locationFavorites = [];
                                }
 
+                               //With session application dance id
+                               if (!empty($parameters['session']['application']['dance']['id'])) {
+                                       //Iterate on each dance
+                                       foreach($dances as $dance) {
+                                               //Found dance
+                                               if ($dance->getId() == $parameters['session']['application']['dance']['id']) {
+                                                       //Set dance as default
+                                                       $danceDefault = $dance;
+
+                                                       //Stop search
+                                                       break;
+                                               }
+                                       }
+                               }
+
                                //With session location id
                                //XXX: set in session controller
+                               //TODO: with new findAll that key by id, it should be as simple as isset($locations[$id]) ?
                                if (!empty($parameters['session']['location']['id'])) {
                                        //Iterate on each location
                                        foreach($locations as $location) {
@@ -558,9 +293,9 @@ abstract class AbstractController extends BaseAbstractController implements Serv
                                }
 
                                //Create ApplicationType form
-                               $application = $this->createForm('Rapsys\AirBundle\Form\ApplicationType', null, [
+                               $application = $this->factory->create('Rapsys\AirBundle\Form\ApplicationType', null, [
                                        //Set the action
-                                       'action' => $this->generateUrl('rapsys_air_application_add'),
+                                       'action' => $this->generateUrl('rapsysair_application_add'),
                                        //Set the form attribute
                                        'attr' => [ 'class' => 'col' ],
                                        //Set dance choices
@@ -576,25 +311,28 @@ abstract class AbstractController extends BaseAbstractController implements Serv
                                        //Set location favorites
                                        'location_favorites' => $locationFavorites,
                                        //With user
-                                       'user' => $this->isGranted('ROLE_ADMIN'),
+                                       'user' => $this->checker->isGranted('ROLE_ADMIN'),
                                        //Set user choices
-                                       'user_choices' => $doctrine->getRepository(User::class)->findAllWithTranslatedGroupAndCivility($this->translator),
+                                       'user_choices' => $this->doctrine->getRepository(User::class)->findChoicesAsArray(),
                                        //Set default user to current
-                                       'user_default' => $this->getUser()->getId(),
+                                       'user_default' => $this->security->getUser()->getId(),
                                        //Set to session slot or evening by default
                                        //XXX: default to Evening (3)
-                                       'slot_default' => $doctrine->getRepository(Slot::class)->findOneById($parameters['session']['slot']['id']??3)
+                                       'slot_default' => $this->doctrine->getRepository(Slot::class)->findOneById($parameters['session']['slot']['id']??3)
                                ]);
 
                                //Add form to context
                                $parameters['forms']['application'] = $application->createView();
                        }
+               }/*
+               #XXX: removed because it fucks up the seo by displaying register and login form instead of content
+               #XXX: until we find a better way, removed !!!
                //Create login form for anonymous
-               } elseif (!$this->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
+               elseif (!$this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
                        //Create LoginType form
-                       $login = $this->createForm('Rapsys\UserBundle\Form\LoginType', null, [
+                       $login = $this->factory->create('Rapsys\UserBundle\Form\LoginType', null, [
                                //Set the action
-                               'action' => $this->generateUrl('rapsys_user_login'),
+                               'action' => $this->generateUrl('rapsysuser_login'),
                                //Disable password repeated
                                'password_repeated' => false,
                                //Set the form attribute
@@ -624,18 +362,15 @@ abstract class AbstractController extends BaseAbstractController implements Serv
                                'phone' => false
                        ];
 
-                       //Get slugger
-                       $slugger = $this->container->get('rapsys_pack.slugger_util');
-
                        //Create RegisterType form
-                       $register = $this->createForm('Rapsys\AirBundle\Form\RegisterType', null, $field+[
+                       $register = $this->factory->create('Rapsys\AirBundle\Form\RegisterType', null, $field+[
                                //Set the action
                                'action' => $this->generateUrl(
-                                       'rapsys_user_register',
+                                       'rapsysuser_register',
                                        [
-                                               'mail' => $smail = $slugger->short(''),
-                                               'field' => $sfield = $slugger->serialize($field),
-                                               'hash' => $slugger->hash($smail.$sfield)
+                                               'mail' => $smail = $this->slugger->short(''),
+                                               'field' => $sfield = $this->slugger->serialize($field),
+                                               'hash' => $this->slugger->hash($smail.$sfield)
                                        ]
                                ),
                                //Set the form attribute
@@ -644,50 +379,110 @@ abstract class AbstractController extends BaseAbstractController implements Serv
 
                        //Add form to context
                        $parameters['forms']['register'] = $register->createView();
+               }*/
+
+               //Without alternates
+               if (count($parameters['alternates']) <= 1) {
+                       //Set routeParams
+                       $routeParams = $this->routeParams;
+
+                       //Iterate on locales excluding current one
+                       foreach($this->config['locales'] as $locale) {
+                               //With current locale
+                               if ($locale !== $this->locale) {
+                                       //Set titles
+                                       $titles = [];
+
+                                       //Set route params locale
+                                       $routeParams['_locale'] = $locale;
+
+                                       //Iterate on other locales
+                                       foreach(array_diff($this->config['locales'], [$locale]) as $other) {
+                                               //Set other locale title
+                                               $titles[$other] = $this->translator->trans($this->config['languages'][$locale], [], null, $other);
+                                       }
+
+                                       //Set locale locales context
+                                       $parameters['alternates'][str_replace('_', '-', $locale)] = [
+                                               'absolute' => $this->router->generate($this->route, $routeParams, UrlGeneratorInterface::ABSOLUTE_URL),
+                                               'relative' => $this->router->generate($this->route, $routeParams),
+                                               'title' => implode('/', $titles),
+                                               'translated' => $this->translator->trans($this->config['languages'][$locale], [], null, $locale)
+                                       ];
+
+                                       //Add shorter locale
+                                       if (empty($parameters['alternates'][$shortCurrent = substr($locale, 0, 2)])) {
+                                               //Set locale locales context
+                                               $parameters['alternates'][$shortCurrent] = $parameters['alternates'][str_replace('_', '-', $locale)];
+                                       }
+                               }
+                       }
                }
 
                //With page infos and without facebook texts
-               if (empty($parameters['facebook']['texts']) && !empty($parameters['site']['title']) && !empty($parameters['title']) && !empty($parameters['canonical'])) {
-                       //Set facebook image
-                       $parameters['facebook']['texts'] = [
-                               $parameters['site']['title'] => [
-                                       'font' => 'irishgrover',
-                                       'size' => 110
-                               ],
-                               $parameters['title'] => [
-                                       'align' => 'left'
-                               ],
-                               $parameters['canonical'] => [
+               if (count($parameters['fbimage']) <= 1 && isset($parameters['title']) && isset($this->route) && isset($this->routeParams)) {
+                       //Append facebook image texts
+                       $parameters['fbimage'] += [
+                               'texts' => [
+                                       $parameters['title']['page'] => [
+                                               'font' => 'irishgrover',
+                                               'align' => 'left'
+                                       ]/*XXX: same problem as url, too long :'(,
+                                       $parameters['description'] => [
+                                               'align' => 'right',
+                                               'canonical' => true,
+                                               'font' => 'labelleaurore',
+                                               'size' => 50
+                                       ]*/
+                               ]
+                       ];
+
+                       /*With short path info
+                       We don't add this stupid url in image !!!
+                       if (strlen($pathInfo = $this->router->generate($this->route, $this->routeParams)) <= 64) {
+                                => [
                                        'align' => 'right',
                                        'canonical' => true,
                                        'font' => 'labelleaurore',
                                        'size' => 50
-                               ]
-                       ];
+                                ]
+                       }*/
+               }
+
+               //With empty locations link
+               if (empty($parameters['locations_link'])) {
+                       //Set locations link
+                       $parameters['locations_link'] = $this->router->generate('rapsysair_location');
+               }
+
+               //With empty locations title
+               if (empty($parameters['locations_title'])) {
+                       //Set locations title
+                       $parameters['locations_title'] = $this->translator->trans('Locations', [], null, $this->locale);
                }
 
                //With canonical
                if (!empty($parameters['canonical'])) {
                        //Set facebook url
-                       $parameters['facebook']['metas']['og:url'] = $parameters['canonical'];
+                       $parameters['facebook']['og:url'] = $parameters['canonical'];
                }
 
                //With empty facebook title and title
-               if (empty($parameters['facebook']['metas']['og:title']) && !empty($parameters['title'])) {
+               if (empty($parameters['facebook']['og:title']) && !empty($parameters['title'])) {
                        //Set facebook title
-                       $parameters['facebook']['metas']['og:title'] = $parameters['title'];
+                       $parameters['facebook']['og:title'] = $parameters['title'];
                }
 
                //With empty facebook description and description
-               if (empty($parameters['facebook']['metas']['og:description']) && !empty($parameters['description'])) {
+               if (empty($parameters['facebook']['og:description']) && !empty($parameters['description'])) {
                        //Set facebook description
-                       $parameters['facebook']['metas']['og:description'] = $parameters['description'];
+                       $parameters['facebook']['og:description'] = $parameters['description'];
                }
 
                //With locale
-               if (!empty($locale)) {
+               if (!empty($this->locale)) {
                        //Set facebook locale
-                       $parameters['facebook']['metas']['og:locale'] = $locale;
+                       $parameters['facebook']['og:locale'] = $this->locale;
 
                        //With alternates
                        //XXX: locale change when fb_locale=xx_xx is provided is done in FacebookSubscriber
@@ -697,20 +492,36 @@ abstract class AbstractController extends BaseAbstractController implements Serv
                                foreach($parameters['alternates'] as $lang => $alternate) {
                                        if (strlen($lang) == 5) {
                                                //Set facebook locale alternate
-                                               $parameters['facebook']['metas']['og:locale:alternate'] = str_replace('-', '_', $lang);
+                                               $parameters['facebook']['og:locale:alternate'] = str_replace('-', '_', $lang);
                                        }
                                }
                        }
                }
 
                //Without facebook image defined and texts
-               if (empty($parameters['facebook']['metas']['og:image']) && !empty($parameters['facebook']['texts'])) {
+               if (empty($parameters['facebook']['og:image']) && !empty($this->request) && !empty($parameters['fbimage']['texts']) && !empty($this->modified)) {
                        //Get facebook image
-                       $parameters['facebook']['metas'] += $this->getFacebookImage($pathInfo, $parameters['facebook']);
+                       $parameters['facebook'] += $this->facebook->getImage($this->request->getPathInfo(), $parameters['fbimage']['texts'], $this->modified->getTimestamp());
+               }
+
+               //Call twig render method
+               $content = $this->twig->render($view, $parameters);
+
+               //Invalidate OK response on invalid form
+               if (200 === $response->getStatusCode()) {
+                       foreach ($parameters as $v) {
+                               if ($v instanceof FormInterface && $v->isSubmitted() && !$v->isValid()) {
+                                       $response->setStatusCode(422);
+                                       break;
+                               }
+                       }
                }
 
-               //Call parent method
-               return $this->baseRender($view, $parameters, $response);
+               //Store content in response
+               $response->setContent($content);
+
+               //Return response
+               return $response;
        }
 
        /**
@@ -721,11 +532,20 @@ abstract class AbstractController extends BaseAbstractController implements Serv
        public static function getSubscribedServices(): array {
                //Return subscribed services
                return [
-                       //'logger' => LoggerInterface::class,
                        'doctrine' => ManagerRegistry::class,
-                       'rapsys_pack.path_package' => PackageInterface::class,
+                       'doctrine.orm.default_entity_manager' => EntityManagerInterface::class,
+                       'form.factory' => FormFactoryInterface::class,
+                       'mailer.mailer' => MailerInterface::class,
+                       'rapsysair.facebook_util' => FacebookUtil::class,
+                       'rapsyspack.image_util' => ImageUtil::class,
+                       'rapsyspack.map_util' => MapUtil::class,
+                       'rapsyspack.path_package' => PackageInterface::class,
+                       'rapsyspack.slugger_util' => SluggerUtil::class,
+                       'rapsysuser.access_decision_manager' => AccessDecisionManagerInterface::class,
                        'request_stack' => RequestStack::class,
                        'router' => RouterInterface::class,
+                       'security.authorization_checker' => AuthorizationCheckerInterface::class,
+                       'service_container' => ContainerInterface::class,
                        'translator' => TranslatorInterface::class
                ];
        }
index caa190641c1bfa1eb34a2e3c609a37508fafc2d0..f818dbe03991915155273c2d9a7a3249b08482f0 100644 (file)
@@ -11,8 +11,6 @@
 
 namespace Rapsys\AirBundle\Controller;
 
-use Doctrine\Bundle\DoctrineBundle\Registry;
-use Doctrine\ORM\EntityManagerInterface;
 use Doctrine\ORM\NoResultException;
 use Doctrine\ORM\ORMInvalidArgumentException;
 use Symfony\Component\Form\FormError;
@@ -36,7 +34,7 @@ class ApplicationController extends AbstractController {
        /**
         * Add application
         *
-        * @desc Persist application and all required dependencies in database
+        * Persist application and all required dependencies in database
         *
         * @param Request $request The request instance
         * @param Registry $manager The doctrine registry
@@ -46,30 +44,32 @@ class ApplicationController extends AbstractController {
         *
         * @throws \RuntimeException When user has not at least guest role
         */
-       public function add(Request $request, Registry $doctrine, EntityManagerInterface $manager) {
-               //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')]));
+       public function add(Request $request) {
+               //Without guest role
+               if (!$this->checker->isGranted('ROLE_GUEST')) {
+                       //Throw 403
+                       throw $this->createAccessDeniedException($this->translator->trans('Unable to access this page without role %role%!', ['%role%' => $this->translator->trans('Guest')]));
+               }
 
                //Get favorites dances
-               $danceFavorites = $doctrine->getRepository(Dance::class)->findByUserId($this->getUser()->getId());
+               $danceFavorites = $this->doctrine->getRepository(Dance::class)->findByUserId($this->security->getUser()->getId());
 
                //Set dance default
                $danceDefault = !empty($danceFavorites)?current($danceFavorites):null;
 
-
                //Get favorites locations
-               $locationFavorites = $doctrine->getRepository(Location::class)->findByUserId($this->getUser()->getId());
+               $locationFavorites = $this->doctrine->getRepository(Location::class)->findByUserId($this->security->getUser()->getId());
 
                //Set location default
                $locationDefault = !empty($locationFavorites)?current($locationFavorites):null;
 
                //With admin
-               if ($this->isGranted('ROLE_ADMIN')) {
+               if ($this->checker->isGranted('ROLE_ADMIN')) {
                        //Get dances
-                       $dances = $doctrine->getRepository(Dance::class)->findAll();
+                       $dances = $this->doctrine->getRepository(Dance::class)->findAll();
 
                        //Get locations
-                       $locations = $doctrine->getRepository(Location::class)->findAll();
+                       $locations = $this->doctrine->getRepository(Location::class)->findAll();
                //Without admin
                } else {
                        //Restrict to favorite dances
@@ -86,9 +86,9 @@ class ApplicationController extends AbstractController {
                }
 
                //Create ApplicationType form
-               $form = $this->createForm('Rapsys\AirBundle\Form\ApplicationType', null, [
+               $form = $this->factory->create('Rapsys\AirBundle\Form\ApplicationType', null, [
                        //Set the action
-                       'action' => $this->generateUrl('rapsys_air_application_add'),
+                       'action' => $this->generateUrl('rapsysair_application_add'),
                        //Set the form attribute
                        #'attr' => [ 'class' => 'col' ],
                        //Set dance choices
@@ -104,26 +104,32 @@ class ApplicationController extends AbstractController {
                        //Set location favorites
                        'location_favorites' => $locationFavorites,
                        //With user
-                       'user' => $this->isGranted('ROLE_ADMIN'),
+                       'user' => $this->checker->isGranted('ROLE_ADMIN'),
                        //Set user choices
-                       'user_choices' => $doctrine->getRepository(User::class)->findAllWithTranslatedGroupAndCivility($this->translator),
+                       'user_choices' => $this->doctrine->getRepository(User::class)->findChoicesAsArray(),
                        //Set default user to current
-                       'user_default' => $this->getUser()->getId(),
+                       'user_default' => $this->security->getUser()->getId(),
                        //Set default slot to evening
                        //XXX: default to Evening (3)
-                       'slot_default' => $doctrine->getRepository(Slot::class)->findOneByTitle('Evening')
+                       'slot_default' => $this->doctrine->getRepository(Slot::class)->findOneByTitle('Evening')
                ]);
 
+               //Set title
+               $this->context['title']['page'] = $this->translator->trans('Application add');
+
+               //Set section
+               $this->context['title']['section'] = $this->translator->trans('Application');
+
+               //Set description
+               $this->context['description'] = $this->translator->trans('Add an application and session');
+
                //Refill the fields in case of invalid form
                $form->handleRequest($request);
 
                //Handle invalid form
                if (!$form->isSubmitted() || !$form->isValid()) {
-                       //Set title
-                       $title = $this->translator->trans('Application add');
-
                        //Render the view
-                       return $this->render('@RapsysAir/application/add.html.twig', ['title' => $title, 'form' => $form->createView()]+$this->context);
+                       return $this->render('@RapsysAir/application/add.html.twig', ['form' => $form->createView()]+$this->context);
                }
 
                //Get data
@@ -132,14 +138,11 @@ class ApplicationController extends AbstractController {
                //Protect session fetching
                try {
                        //Fetch session
-                       $session = $doctrine->getRepository(Session::class)->findOneByLocationSlotDate($data['location'], $data['slot'], $data['date']);
+                       $session = $this->doctrine->getRepository(Session::class)->findOneByLocationSlotDate($data['location'], $data['slot'], $data['date']);
                //Catch no session case
                } catch (NoResultException $e) {
                        //Create the session
-                       $session = new Session();
-                       $session->setLocation($data['location']);
-                       $session->setDate($data['date']);
-                       $session->setSlot($data['slot']);
+                       $session = new Session($data['date'], $data['location'], $data['slot']);
 
                        //Get location
                        $location = $data['location']->getTitle();
@@ -156,7 +159,7 @@ class ApplicationController extends AbstractController {
                        $session->setLength(new \DateTime('06:00:00'));
 
                        //Check if admin
-                       if ($this->isGranted('ROLE_ADMIN')) {
+                       if ($this->checker->isGranted('ROLE_ADMIN')) {
                                //Check if morning
                                if ($slot == 'Morning') {
                                        //Set begin at 9h
@@ -167,19 +170,22 @@ class ApplicationController extends AbstractController {
                                //Check if afternoon
                                } elseif ($slot == 'Afternoon') {
                                        //Set begin at 18h
-                                       $session->setBegin(new \DateTime('14:00:00'));
+                                       $session->setBegin(new \DateTime('15:30:00'));
 
                                        //Set length at 5h
-                                       $session->setLength(new \DateTime('05:00:00'));
+                                       $session->setLength(new \DateTime('05:30:00'));
                                //Check if evening
                                } elseif ($slot == 'Evening') {
                                        //Set begin at 19h00
-                                       $session->setBegin(new \DateTime('19:00:00'));
+                                       $session->setBegin(new \DateTime('19:30:00'));
+
+                                       //Set length at 5h
+                                       $session->setLength(new \DateTime('05:30:00'));
 
                                        //Check if next day is premium
                                        if ($premium) {
                                                //Set length at 7h
-                                               $session->setLength(new \DateTime('07:00:00'));
+                                               $session->setLength(new \DateTime('06:30:00'));
                                        }
                                //Check if after
                                } else {
@@ -331,46 +337,40 @@ class ApplicationController extends AbstractController {
                                //Add error in flash message
                                $this->addFlash('error', $this->translator->trans('Session on %date% %location% %slot% not yet supported', ['%location%' => $this->translator->trans('at '.$data['location']), '%slot%' => $this->translator->trans('the '.strtolower(strval($data['slot']))), '%date%' => $data['date']->format('Y-m-d')]));
 
-                               //Set title
-                               $title = $this->translator->trans('Application add');
-
                                //Render the view
-                               return $this->render('@RapsysAir/application/add.html.twig', ['title' => $title, 'form' => $form->createView()]+$this->context);
+                               return $this->render('@RapsysAir/application/add.html.twig', ['form' => $form->createView()]+$this->context);
                        }
 
                        //Check if admin
-                       if (!$this->isGranted('ROLE_ADMIN') && $session->getStart() < new \DateTime('00:00:00')) {
+                       if (!$this->checker->isGranted('ROLE_ADMIN') && $session->getStart() < new \DateTime('00:00:00')) {
                                //Add error in flash message
                                $this->addFlash('error', $this->translator->trans('Session in the past on %date% %location% %slot% not yet supported', ['%location%' => $this->translator->trans('at '.$data['location']), '%slot%' => $this->translator->trans('the '.strtolower(strval($data['slot']))), '%date%' => $data['date']->format('Y-m-d')]));
 
-                               //Set title
-                               $title = $this->translator->trans('Application add');
-
                                //Render the view
-                               return $this->render('@RapsysAir/application/add.html.twig', ['title' => $title, 'form' => $form->createView()]+$this->context);
+                               return $this->render('@RapsysAir/application/add.html.twig', ['form' => $form->createView()]+$this->context);
                        }
 
                        //Queue session save
-                       $manager->persist($session);
+                       $this->manager->persist($session);
 
                        //Flush to get the ids
-                       #$manager->flush();
+                       #$this->manager->flush();
 
                        $this->addFlash('notice', $this->translator->trans('Session on %date% %location% %slot% created', ['%location%' => $this->translator->trans('at '.$data['location']), '%slot%' => $this->translator->trans('the '.strtolower(strval($data['slot']))), '%date%' => $data['date']->format('Y-m-d')]));
                }
 
                //Set user
-               $user = $this->getUser();
+               $user = $this->security->getUser();
 
                //Replace with requested user for admin
-               if ($this->isGranted('ROLE_ADMIN') && !empty($data['user'])) {
-                       $user = $this->getDoctrine()->getRepository(User::class)->findOneById($data['user']);
+               if ($this->checker->isGranted('ROLE_ADMIN') && !empty($data['user'])) {
+                       $user = $this->doctrine->getRepository(User::class)->findOneById($data['user']);
                }
 
                //Protect application fetching
                try {
                        //Retrieve application
-                       $application = $doctrine->getRepository(Application::class)->findOneBySessionUser($session, $user);
+                       $application = $this->doctrine->getRepository(Application::class)->findOneBySessionUser($session, $user);
 
                        //Add warning in flash message
                        $this->addFlash('warning', $this->translator->trans('Application on %date% %location% %slot% already exists', ['%location%' => $this->translator->trans('at '.$data['location']), '%slot%' => $this->translator->trans('the '.strtolower(strval($data['slot']))), '%date%' => $data['date']->format('Y-m-d')]));
@@ -386,13 +386,13 @@ class ApplicationController extends AbstractController {
                        $session->setUpdated(new \DateTime('now'));
 
                        //Queue session save
-                       $manager->persist($session);
+                       $this->manager->persist($session);
 
                        //Queue application save
-                       $manager->persist($application);
+                       $this->manager->persist($application);
 
                        //Flush to get the ids
-                       $manager->flush();
+                       $this->manager->flush();
 
                        //Add notice in flash message
                        $this->addFlash('notice', $this->translator->trans('Application on %date% %location% %slot% created', ['%location%' => $this->translator->trans('at '.$data['location']), '%slot%' => $this->translator->trans('the '.strtolower(strval($data['slot']))), '%date%' => $data['date']->format('Y-m-d')]));
@@ -437,7 +437,7 @@ class ApplicationController extends AbstractController {
                                unset($route['_route'], $route['_controller']);
 
                                //Check if session view route
-                               if ($name == 'rapsys_air_session_view' && !empty($route['id'])) {
+                               if ($name == 'rapsysair_session_view' && !empty($route['id'])) {
                                        //Replace id
                                        $route['id'] = $session->getId();
                                //Other routes
@@ -456,6 +456,6 @@ class ApplicationController extends AbstractController {
                }
 
                //Redirect to cleanup the form
-               return $this->redirectToRoute('rapsys_air', ['session' => $session->getId()]);
+               return $this->redirectToRoute('rapsysair', ['session' => $session->getId()]);
        }
 }
index 7b61e9e48b53cfd449f6b8ed0a889c337f1e14da..663349cac91b6e01c9cc9c5ab1b4d35d0c654d7b 100644 (file)
@@ -18,32 +18,32 @@ class CalendarController extends DefaultController {
        /**
         * Calendar authorization
         *
-        * @desc Initiate calendar oauth process
+        * Initiate calendar oauth process
         *
         * @param Request $request The request instance
         *
         * @return Response The rendered view
         */
        public function index(Request $request): Response {
-               //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')]));
-
-               //Fetch doctrine
-               #$doctrine = $this->getDoctrine();
-
-               //Set section
-               $section = $this->translator->trans('Calendar oauth form');
+               //Without admin role
+               if (!$this->checker->isGranted('ROLE_ADMIN')) {
+                       //Throw 403
+                       throw $this->createAccessDeniedException($this->translator->trans('Unable to access this page without role %role%!', ['%role%' => $this->translator->trans('Admin')]));
+               }
 
                //Set description
                $this->context['description'] = $this->translator->trans('Initiate calendar oauth process');
 
                //Set title
-               $title = $this->translator->trans($this->config['site']['title']).' - '.$section;
+               $this->context['title']['page'] = $this->translator->trans('Oauth form');
+
+               //Set section
+               $this->context['title']['section'] = $this->translator->trans('Calendar');
 
                //Create the form according to the FormType created previously.
                //And give the proper parameters
                $form = $this->createForm('Rapsys\AirBundle\Form\CalendarType', ['calendar' => $this->config['calendar']['calendar'], 'prefix' => $this->config['calendar']['prefix']], [
-                       'action' => $this->generateUrl('rapsys_air_calendar'),
+                       'action' => $this->generateUrl('rapsysair_calendar'),
                        'method' => 'POST'
                ]);
 
@@ -70,7 +70,7 @@ class CalendarController extends DefaultController {
                                                'application_name' => $data['project'],
                                                'client_id' => $data['client'],
                                                'client_secret' => $data['secret'],
-                                               'redirect_uri' => $redirect = $this->generateUrl('rapsys_air_calendar_callback', [], UrlGeneratorInterface::ABSOLUTE_URL),
+                                               'redirect_uri' => $redirect = $this->generateUrl('rapsysair_calendar_callback', [], UrlGeneratorInterface::ABSOLUTE_URL),
                                                'scopes' => [Calendar::CALENDAR, Calendar::CALENDAR_EVENTS],
                                                'access_type' => 'offline',
                                                'approval_prompt' => 'force'
@@ -93,7 +93,7 @@ class CalendarController extends DefaultController {
 #                              $googleClient->addScope(Calendar::CALENDAR_EVENTS);
 #
 #                              //Set redirect uri
-#                              $googleClient->setRedirectUri($redirect = $this->generateUrl('rapsys_air_calendar_callback', [], UrlGeneratorInterface::ABSOLUTE_URL));
+#                              $googleClient->setRedirectUri($redirect = $this->generateUrl('rapsysair_calendar_callback', [], UrlGeneratorInterface::ABSOLUTE_URL));
 #
 #                              //Set offline access
 #                              $googleClient->setAccessType('offline');
@@ -129,7 +129,7 @@ class CalendarController extends DefaultController {
                }
 
                //Render template
-               return $this->render('@RapsysAir/calendar/index.html.twig', ['title' => $title, 'section' => $section, 'form' => $form->createView()]+$this->context);
+               return $this->render('@RapsysAir/calendar/index.html.twig', ['form' => $form->createView()]+$this->context);
        }
 
        /**
@@ -152,7 +152,10 @@ class CalendarController extends DefaultController {
                $this->context['description'] = $this->translator->trans('Finish calendar oauth process');
 
                //Set title
-               $title = $this->translator->trans($this->config['site']['title']).' - '.$section;
+               $this->context['title']['page'] = $this->translator->trans('Oauth callback');
+
+               //Set section
+               $this->context['title']['section'] = $this->translator->trans('Calendar');
 
                //With code
                if (!empty($code = $request->get('code'))) {
@@ -257,6 +260,6 @@ class CalendarController extends DefaultController {
                }
 
                //Render template
-               return $this->render('@RapsysAir/calendar/callback.html.twig', ['title' => $title, 'section' => $section]+$this->context);
+               return $this->render('@RapsysAir/calendar/callback.html.twig', $this->context);
        }
 }
diff --git a/Controller/DanceController.php b/Controller/DanceController.php
new file mode 100644 (file)
index 0000000..34ad492
--- /dev/null
@@ -0,0 +1,176 @@
+<?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\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+use Rapsys\AirBundle\Entity\Dance;
+
+class DanceController extends AbstractController {
+       public function index(Request $request): Response {
+               throw new \RuntimeException('TODO', 503);
+               header('Content-Type: text/plain');
+               var_dump('TODO');
+               #var_dump($name);
+               #var_dump($type);
+               #var_dump($slug);
+               exit;
+       }
+
+       /**
+        * Display dance by name
+        *
+        * @todo XXX: TODO: add <link rel="prev|next" for dances ? />
+        * @todo XXX: TODO: like described in: https://www.alsacreations.com/article/lire/1400-attribut-rel-relations.html#xnf-rel-attribute
+        * @todo XXX: TODO: or here: http://microformats.org/wiki/existing-rel-values#HTML5_link_type_extensions
+        *
+        * @TODO: faire plutôt comme /ville/x/y/paris
+        *
+        * @param Request $request The request instance
+        * @param string $name The shorted dance name
+        * @param string $dance The translated dance name
+        * @return Response The rendered view
+        */
+       public function name(Request $request, $name, $dance): Response {
+               throw new \RuntimeException('TODO', 503);
+
+               //Get name
+               $name = $this->slugger->unshort($sname = $name);
+
+               //With existing dance
+               if (empty($this->context['dances'] = $this->doctrine->getRepository(Dance::class)->findByName($name))) {
+                       //Throw not found
+                       //XXX: prevent slugger reverse engineering by not displaying decoded name
+                       throw $this->createNotFoundException($this->translator->trans('Unable to find dance %name%', ['%name%' => $sname]));
+               }
+
+               header('Content-Type: text/plain');
+               var_dump('TODO');
+               #var_dump($name);
+               #var_dump($type);
+               #var_dump($slug);
+               exit;
+
+               //Get city
+               if (!($this->context['city'] = $this->doctrine->getRepository(Location::class)->findCityByLatitudeLongitudeAsArray(floatval($latitude), floatval($longitude)))) {
+                       throw $this->createNotFoundException($this->translator->trans('Unable to find city: %latitude%,%longitude%', ['%latitude%' => $latitude, '%longitude%' => $longitude]));
+               }
+
+               //Add calendar
+               $this->context['calendar'] = $this->doctrine->getRepository(Session::class)->findAllByPeriodAsCalendarArray($this->period, !$this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED'), floatval($latitude), floatval($longitude));
+
+               //Set dances
+               $this->context['dances'] = [];
+
+               //Iterate on each calendar
+               foreach($this->context['calendar'] as $date => $calendar) {
+                       //Iterate on each session
+                       foreach($calendar['sessions'] as $sessionId => $session) {
+                               //Session with application dance
+                               if (!empty($session['application']['dance'])) {
+                                       //Add dance
+                                       $this->context['dances'][$session['application']['dance']['id']] = $session['application']['dance'];
+                               }
+                       }
+               }
+
+               //Add locations
+               $this->context['locations'] = $this->doctrine->getRepository(Location::class)->findAllByLatitudeLongitudeAsArray(floatval($latitude), floatval($longitude), $this->period);
+
+               //Set modified
+               //XXX: dance modified is already computed inside calendar modified
+               $this->modified = max(array_merge([$this->context['city']['updated']], array_map(function ($v) { return $v['modified']; }, array_merge($this->context['calendar'], $this->context['locations']))));
+
+               //Create response
+               $response = new Response();
+
+               //With logged user
+               if ($this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
+                       //Set last modified
+                       $response->setLastModified(new \DateTime('-1 year'));
+
+                       //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['city'], $this->context['dances'], $this->context['calendar'], $this->context['locations']))));
+
+                       //Set last modified
+                       $response->setLastModified($this->modified);
+
+                       //Set as public
+                       $response->setPublic();
+                
+                       //Without role and modification
+                       if ($response->isNotModified($request)) {
+                               //Return 304 response
+                               return $response;
+                       }
+               }
+
+               //Add multi
+               #$this->context['osm'] = $this->osm->getMultiImage($this->context['city']['link'], $this->context['city']['osm'], $this->modified->getTimestamp(), $latitude, $longitude, $this->context['locations'], $this->osm->getMultiZoom($latitude, $longitude, $this->context['locations'], 16));
+               $this->context['multimap'] = $this->map->getMultiMap($this->context['city']['multimap'], $this->modified->getTimestamp(), $latitude, $longitude, $this->context['locations'], $this->map->getMultiZoom($latitude, $longitude, $this->context['locations']));
+
+               //Set keywords
+               $this->context['keywords'] = [
+                       $this->context['city']['city'],
+                       $this->translator->trans('Indoor'),
+                       $this->translator->trans('Outdoor'),
+                       $this->translator->trans('Calendar'),
+                       $this->translator->trans('Libre Air')
+               ];
+
+               //With context dances
+               if (!empty($this->context['dances'])) {
+                       //Set dances
+                       $dances = array_map(function ($v) { return $v['name']; }, $this->context['dances']);
+
+                       //Insert dances in keywords
+                       array_splice($this->context['keywords'], 1, 0, $dances);
+
+                       //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 title
+                       $this->context['title'] = $this->translator->trans('%dances% %city%', ['%dances%' => $dances, '%city%' => $this->context['city']['in']]);
+
+                       //Set description
+                       $this->context['description'] = $this->translator->trans('%dances% indoor and outdoor calendar %city%', ['%dances%' => $dances, '%city%' => $this->context['city']['in']]);
+               } else {
+                       //Set title
+                       $this->context['title'] = $this->translator->trans('Dance %city%', ['%city%' => $this->context['city']['in']]);
+
+                       //Set description
+                       $this->context['description'] = $this->translator->trans('Indoor and outdoor dance calendar %city%', ['%city%' => $this->context['city']['in']]);
+               }
+
+               //Set locations description
+               $this->context['locations_description'] = $this->translator->trans('Libre Air location list %city%', ['%city%' => $this->context['city']['in']]);
+
+               //Render the view
+               return $this->render('@RapsysAir/dance/name.html.twig', $this->context, $response);
+       }
+
+       public function view(Request $request, $id, $name, $type): Response {
+               throw new \RuntimeException('TODO', 503);
+               header('Content-Type: text/plain');
+               var_dump('TODO');
+               #var_dump($name);
+               #var_dump($type);
+               #var_dump($slug);
+               exit;
+       }
+}
index e55d6e8398cb48baf38145cc2877b756ba20c8b5..14590963806e098561cc9da8ab44c9f23fb37847 100644 (file)
 namespace Rapsys\AirBundle\Controller;
 
 use Symfony\Bridge\Twig\Mime\TemplatedEmail;
+use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
+use Symfony\Component\Filesystem\Filesystem;
 use Symfony\Component\Form\FormError;
 use Symfony\Component\HttpFoundation\Request;
 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\Security\Core\Authentication\Token\UsernamePasswordToken;
+use Symfony\Component\Security\Core\Exception\AccessDeniedException;
 
-use Rapsys\AirBundle\Entity\Civility;
+use Rapsys\AirBundle\Entity\Dance;
 use Rapsys\AirBundle\Entity\Location;
 use Rapsys\AirBundle\Entity\Session;
 use Rapsys\AirBundle\Entity\Snippet;
 use Rapsys\AirBundle\Entity\User;
-use Rapsys\AirBundle\Pdf\DisputePdf;
+use Rapsys\AirBundle\Token\AnonymousToken;
 
 /**
  * {@inheritdoc}
@@ -34,14 +36,14 @@ class DefaultController extends AbstractController {
        /**
         * The about page
         *
-        * @desc Display the about informations
+        * Display the about informations
         *
         * @param Request $request The request instance
         * @return Response The rendered view
         */
        public function about(Request $request): Response {
                //Set page
-               $this->context['title'] = $this->translator->trans('About');
+               $this->context['title']['page'] = $this->translator->trans('About');
 
                //Set description
                $this->context['description'] = $this->translator->trans('Libre Air about');
@@ -65,16 +67,15 @@ class DefaultController extends AbstractController {
        /**
         * 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['title'] = $this->translator->trans('Contact');
+               $this->context['title']['page'] = $this->translator->trans('Contact');
 
                //Set description
                $this->context['description'] = $this->translator->trans('Contact Libre Air');
@@ -88,10 +89,23 @@ class DefaultController extends AbstractController {
                        $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'),
+                       'captcha' => true,
                        'method' => 'POST'
                ]);
 
@@ -99,7 +113,7 @@ class DefaultController extends AbstractController {
                        // 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();
 
@@ -108,7 +122,7 @@ class DefaultController extends AbstractController {
                                        //Set sender
                                        ->from(new Address($data['mail'], $data['name']))
                                        //Set recipient
-                                       ->to(new Address($this->context['contact']['mail'], $this->context['contact']['title']))
+                                       ->to(new Address($this->context['contact']['address'], $this->context['contact']['name']))
                                        //Set subject
                                        ->subject($data['subject'])
 
@@ -128,7 +142,7 @@ class DefaultController extends AbstractController {
                                //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'));
@@ -136,10 +150,10 @@ class DefaultController extends AbstractController {
                                } 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->context['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->context['contact']['mail']])));
+                                               $form->get('mail')->addError(new FormError($this->translator->trans('Unable to contact: %mail%', ['%mail%' => $this->context['contact']['address']])));
                                        }
                                }
                        }
@@ -150,202 +164,133 @@ class DefaultController extends AbstractController {
        }
 
        /**
-        * 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')]));
+       public function index(Request $request): Response {
+               //Add cities
+               $this->context['cities'] = $this->doctrine->getRepository(Location::class)->findCitiesAsArray($this->period);
 
-               //Set page
-               $this->context['title'] = $this->translator->trans('Dispute');
+               //Add calendar
+               $this->context['calendar'] = $this->doctrine->getRepository(Session::class)->findAllByPeriodAsCalendarArray($this->period, !$this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED'));
 
-               //Set description
-               $this->context['description'] = $this->translator->trans('Libre Air dispute');
+               //Add dances
+               $this->context['dances'] = $this->doctrine->getRepository(Dance::class)->findNamesAsArray();
 
-               //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')
-               ];
+               //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);
+
+                       //Set as public
+                       $response->setPublic();
 
-                               //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']['title']))
-#                                      //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']])));
-#                                      }
-#                              }
+                       //Without role and modification
+                       if ($response->isNotModified($request)) {
+                               //Return 304 response
+                               return $response;
                        }
                }
 
-               //Render template
-               return $this->render('@RapsysAir/default/dispute.html.twig', ['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['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['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 facebook type
-               //XXX: only valid for home page
-               $this->context['facebook']['metas']['og:type'] = 'website';
+                       //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'), $request->getLocale());
+               //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]);
+
+               //Set facebook type
+               //XXX: only valid for home page
+               $this->context['facebook']['metas']['og:type'] = 'website';
 
-               //Return the response
-               #return $response;
+               //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(Request $request): Response {
                //Set page
-               $this->context['title'] = $this->translator->trans('Organizer regulation');
+               $this->context['title']['page'] = $this->translator->trans('Organizer regulation');
 
                //Set description
                $this->context['description'] = $this->translator->trans('Libre Air organizer regulation');
@@ -371,14 +316,14 @@ class DefaultController extends AbstractController {
        /**
         * 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(Request $request): Response {
                //Set page
-               $this->context['title'] = $this->translator->trans('Terms of service');
+               $this->context['title']['page'] = $this->translator->trans('Terms of service');
 
                //Set description
                $this->context['description'] = $this->translator->trans('Libre Air terms of service');
@@ -404,14 +349,14 @@ class DefaultController extends AbstractController {
        /**
         * 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(Request $request): Response {
                //Set page
-               $this->context['title'] = $this->translator->trans('Frequently asked questions');
+               $this->context['title']['page'] = $this->translator->trans('Frequently asked questions');
 
                //Set description
                $this->context['description'] = $this->translator->trans('Libre Air frequently asked questions');
@@ -438,30 +383,33 @@ class DefaultController extends AbstractController {
        /**
         * List all users
         *
-        * @desc Display all user with a group listed as users
+        * Display all user with a group listed as users
         *
         * @param Request $request The request instance
         *
         * @return Response The rendered view
         */
        public function userIndex(Request $request): Response {
-               //Fetch doctrine
-               $doctrine = $this->getDoctrine();
-
                //With admin role
-               if ($this->isGranted('ROLE_ADMIN')) {
+               if ($this->checker->isGranted('ROLE_ADMIN')) {
+                       //Set title
+                       $this->context['title']['page'] = $this->translator->trans('Libre Air user list');
+
                        //Set section
-                       $section = $this->translator->trans('Libre Air users');
+                       $this->context['title']['section'] = $this->translator->trans('User');
 
                        //Set description
-                       $this->context['description'] = $this->translator->trans('Libre Air user list');
+                       $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
-                       $section = $this->translator->trans('Libre Air organizers');
+                       $this->context['title']['section'] = $this->translator->trans('Organizer');
 
                        //Set description
-                       $this->context['description'] = $this->translator->trans('Libre Air organizers list');
+                       $this->context['description'] = $this->translator->trans('Lists Libre air organizers');
                }
 
                //Set keywords
@@ -472,26 +420,11 @@ class DefaultController extends AbstractController {
                        $this->translator->trans('Libre Air')
                ];
 
-               //Set title
-               $title = $this->translator->trans($this->config['site']['title']).' - '.$section;
-
                //Fetch users
-               $users = $doctrine->getRepository(User::class)->findUserGroupedByTranslatedGroup($this->translator);
-
-               //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'
-                       )
-               );
+               $users = $this->doctrine->getRepository(User::class)->findIndexByGroupId();
 
                //With admin role
-               if ($this->isGranted('ROLE_ADMIN')) {
+               if ($this->checker->isGranted('ROLE_ADMIN')) {
                        //Display all users
                        $this->context['groups'] = $users;
                //Without admin role
@@ -500,236 +433,342 @@ class DefaultController extends AbstractController {
                        $this->context['users'] = $users[$this->translator->trans('Senior')];
                }
 
-               //Fetch locations
-               //XXX: we want to display all active locations anyway
-               $locations = $doctrine->getRepository(Location::class)->findTranslatedSortedByPeriod($this->translator, $period);
-
                //Render the view
-               return $this->render('@RapsysAir/user/index.html.twig', ['title' => $title, 'section' => $section, 'locations' => $locations]+$this->context);
+               return $this->render('@RapsysAir/user/index.html.twig', $this->context);
        }
 
        /**
         * List all sessions for the user
         *
-        * @desc Display all sessions for the user with an application or login form
+        * Display all sessions for the user with an application or login form
         *
         * @param Request $request The request instance
         * @param int $id The user id
         *
         * @return Response The rendered view
         */
-       public function userView(Request $request, $id): Response {
-               //Fetch doctrine
-               $doctrine = $this->getDoctrine();
-
-               //Fetch user
-               if (empty($user = $doctrine->getRepository(User::class)->findOneById($id))) {
-                       throw $this->createNotFoundException($this->translator->trans('Unable to find user: %id%', ['%id%' => $id]));
+       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]));
                }
 
-               //Get user token
-               $token = new UsernamePasswordToken($user, null, 'none', $user->getRoles());
-
-               //Check if guest
-               $isGuest = $this->get('rapsys_user.access_decision_manager')->decide($token, ['ROLE_GUEST']);
+               //Create token
+               $token = new AnonymousToken($this->context['user']['roles']);
 
                //Prevent access when not admin, user is not guest and not currently logged user
-               if (!$this->isGranted('ROLE_ADMIN') && empty($isGuest) && $user != $this->getUser()) {
-                       throw $this->createAccessDeniedException($this->translator->trans('Unable to access user: %id%', ['%id%' => $id]));
+               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]));
                }
 
-               //Set section
-               $section = $user->getPseudonym();
+               //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']]);
+               }
 
-               //Set title
-               $title = $this->translator->trans($this->config['site']['title']).' - '.$section;
+               //Fetch calendar
+               $this->context['calendar'] = $this->doctrine->getRepository(Session::class)->findAllByPeriodAsCalendarArray($this->period, !$this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED'), null, null, $id);
 
-               //Set description
-               $this->context['description'] = $this->translator->trans('%pseudonym% outdoor Argentine Tango session calendar', [ '%pseudonym%' => $user->getPseudonym() ]);
+               //Get locations at less than 2 km
+               $this->context['locations'] = $this->doctrine->getRepository(Location::class)->findAllByUserIdAsArray($id, $this->period, 2);
 
-               //Set keywords
-               $this->context['keywords'] = [
-                       $user->getPseudonym(),
-                       $this->translator->trans('outdoor'),
-                       $this->translator->trans('Argentine Tango'),
-                       $this->translator->trans('calendar')
-               ];
+               //Set ats
+               $ats = [];
 
-               //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 dances
+               $dances = [];
 
-               //Fetch calendar
-               //TODO: highlight with current session route parameter
-               $calendar = $doctrine->getRepository(Session::class)->fetchUserCalendarByDatePeriod($this->translator, $period, $isGuest?$id:null, $request->get('session'), $request->getLocale());
-
-               //Fetch locations
-               //XXX: we want to display all active locations anyway
-               $locations = $doctrine->getRepository(Location::class)->findTranslatedSortedByPeriod($this->translator, $period, $id);
-
-               //Create user form for admin or current user
-               if ($this->isGranted('ROLE_ADMIN') || $user == $this->getUser()) {
-                       //Create SnippetType form
-                       $userForm = $this->createForm('Rapsys\AirBundle\Form\RegisterType', $user, [
-                               //Set action
-                               'action' => $this->generateUrl('rapsys_air_user_view', ['id' => $id]),
-                               //Set the form attribute
-                               'attr' => [ 'class' => 'col' ],
-                               //Set civility class
-                               'civility_class' => Civility::class,
-                               //Disable mail
-                               'mail' => $this->isGranted('ROLE_ADMIN'),
-                               //Disable password
-                               'password' => false
-                       ]);
-
-                       //Init user to context
-                       $this->context['forms']['user'] = $userForm->createView();
-
-                       //Check if submitted
-                       if ($request->isMethod('POST')) {
-                               //Refill the fields in case the form is not valid.
-                               $userForm->handleRequest($request);
-
-                               //Handle invalid form
-                               if (!$userForm->isSubmitted() || !$userForm->isValid()) {
-                                       //Render the view
-                                       return $this->render('@RapsysAir/user/view.html.twig', ['id' => $id, 'title' => $title, 'section' => $section, 'calendar' => $calendar, 'locations' => $locations]+$this->context);
-                               }
+               //Set indoors
+               $indoors = [];
 
-                               //Get data
-                               $data = $userForm->getData();
+               //Set ins
+               $ins = [];
 
-                               //Get manager
-                               $manager = $doctrine->getManager();
+               //Set insides
+               $insides = [];
 
-                               //Queue snippet save
-                               $manager->persist($data);
+               //Set locations
+               $locations = [];
 
-                               //Flush to get the ids
-                               $manager->flush();
+               //Set types
+               $types = [];
 
-                               //Add notice
-                               $this->addFlash('notice', $this->translator->trans('User %id% updated', ['%id%' => $id]));
+               //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'];
 
-                               //Extract and process referer
-                               if ($referer = $request->headers->get('referer')) {
-                                       //Create referer request instance
-                                       $req = Request::create($referer);
+                               //Add types
+                               $types[$session['application']['dance']['type']] = lcfirst($session['application']['dance']['type']);
 
-                                       //Get referer path
-                                       $path = $req->getPathInfo();
+                               //Add indoors
+                               $indoors[$session['location']['indoor']?'indoor':'outdoor'] = $this->translator->trans($session['location']['indoor']?'indoor':'outdoor');
 
-                                       //Get referer query string
-                                       $query = $req->getQueryString();
+                               //Add insides
+                               $insides[$session['location']['indoor']?'inside':'outside'] = $this->translator->trans($session['location']['indoor']?'inside':'outside');
 
-                                       //Remove script name
-                                       $path = str_replace($request->getScriptName(), '', $path);
+                               //Add ats
+                               $ats[$session['location']['id']] = $session['location']['at'];
 
-                                       //Try with referer path
-                                       try {
-                                               //Save old context
-                                               $oldContext = $this->router->getContext();
+                               //Add ins
+                               $ins[$session['location']['id']] = $session['location']['in'];
 
-                                               //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());
+                               //Session with application user id
+                               if (!empty($session['application']['user']['id']) && $session['application']['user']['id'] == $id) {
+                                       //Add location
+                                       $locations[$session['location']['id']] = $session['location'];
+                               }
+                       }
+               }
 
-                                               //Retrieve route matching path
-                                               $route = $this->router->match($path);
+               //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']))));
 
-                                               //Reset context
-                                               $this->router->setContext($oldContext);
+               //Create response
+               $response = new Response();
 
-                                               //Clear old context
-                                               unset($oldContext);
+               //With logged user
+               if ($this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
+                       //Set last modified
+                       $response->setLastModified(new \DateTime('-1 year'));
 
-                                               //Extract name
-                                               $name = $route['_route'];
+                       //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']))));
 
-                                               //Remove route and controller from route defaults
-                                               unset($route['_route'], $route['_controller']);
+                       //Set last modified
+                       $response->setLastModified($this->modified);
 
-                                               //Check if user view route
-                                               if ($name == 'rapsys_air_user_view' && !empty($route['id'])) {
-                                                       //Replace id
-                                                       $route['id'] = $data->getId();
-                                               //Other routes
-                                               } else {
-                                                       //Set user
-                                                       $route['user'] = $data->getId();
-                                               }
+                       //Set as public
+                       $response->setPublic();
 
-                                               //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', ['user' => $data->getId()]);
+                       //Without role and modification
+                       if ($response->isNotModified($request)) {
+                               //Return 304 response
+                               return $response;
                        }
                }
 
-               //Create snippet forms for role_guest
-               if ($this->isGranted('ROLE_ADMIN') || ($this->isGranted('ROLE_GUEST') && $user == $this->getUser())) {
-                       //Fetch all user snippet
-                       $snippets = $doctrine->getRepository(Snippet::class)->findByLocaleUserId($request->getLocale(), $id);
+               //Add multi map
+               $this->context['multimap'] = $this->map->getMultiMap($this->context['user']['multimap'], $this->modified->getTimestamp(), $this->context['locations']);
 
-                       //Rekey by location id
-                       $snippets = array_reduce($snippets, function($carry, $item){$carry[$item->getLocation()->getId()] = $item; return $carry;}, []);
+               //Set keywords
+               $this->context['keywords'] = [
+                       $this->context['user']['pseudonym'],
+                       $this->translator->trans('calendar'),
+                       $this->translator->trans('Libre Air')
+               ];
 
-                       //Init snippets to context
-                       $this->context['forms']['snippets'] = [];
+               //Set cities
+               $cities = array_unique(array_map(function ($v) { return $v['city']; }, $locations));
 
-                       //Iterate on locations
-                       foreach($locations as $locationId => $location) {
-                               //Init snippet
-                               $snippet = new Snippet();
+               //Set titles
+               $titles = array_map(function ($v) { return $v['title']; }, $locations);
+
+               //Insert dances in keywords
+               array_splice($this->context['keywords'], 1, 0, array_merge($types, $dances, $indoors, $insides, $titles, $cities));
+
+               //Deduplicate ins
+               $ins = array_unique($ins);
 
-                               //Set default locale
-                               $snippet->setLocale($request->getLocale());
+               //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 default user
-                               $snippet->setUser($user);
+               //Get textual types
+               $types = implode($this->translator->trans(' and '), array_filter(array_merge([implode(', ', array_slice($types, 0, -1))], array_slice($types, -1)), 'strlen'));
 
-                               //Set default location
-                               $snippet->setLocation($doctrine->getRepository(Location::class)->findOneById($locationId));
+               //Get textual indoors
+               $indoors = implode($this->translator->trans(' and '), array_filter(array_merge([implode(', ', array_slice($indoors, 0, -1))], array_slice($indoors, -1)), 'strlen'));
 
+               //Get textual ats
+               $ats = implode($this->translator->trans(' and '), array_filter(array_merge([implode(', ', array_slice($ats, 0, -1))], array_slice($ats, -1)), 'strlen'));
+
+               //Get textual ins
+               $ins = implode($this->translator->trans(' and '), array_filter(array_merge([implode(', ', array_slice($ins, 0, -1))], array_slice($ins, -1)), 'strlen'));
+
+               //Set title
+               $this->context['title']['page'] = $this->translator->trans('%pseudonym% organizer', ['%pseudonym%' => $this->context['user']['pseudonym']]);
+
+               //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']]);
+               }
+
+               //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 alternates
+               $this->context['alternates'] += $this->context['user']['alternates'];
+
+               //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);
+
+                       //Get user
+                       $user = $this->doctrine->getRepository(User::class)->findOneById($id);
+
+                       //Iterate on locations
+                       foreach($this->context['locations'] as $locationId => $location) {
                                //With existing snippet
-                               if (!empty($snippets[$locationId])) {
-                                       $snippet = $snippets[$locationId];
-                                       $action = $this->generateUrl('rapsys_air_snippet_edit', ['id' => $snippet->getId()]);
-                               //Without snippet
+                               if (isset($snippets[$location['id']])) {
+                                       //Set existing in current
+                                       $current = $snippets[$location['id']];
+                               //Without existing snippet
                                } else {
-                                       $action = $this->generateUrl('rapsys_air_snippet_add', ['location' => $locationId]);
+                                       //Init snippet
+                                       $current = new Snippet($this->locale, $this->doctrine->getRepository(Location::class)->findOneById($location['id']), $user);
                                }
 
                                //Create SnippetType form
-                               $form = $this->container->get('form.factory')->createNamed('snipped_'.$request->getLocale().'_'.$locationId, 'Rapsys\AirBundle\Form\SnippetType', $snippet, [
-                                       //Set the action
-                                       'action' => $action,
-                                       //Set the form attribute
-                                       'attr' => []
-                               ]);
+                               $form = $this->factory->createNamed(
+                                       //Set form id
+                                       'snippet_'.$locationId.'_'.$id.'_'.$this->locale,
+                                       //Set form type
+                                       'Rapsys\AirBundle\Form\SnippetType',
+                                       //Set form data
+                                       $current
+                               );
+
+                               //Refill the fields in case of invalid form
+                               $form->handleRequest($request);
+
+                               //Handle submitted and valid form
+                               //TODO: add a delete snippet ?
+                               if ($form->isSubmitted() && $form->isValid()) {
+                                       //Get snippet
+                                       $snippet = $form->getData();
+
+                                       //Queue snippet save
+                                       $this->manager->persist($snippet);
+
+                                       //Flush to get the ids
+                                       $this->manager->flush();
+
+                                       //Add notice
+                                       $this->addFlash('notice', $this->translator->trans('Snippet 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']]);
+                               }
 
                                //Add form to context
                                $this->context['forms']['snippets'][$locationId] = $form->createView();
+
+                               //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);
+                               }
+
+                               //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 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']]);
+                                       }
+                               }
+
+                               //Add form to context
+                               $this->context['forms']['images'][$locationId] = $form->createView();
                        }
                }
 
                //Render the view
-               return $this->render('@RapsysAir/user/view.html.twig', ['id' => $id, 'title' => $title, 'section' => $section, 'calendar' => $calendar, 'locations' => $locations]+$this->context);
+               return $this->render('@RapsysAir/user/view.html.twig', ['id' => $id]+$this->context);
        }
 }
index ce735583bfd92e5c0e0690b12b3219dca60f0f55..9ee9471af6ceda4badb0f3086cdfd35a3d025727 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\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\Routing\RequestContext;
-use Symfony\Component\Routing\Exception\MethodNotAllowedException;
-use Symfony\Component\Routing\Exception\ResourceNotFoundException;
-use Rapsys\AirBundle\Entity\Slot;
-use Rapsys\AirBundle\Entity\Session;
+
+use Rapsys\AirBundle\Entity\Dance;
 use Rapsys\AirBundle\Entity\Location;
+use Rapsys\AirBundle\Entity\Session;
 
+/**
+ * {@inheritdoc}
+ */
 class LocationController extends DefaultController {
        /**
-        * Add location
-        *
-        * @desc Persist location in database
+        * List all cities
         *
         * @param Request $request The request instance
-        *
-        * @return Response The rendered view or redirection
-        *
-        * @throws \RuntimeException When user has not at least admin role
+        * @return Response The rendered view
         */
-       public function add(Request $request) {
-               //Prevent non-guest to access here
-               $this->denyAccessUnlessGranted('ROLE_ADMIN', null, $this->translator->trans('Unable to access this page without role %role%!', ['%role%' => $this->translator->trans('Admin')]));
-
-               //Create LocationType form
-               $form = $this->createForm('Rapsys\AirBundle\Form\LocationType', null, [
-                       //Set the action
-                       'action' => $this->generateUrl('rapsys_air_location_add'),
-                       //Set the form attribute
-                       'attr' => []
-               ]);
-
-               //Refill the fields in case of invalid form
-               $form->handleRequest($request);
-
-               //Handle invalid form
-               if (!$form->isSubmitted() || !$form->isValid()) {
-                       //Set section
-                       $section = $this->translator->trans('Location add');
-
-                       //Set title
-                       $title = $this->translator->trans($this->config['site']['title']).' - '.$section;
-
-                       //Render the view
-                       return $this->render('@RapsysAir/location/add.html.twig', ['title' => $title, 'section' => $section, 'form' => $form->createView()]+$this->context);
-               }
-
-               //Get doctrine
-               $doctrine = $this->getDoctrine();
-
-               //Get manager
-               $manager = $doctrine->getManager();
+       public function cities(Request $request): Response {
+               //Add cities
+               $this->context['cities'] = $this->doctrine->getRepository(Location::class)->findCitiesAsArray($this->period, 0);
 
-               //Get location
-               $location = $form->getData();
+               //Add dances
+               $this->context['dances'] = $this->doctrine->getRepository(Dance::class)->findNamesAsArray();
 
-               //Set created
-               $location->setCreated(new \DateTime('now'));
+               //Create response
+               $response = new Response();
 
-               //Set updated
-               $location->setUpdated(new \DateTime('now'));
+               //Set modified
+               $this->modified = max(array_map(function ($v) { return $v['modified']; }, array_merge($this->context['cities'], $this->context['dances'])));
 
-               //Queue location save
-               $manager->persist($location);
-
-               //Flush to get the ids
-               $manager->flush();
-
-               //Add notice
-               $this->addFlash('notice', $this->translator->trans('Location %id% created', ['%id%' => $location->getId()]));
-
-               //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());
+               //Add city multi
+               foreach($this->context['cities'] as $id => $city) {
+                       //Add city multi
+                       $this->context['cities'][$id]['multimap'] = $this->map->getMultiMap($city['multimap'], $this->modified->getTimestamp(), $city['locations']);
+               }
 
-                               //Retrieve route matching path
-                               $route = $this->router->match($path);
+               //With logged user
+               if ($this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
+                       //Set last modified
+                       $response->setLastModified(new \DateTime('-1 year'));
+
+                       //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['cities'], $this->context['dances']))));
+
+                       //Set last modified
+                       $response->setLastModified($this->modified);
+
+                       //Set as public
+                       $response->setPublic();
+
+                       //Without role and modification
+                       if ($response->isNotModified($request)) {
+                               //Return 304 response
+                               return $response;
+                       }
+               }
 
-                               //Reset context
-                               $this->router->setContext($oldContext);
+               //Set section
+               $this->context['title']['page'] = $this->translator->trans('Libre Air cities');
 
-                               //Clear old context
-                               unset($oldContext);
+               //Set description
+               $this->context['description'] = $this->translator->trans('Libre Air city list');
 
-                               //Extract name
-                               $name = $route['_route'];
+               //Set cities
+               $cities = array_map(function ($v) { return $v['in']; }, $this->context['cities']);
 
-                               //Remove route and controller from route defaults
-                               unset($route['_route'], $route['_controller']);
+               //Set dances
+               $dances = array_map(function ($v) { return $v['name']; }, $this->context['dances']);
 
-                               //Check if location view route
-                               if ($name == 'rapsys_air_location_view' && !empty($route['id'])) {
-                                       //Replace id
-                                       $route['id'] = $location->getId();
-                               //Other routes
-                               } else {
-                                       //Set location
-                                       $route['location'] = $location->getId();
-                               }
+               //Set indoors
+               $indoors = array_reduce($this->context['cities'], function ($c, $v) { return array_merge($c, $v['indoors']); }, []);
 
-                               //Generate url
-                               return $this->redirectToRoute($name, $route);
-                       //No route matched
-                       } catch(MethodNotAllowedException|ResourceNotFoundException $e) {
-                               //Unset referer to fallback to default route
-                               unset($referer);
-                       }
-               }
+               //Set keywords
+               $this->context['keywords'] = array_values(
+                       array_merge(
+                               [
+                                       $this->translator->trans('Cities'),
+                                       $this->translator->trans('City list'),
+                                       $this->translator->trans('Listing'),
+                               ],
+                               $cities,
+                               $indoors,
+                               [
+                                       $this->translator->trans('calendar'),
+                                       $this->translator->trans('Libre Air')
+                               ]
+                       )
+               );
 
-               //Redirect to cleanup the form
-               return $this->redirectToRoute('rapsys_air', ['location' => $location->getId()]);
+               //Render the view
+               return $this->render('@RapsysAir/location/cities.html.twig', $this->context, $response);
        }
 
        /**
-        * Edit location
+        * Display city
         *
-        * @desc Persist location in database
+        * @todo XXX: TODO: add <link rel="prev|next" for sessions or classes ? />
+        * @todo XXX: TODO: like described in: https://www.alsacreations.com/article/lire/1400-attribut-rel-relations.html#xnf-rel-attribute
+        * @todo XXX: TODO: or here: http://microformats.org/wiki/existing-rel-values#HTML5_link_type_extensions
         *
         * @param Request $request The request instance
-        *
-        * @return Response The rendered view or redirection
-        *
-        * @throws \RuntimeException When user has not at least guest role
+        * @param float $latitude The city latitude
+        * @param float $longitude The city longitude
+        * @return Response The rendered view
         */
-       public function edit(Request $request, $id) {
-               //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')]));
-
-               //Get doctrine
-               $doctrine = $this->getDoctrine();
-
-               //Get location
-               if (empty($location = $doctrine->getRepository(Location::class)->findOneById($id))) {
-                       throw $this->createNotFoundException($this->translator->trans('Unable to find location: %id%', ['%id%' => $id]));
+       public function city(Request $request, float $latitude, float $longitude, string $city): Response {
+               //Get city
+               if (!($this->context['city'] = $this->doctrine->getRepository(Location::class)->findCityByLatitudeLongitudeAsArray(floatval($latitude), floatval($longitude)))) {
+                       throw $this->createNotFoundException($this->translator->trans('Unable to find city: %latitude%,%longitude%', ['%latitude%' => $latitude, '%longitude%' => $longitude]));
                }
 
-               //Create LocationType form
-               $form = $this->createForm('Rapsys\AirBundle\Form\LocationType', $location, [
-                       //Set the action
-                       'action' => $this->generateUrl('rapsys_air_location_edit', ['id' => $id]),
-                       //Set the form attribute
-                       'attr' => []
-               ]);
-
-               //Refill the fields in case of invalid form
-               $form->handleRequest($request);
+               //Add calendar
+               $this->context['calendar'] = $this->doctrine->getRepository(Session::class)->findAllByPeriodAsCalendarArray($this->period, !$this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED'), floatval($latitude), floatval($longitude));
 
-               //Handle invalid form
-               if (!$form->isSubmitted() || !$form->isValid()) {
-                       //Set section
-                       $section = $this->translator->trans('Location %id%', ['%id%' => $id]);
+               //Set dances
+               $this->context['dances'] = [];
 
-                       //Set title
-                       $title = $this->translator->trans($this->config['site']['title']).' - '.$section;
-
-                       //Render the view
-                       return $this->render('@RapsysAir/location/edit.html.twig', ['id' => $id, 'title' => $title, 'section' => $section, 'form' => $form->createView()]+$this->context);
+               //Iterate on each calendar
+               foreach($this->context['calendar'] as $date => $calendar) {
+                       //Iterate on each session
+                       foreach($calendar['sessions'] as $sessionId => $session) {
+                               //Session with application dance
+                               if (!empty($session['application']['dance'])) {
+                                       //Add dance
+                                       $this->context['dances'][$session['application']['dance']['id']] = $session['application']['dance'];
+                               }
+                       }
                }
 
-               //Get manager
-               $manager = $doctrine->getManager();
+               //Add locations
+               $this->context['locations'] = $this->doctrine->getRepository(Location::class)->findAllByLatitudeLongitudeAsArray(floatval($latitude), floatval($longitude), $this->period, 0);
 
-               //Set updated
-               $location->setUpdated(new \DateTime('now'));
+               //Set modified
+               //XXX: dance modified is already computed inside calendar modified
+               $this->modified = max(array_merge([$this->context['city']['updated']], array_map(function ($v) { return $v['modified']; }, array_merge($this->context['calendar'], $this->context['locations']))));
 
-               //Queue location save
-               $manager->persist($location);
+               //Create response
+               $response = new Response();
 
-               //Flush to get the ids
-               $manager->flush();
+               //With logged user
+               if ($this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
+                       //Set last modified
+                       $response->setLastModified(new \DateTime('-1 year'));
 
-               //Add notice
-               $this->addFlash('notice', $this->translator->trans('Location %id% updated', ['%id%' => $id]));
+                       //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['city'], $this->context['dances'], $this->context['calendar'], $this->context['locations']))));
 
-               //Extract and process referer
-               if ($referer = $request->headers->get('referer')) {
-                       //Create referer request instance
-                       $req = Request::create($referer);
+                       //Set last modified
+                       $response->setLastModified($this->modified);
 
-                       //Get referer path
-                       $path = $req->getPathInfo();
+                       //Set as public
+                       $response->setPublic();
 
-                       //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();
+                       //Without role and modification
+                       if ($response->isNotModified($request)) {
+                               //Return 304 response
+                               return $response;
+                       }
+               }
 
-                               //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());
+               //Add multi
+               $this->context['multimap'] = $this->map->getMultiMap($this->context['city']['multimap'], $this->modified->getTimestamp(), $this->context['locations']);
 
-                               //Retrieve route matching path
-                               $route = $this->router->match($path);
+               //Set keywords
+               $this->context['keywords'] = [
+                       $this->context['city']['city'],
+                       $this->translator->trans('Indoor'),
+                       $this->translator->trans('Outdoor'),
+                       $this->translator->trans('Calendar'),
+                       $this->translator->trans('Libre Air')
+               ];
 
-                               //Reset context
-                               $this->router->setContext($oldContext);
+               //With context dances
+               if (!empty($this->context['dances'])) {
+                       //Set dances
+                       $dances = array_map(function ($v) { return $v['name']; }, $this->context['dances']);
 
-                               //Clear old context
-                               unset($oldContext);
+                       //Insert dances in keywords
+                       array_splice($this->context['keywords'], 1, 0, $dances);
 
-                               //Extract name
-                               $name = $route['_route'];
+                       //Get textual dances
+                       $dances = implode($this->translator->trans(' and '), array_filter(array_merge([implode(', ', array_slice($dances, 0, -1))], array_slice($dances, -1)), 'strlen'));
 
-                               //Remove route and controller from route defaults
-                               unset($route['_route'], $route['_controller']);
+                       //Set title
+                       $this->context['title']['page'] = $this->translator->trans('%dances% %city%', ['%dances%' => $dances, '%city%' => $this->context['city']['in']]);
 
-                               //Check if location view route
-                               if ($name == 'rapsys_air_location_view' && !empty($route['id'])) {
-                                       //Replace id
-                                       $route['id'] = $location->getId();
-                               //Other routes
-                               } else {
-                                       //Set location
-                                       $route['location'] = $location->getId();
-                               }
+                       //Set description
+                       $this->context['description'] = $this->translator->trans('%dances% indoor and outdoor calendar %city%', ['%dances%' => $dances, '%city%' => $this->context['city']['in']]);
+               } else {
+                       //Set title
+                       $this->context['title']['page'] = $this->translator->trans('Dance %city%', ['%city%' => $this->context['city']['in']]);
 
-                               //Generate url
-                               return $this->redirectToRoute($name, $route);
-                       //No route matched
-                       } catch(MethodNotAllowedException|ResourceNotFoundException $e) {
-                               //Unset referer to fallback to default route
-                               unset($referer);
-                       }
+                       //Set description
+                       $this->context['description'] = $this->translator->trans('Indoor and outdoor dance calendar %city%', ['%city%' => $this->context['city']['in']]);
                }
 
-               //Redirect to cleanup the form
-               return $this->redirectToRoute('rapsys_air', ['location' => $location->getId()]);
+               //Set locations description
+               $this->context['locations_description'] = $this->translator->trans('Libre Air location list %city%', ['%city%' => $this->context['city']['in']]);
+
+               //Render the view
+               return $this->render('@RapsysAir/location/city.html.twig', $this->context, $response);
        }
 
        /**
         * List all locations
         *
-        * @desc Display all locations
+        * Display all locations
         *
         * @param Request $request The request instance
         *
         * @return Response The rendered view
         */
        public function index(Request $request): Response {
-               //Fetch doctrine
-               $doctrine = $this->getDoctrine();
+               //Get locations
+               $this->context['locations'] = $this->doctrine->getRepository(Location::class)->findAllAsArray($this->period);
+
+               //Set modified
+               $this->modified = max(array_map(function ($v) { return $v['updated']; }, $this->context['locations']));
+
+               //Create response
+               $response = new Response();
+
+               //With logged user
+               if ($this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
+                       //Set last modified
+                       $response->setLastModified(new \DateTime('-1 year'));
+
+                       //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($this->context['locations'])));
+
+                       //Set last modified
+                       $response->setLastModified($this->modified);
+
+                       //Set as public
+                       $response->setPublic();
+
+                       //Without role and modification
+                       if ($response->isNotModified($request)) {
+                               //Return 304 response
+                               return $response;
+                       }
+               }
 
-               //Set section
-               $section = $this->translator->trans('Libre Air locations');
+               //Add multi map
+               $this->context['multimap'] = $this->map->getMultiMap($this->translator->trans('Libre Air locations sector map'), $this->modified->getTimestamp(), $this->context['locations']);
+
+               //Set title
+               $this->context['title']['page'] = $this->translator->trans('Libre Air locations');
 
                //Set description
                $this->context['description'] = $this->translator->trans('Libre Air location list');
@@ -284,126 +286,227 @@ class LocationController extends DefaultController {
                        $this->translator->trans('Libre Air')
                ];
 
-               //Set title
-               $title = $this->translator->trans($this->config['site']['title']).' - '.$section;
-
-               //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'
-                       )
-               );
-
                //Create location forms for role_admin
-               if ($this->isGranted('ROLE_ADMIN')) {
+               if ($this->checker->isGranted('ROLE_ADMIN')) {
                        //Fetch all locations
-                       $locations = $doctrine->getRepository(Location::class)->findAll();
-
-                       //Rekey by id
-                       $locations = array_reduce($locations, function($carry, $item){$carry[$item->getId()] = $item; return $carry;}, []);
+                       $locations = $this->doctrine->getRepository(Location::class)->findAll();
 
                        //Init locations to context
                        $this->context['forms']['locations'] = [];
 
                        //Iterate on locations
-                       foreach($locations as $locationId => $location) {
+                       foreach($this->context['locations'] as $id => $location) {
                                //Create LocationType form
-                               $form = $this->createForm('Rapsys\AirBundle\Form\LocationType', $location, [
-                                       //Set the action
-                                       'action' => $this->generateUrl('rapsys_air_location_edit', ['id' => $location->getId()]),
-                                       //Set the form attribute
-                                       'attr' => [],
-                                       //Set block prefix
-                                       //TODO: make this shit works to prevent label collision
-                                       //XXX: see https://stackoverflow.com/questions/8703016/adding-a-prefix-to-a-form-label-for-translation
-                                       'label_prefix' => 'location_'.$locationId
-                               ]);
+                               $form = $this->factory->createNamed(
+                                       //Set form id
+                                       'locations_'.$id,
+                                       //Set form type
+                                       'Rapsys\AirBundle\Form\LocationType',
+                                       //Set form data
+                                       $locations[$location['id']],
+                                       //Set the form attributes
+                                       ['attr' => []]
+                               );
+
+                               //Refill the fields in case of invalid form
+                               $form->handleRequest($request);
+
+                               //Handle valid form
+                               if ($form->isSubmitted() && $form->isValid()) {
+                                       //Get data
+                                       $data = $form->getData();
+
+                                       //Set updated
+                                       $data->setUpdated(new \DateTime('now'));
+
+                                       //Queue location save
+                                       $this->manager->persist($data);
+
+                                       //Flush to get the ids
+                                       $this->manager->flush();
+
+                                       //Add notice
+                                       $this->addFlash('notice', $this->translator->trans('Location %id% updated', ['%id%' => $location['id']]));
+
+                                       //Redirect to cleanup the form
+                                       return $this->redirectToRoute('rapsysair_location', ['location' => $location['id']]);
+                               }
 
                                //Add form to context
-                               $this->context['forms']['locations'][$locationId] = $form->createView();
+                               $this->context['forms']['locations'][$id] = $form->createView();
                        }
 
                        //Create LocationType form
-                       $form = $this->createForm('Rapsys\AirBundle\Form\LocationType', null, [
-                               //Set the action
-                               'action' => $this->generateUrl('rapsys_air_location_add'),
-                               //Set the form attribute
-                               'attr' => [ 'class' => 'col' ]
-                       ]);
+                       $form = $this->factory->createNamed(
+                               //Set form id
+                               'locations',
+                               //Set form type
+                               'Rapsys\AirBundle\Form\LocationType',
+                               //Set form data
+                               new Location(),
+                               //Set the form attributes
+                               ['attr' => ['class' => 'col']]
+                       );
+
+                       //Refill the fields in case of invalid form
+                       $form->handleRequest($request);
+
+                       //Handle valid form
+                       if ($form->isSubmitted() && $form->isValid()) {
+                               //Get data
+                               $data = $form->getData();
+
+                               //Queue location save
+                               $this->manager->persist($data);
+
+                               //Flush to get the ids
+                               $this->manager->flush();
+
+                               //Add notice
+                               $this->addFlash('notice', $this->translator->trans('Location created'));
+
+                               //Redirect to cleanup the form
+                               return $this->redirectToRoute('rapsysair_location', ['location' => $data->getId()]);
+                       }
 
                        //Add form to context
                        $this->context['forms']['location'] = $form->createView();
                }
 
-               //Fetch locations
-               //XXX: we want to display all active locations anyway
-               $locations = $doctrine->getRepository(Location::class)->findTranslatedSortedByPeriod($this->translator, $period);
-
                //Render the view
-               return $this->render('@RapsysAir/location/index.html.twig', ['title' => $title, 'section' => $section, 'locations' => $locations]+$this->context);
+               return $this->render('@RapsysAir/location/index.html.twig', $this->context);
        }
 
        /**
         * List all sessions for the location
         *
-        * @desc Display all sessions for the location with an application or login form
+        * Display all sessions for the location with an application or login form
+        *
+        * @TODO: add location edit form ???
         *
         * @param Request $request The request instance
         * @param int $id The location id
+        * @param ?string $location The location slug
         *
         * @return Response The rendered view
         */
-       public function view(Request $request, $id): Response {
-               //Fetch doctrine
-               $doctrine = $this->getDoctrine();
-
-               //Fetch location
-               if (empty($location = $doctrine->getRepository(Location::class)->findOneById($id))) {
+       public function view(Request $request, int $id, ?string $location): Response {
+               //Without location
+               if (empty($this->context['location'] = $this->doctrine->getRepository(Location::class)->findOneByIdAsArray($id, $this->locale))) {
+                       //Throw 404
                        throw $this->createNotFoundException($this->translator->trans('Unable to find location: %id%', ['%id%' => $id]));
                }
 
-               //Set section
-               $section = $this->translator->trans('Argentine Tango at '.$location);
+               //With invalid slug
+               if ($location !== $this->context['location']['slug']) {
+                       //Redirect on correctly spelled location
+                       return $this->redirectToRoute('rapsysair_location_view', ['id' => $this->context['location']['id'], 'location' => $this->context['location']['slug']], Response::HTTP_MOVED_PERMANENTLY);
+               }
 
-               //Set description
-               $this->context['description'] = $this->translator->trans('Outdoor Argentine Tango session calendar %location%', [ '%location%' => $this->translator->trans('at '.$location) ]);
+               //Fetch calendar
+               $this->context['calendar'] = $this->doctrine->getRepository(Session::class)->findAllByPeriodAsCalendarArray($this->period, !$this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED'), $this->context['location']['latitude'], $this->context['location']['longitude']);
+
+               //Set dances
+               $this->context['dances'] = [];
+
+               //Iterate on each calendar
+               foreach($this->context['calendar'] as $date => $calendar) {
+                       //Iterate on each session
+                       foreach($calendar['sessions'] as $sessionId => $session) {
+                               //Session with application dance
+                               if (!empty($session['application']['dance'])) {
+                                       //Add dance
+                                       $this->context['dances'][$session['application']['dance']['id']] = $session['application']['dance'];
+                               }
+                       }
+               }
+
+               //Get locations at less than 2 km
+               $this->context['locations'] = $this->doctrine->getRepository(Location::class)->findAllByLatitudeLongitudeAsArray($this->context['location']['latitude'], $this->context['location']['longitude'], $this->period, 2);
+
+               //Set modified
+               //XXX: dance modified is already computed inside calendar modified
+               $this->modified = max(array_merge([$this->context['location']['modified']], array_map(function ($v) { return $v['modified']; }, array_merge($this->context['calendar'], $this->context['locations']))));
+
+               //Create response
+               $response = new Response();
+
+               //With logged user
+               if ($this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
+                       //Set last modified
+                       $response->setLastModified(new \DateTime('-1 year'));
+
+                       //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['location'], $this->context['calendar'], $this->context['locations']))));
+
+                       //Set last modified
+                       $response->setLastModified($this->modified);
+
+                       //Set as public
+                       $response->setPublic();
+
+                       //Without role and modification
+                       if ($response->isNotModified($request)) {
+                               //Return 304 response
+                               return $response;
+                       }
+               }
+
+               //Add multi map
+               $this->context['multimap'] = $this->map->getMultiMap($this->context['location']['multimap'], $this->modified->getTimestamp(), $this->context['locations']);
 
                //Set keywords
                $this->context['keywords'] = [
-                       $this->translator->trans($location),
-                       $this->translator->trans('outdoor'),
-                       $this->translator->trans('Argentine Tango'),
-                       $this->translator->trans('calendar')
+                       $this->context['location']['title'],
+                       $this->context['location']['city']['title'],
+                       $this->translator->trans($this->context['location']['indoor']?'Indoor':'Outdoor'),
+                       $this->translator->trans('Calendar'),
+                       $this->translator->trans('Libre Air')
                ];
 
-               //Set title
-               $title = $this->translator->trans($this->config['site']['title']).' - '.$section;
-
-               //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'
-                       )
-               );
+               //With dances
+               if (!empty($this->context['dances'])) {
+                       //Set dances
+                       $dances = array_map(function ($v) { return $v['name']; }, $this->context['dances']);
 
-               //Fetch calendar
-               $calendar = $doctrine->getRepository(Session::class)->fetchCalendarByDatePeriod($this->translator, $period, $id, $request->get('session'), !$this->isGranted('IS_AUTHENTICATED_REMEMBERED'), $request->getLocale());
+                       //Insert dances in keywords
+                       array_splice($this->context['keywords'], 2, 0, $dances);
+
+                       //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 title
+                       $this->context['title']['page'] = $this->translator->trans('%dances% %location%', ['%dances%' => $dances, '%location%' => $this->context['location']['atin']]);
+
+                       //Set description
+                       $this->context['description'] = $this->translator->trans('%dances% indoor and outdoor calendar %location%', ['%dances%' => $dances, '%location%' => $this->context['location']['at']]);
+               //Without dances
+               } else {
+                       //Set title
+                       $this->context['title']['page'] = $this->translator->trans('Dance %location%', ['%location%' => $this->context['location']['atin']]);
+
+                       //Set description
+                       $this->context['description'] = $this->translator->trans('Indoor and outdoor dance calendar %location%', ['%location%' => $this->context['location']['at']]);
+               }
+
+               //Set locations description
+               $this->context['locations_description'] = $this->translator->trans('Libre Air location list %location% %city%', ['%location%' => $this->context['location']['around'], '%city%' => $this->context['location']['city']['in']]);
+
+               //Set locations link
+               $this->context['locations_link'] = $this->context['location']['city']['link'];
+
+               //Set locations title
+               $this->context['locations_title'] = $this->context['location']['city']['title'].' ('.$this->context['location']['city']['id'].')';
 
-               //Fetch locations
-               //XXX: we want to display all active locations anyway
-               $locations = $doctrine->getRepository(Location::class)->findTranslatedSortedByPeriod($this->translator, $period);
+               //Set alternates
+               $this->context['alternates'] += $this->context['location']['alternates'];
 
                //Render the view
-               return $this->render('@RapsysAir/location/view.html.twig', ['id' => $id, 'title' => $title, 'section' => $section, 'calendar' => $calendar, 'locations' => $locations]+$this->context);
+               return $this->render('@RapsysAir/location/view.html.twig', $this->context, $response);
        }
 }
index aca7c2988174fe1ec9fe0ec69a8f43e1d4917f4a..b2dde1d58b630b37f6c8f15bc4347987537e1733 100644 (file)
@@ -23,6 +23,7 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 use Symfony\Component\Routing\RequestContext;
 
 use Rapsys\AirBundle\Entity\Application;
+use Rapsys\AirBundle\Entity\Dance;
 use Rapsys\AirBundle\Entity\User;
 use Rapsys\AirBundle\Entity\Slot;
 use Rapsys\AirBundle\Entity\Session;
@@ -30,498 +31,127 @@ use Rapsys\AirBundle\Entity\Location;
 
 class SessionController extends AbstractController {
        /**
-        * Edit session
+        * List all sessions
         *
-        * @desc Persist session and all required dependencies in database
+        * Display all sessions with an application or login form
         *
         * @param Request $request The request instance
         *
-        * @return Response The rendered view or redirection
-        *
-        * @throws \RuntimeException When user has not at least guest role
+        * @return Response The rendered view
         */
-       public function edit(Request $request, $id): Response {
-               //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')]));
-
-               //Reject non post requests
-               if (!$request->isMethod('POST')) {
-                       throw new \RuntimeException('Request method MUST be POST');
-               }
-
-               //Get doctrine
-               $doctrine = $this->getDoctrine();
-
-               //Set locale
-               $locale = $request->getLocale();
-
-               //Fetch session
-               $session = $doctrine->getRepository(Session::class)->fetchOneById($id, $locale);
-
-               //Check if
-               if (
-                       //we are admin
-                       !$this->isGranted('ROLE_ADMIN') &&
-                       //or attributed user
-                       $this->getUser()->getId() != $session['au_id'] &&
-                       //or application without attributed user
-                       $session['au_id'] !== null && !in_array($this->getUser()->getId(), explode("\n", $session['sau_id']))
-               ) {
-                       //Prevent non admin and non attributed user access
-                       throw $this->createAccessDeniedException();
-               }
-
-               //Set now
-               $now = new \DateTime('now');
-
-               //Create SessionType form
-               $form = $this->createForm('Rapsys\AirBundle\Form\SessionType', null, [
-                       //Set the action
-                       'action' => $this->generateUrl('rapsys_air_session_edit', [ 'id' => $id ]),
-                       //Set the form attribute
-                       'attr' => [],
-                       //Set admin
-                       'admin' => $this->isGranted('ROLE_ADMIN'),
-                       //Set default user to current
-                       'user' => $this->getUser()->getId(),
-                       //Set date
-                       'date' => $session['date'],
-                       //Set begin
-                       'begin' => $session['begin'],
-                       //Set length
-                       'length' => $session['length'],
-                       //Set raincancel
-                       'raincancel' => ($this->isGranted('ROLE_ADMIN') || $this->getUser()->getId() == $session['au_id']) && $session['rainfall'] >= 2,
-                       //Set cancel
-                       'cancel' => $this->isGranted('ROLE_ADMIN') || in_array($this->getUser()->getId(), explode("\n", $session['sau_id'])),
-                       //Set modify
-                       'modify' => $this->isGranted('ROLE_ADMIN') || $this->getUser()->getId() == $session['au_id'] && $session['stop'] >= $now && $this->isGranted('ROLE_REGULAR'),
-                       //Set move
-                       'move' => $this->isGranted('ROLE_ADMIN') || $this->getUser()->getId() == $session['au_id'] && $session['stop'] >= $now && $this->isGranted('ROLE_SENIOR'),
-                       //Set attribute
-                       'attribute' => $this->isGranted('ROLE_ADMIN') && $session['locked'] === null,
-                       //Set session
-                       'session' => $session['id']
-               ]);
-
-               //Refill the fields in case of invalid form
-               $form->handleRequest($request);
-
-               //Handle invalid data
-               if (!$form->isSubmitted() || !$form->isValid()) {
-                       //Set page
-                       $this->context['title'] = $this->translator->trans(!empty($session['au_id'])?'Session %id% by %pseudonym%':'Session %id%', ['%id%' => $id, '%pseudonym%' => $session['au_pseudonym']]);
-
-                       //Set facebook title
-                       $this->context['facebook']['metas']['og:title'] = $this->context['title'].' '.$this->translator->trans('at '.$session['l_title']);
-
-                       //Set section
-                       $this->context['section'] = $this->translator->trans($session['l_title']);
-
-                       //Set localization date formater
-                       $intl = new \IntlDateFormatter($locale, \IntlDateFormatter::GREGORIAN, \IntlDateFormatter::SHORT);
-
-                       //Set description
-                       $this->context['description'] = $this->translator->trans('Outdoor Argentine Tango session the %date%', [ '%date%' => $intl->format($session['start']) ]);
-
-                       //Set localization date formater
-                       $intlDate = new \IntlDateFormatter($locale, \IntlDateFormatter::TRADITIONAL, \IntlDateFormatter::NONE);
-
-                       //Set localization time formater
-                       $intlTime = new \IntlDateFormatter($locale, \IntlDateFormatter::NONE, \IntlDateFormatter::SHORT);
-
-                       //Set facebook image
-                       $this->context['facebook'] += [
-                               'texts' => [
-                                       $session['au_pseudonym'] => [
-                                               'font' => 'irishgrover',
-                                               'size' => 110
-                                       ],
-                                       ucfirst($intlDate->format($session['start']))."\n".$this->translator->trans('From %start% to %stop%', ['%start%' => $intlTime->format($session['start']), '%stop%' => $intlTime->format($session['stop'])]) => [
-                                               'align' => 'left'
-                                       ],
-                                       $this->translator->trans('at '.$session['l_title']) => [
-                                               'align' => 'right',
-                                               'font' => 'labelleaurore',
-                                               'size' => 75
-                                       ]
-                               ],
-                               'updated' => $session['updated']->format('U')
-                       ];
-
-                       //Add session in context
-                       $this->context['session'] = [
-                               'id' => $id,
-                               'title' => $this->translator->trans('Session %id%', ['%id%' => $id]),
-                               'location' => [
-                                       'id' => $session['l_id'],
-                                       'at' => $this->translator->trans('at '.$session['l_title'])
-                               ]
-                       ];
-
-                       //Render the view
-                       return $this->render('@RapsysAir/session/edit.html.twig', ['form' => $form->createView()]+$this->context);
-               }
-
-               //Get manager
-               $manager = $doctrine->getManager();
-
-               //Get data
-               $data = $form->getData();
-
-               //Fetch session
-               $session = $doctrine->getRepository(Session::class)->findOneById($id);
-
-               //Set user
-               $user = $this->getUser();
-
-               //Replace with requested user for admin
-               if ($this->isGranted('ROLE_ADMIN') && !empty($data['user'])) {
-                       $user = $doctrine->getRepository(User::class)->findOneById($data['user']);
-               }
-
-               //Set datetime
-               $datetime = new \DateTime('now');
-
-               //Set canceled time at start minus one day
-               $canceled = (clone $session->getStart())->sub(new \DateInterval('P1D'));
-
-               //Set action
-               $action = [
-                       'raincancel' => $form->has('raincancel') && $form->get('raincancel')->isClicked(),
-                       'modify' => $form->has('modify') && $form->get('modify')->isClicked(),
-                       'move' => $form->has('move') && $form->get('move')->isClicked(),
-                       'cancel' => $form->has('cancel') && $form->get('cancel')->isClicked(),
-                       'forcecancel' => $form->has('forcecancel') && $form->get('forcecancel')->isClicked(),
-                       'attribute' => $form->has('attribute') && $form->get('attribute')->isClicked(),
-                       'autoattribute' => $form->has('autoattribute') && $form->get('autoattribute')->isClicked(),
-                       'lock' => $form->has('lock') && $form->get('lock')->isClicked(),
-               ];
-
-               //With raincancel and application and (rainfall or admin)
-               if ($action['raincancel'] && ($application = $session->getApplication()) && ($session->getRainfall() >= 2 || $this->isGranted('ROLE_ADMIN'))) {
-                       //Cancel application at start minus one day
-                       $application->setCanceled($canceled);
-
-                       //Update time
-                       $application->setUpdated($datetime);
-
-                       //Insufficient rainfall
-                       //XXX: is admin
-                       if ($session->getRainfall() < 2) {
-                               //Set score
-                               //XXX: magic cheat score 42
-                               $application->setScore(42);
-                       }
-
-                       //Queue application save
-                       $manager->persist($application);
-
-                       //Add notice in flash message
-                       $this->addFlash('notice', $this->translator->trans('Application %id% updated', ['%id%' => $application->getId()]));
-
-                       //Update time
-                       $session->setUpdated($datetime);
-
-                       //Queue session save
-                       $manager->persist($session);
-
-                       //Add notice in flash message
-                       $this->addFlash('notice', $this->translator->trans('Session %id% updated', ['%id%' => $id]));
-               //With modify
-               } elseif ($action['modify']) {
-                       //With admin
-                       if ($this->isGranted('ROLE_ADMIN')) {
-                               //Set date
-                               $session->setDate($data['date']);
-                       }
-
-                       //Set begin
-                       $session->setBegin($data['begin']);
-
-                       //Set length
-                       $session->setLength($data['length']);
-
-                       //Update time
-                       $session->setUpdated($datetime);
-
-                       //Queue session save
-                       $manager->persist($session);
-
-                       //Add notice in flash message
-                       $this->addFlash('notice', $this->translator->trans('Session %id% updated', ['%id%' => $id]));
-               //With move
-               } elseif ($action['move']) {
-                       //Set location
-                       $session->setLocation($doctrine->getRepository(Location::class)->findOneById($data['location']));
-
-                       //Update time
-                       $session->setUpdated($datetime);
-
-                       //Queue session save
-                       $manager->persist($session);
-
-                       //Add notice in flash message
-                       $this->addFlash('notice', $this->translator->trans('Session %id% updated', ['%id%' => $id]));
-               //With cancel or forcecancel
-               } elseif ($action['cancel'] || $action['forcecancel']) {
-                       //Get application
-                       $application = $doctrine->getRepository(Application::class)->findOneBySessionUser($session, $user);
-
-                       //Not already canceled
-                       if ($application->getCanceled() === null) {
-                               //Cancel application
-                               $application->setCanceled($datetime);
-
-                               //Check if application is session application and (canceled 24h before start or forcecancel (as admin))
-                               #if ($session->getApplication() == $application && ($datetime < $canceled || $action['forcecancel'])) {
-                               if ($session->getApplication() == $application && $action['forcecancel']) {
-                                       //Set score
-                                       //XXX: magic cheat score 42
-                                       $application->setScore(42);
-
-                                       //Unattribute session
-                                       $session->setApplication(null);
-
-                                       //Update time
-                                       $session->setUpdated($datetime);
-
-                                       //Queue session save
-                                       $manager->persist($session);
-
-                                       //Add notice in flash message
-                                       $this->addFlash('notice', $this->translator->trans('Session %id% updated', ['%id%' => $id]));
-                               }
-                       //Already canceled
-                       } else {
-                               //Uncancel application
-                               $application->setCanceled(null);
-                       }
-
-                       //Update time
-                       $application->setUpdated($datetime);
-
-                       //Queue application save
-                       $manager->persist($application);
-
-                       //Add notice in flash message
-                       $this->addFlash('notice', $this->translator->trans('Application %id% updated', ['%id%' => $application->getId()]));
-               //With attribute
-               } elseif ($action['attribute']) {
-                       //Get application
-                       $application = $doctrine->getRepository(Application::class)->findOneBySessionUser($session, $user);
-
-                       //Already canceled
-                       if ($application->getCanceled() !== null) {
-                               //Uncancel application
-                               $application->setCanceled(null);
-                       }
-
-                       //Set score
-                       //XXX: magic cheat score 42
-                       $application->setScore(42);
+       public function index(Request $request): Response {
+               //Get locations
+               $this->context['locations'] = $this->doctrine->getRepository(Location::class)->findAllAsArray($this->period);
 
-                       //Update time
-                       $application->setUpdated($datetime);
+               //Add cities
+               $this->context['cities'] = $this->doctrine->getRepository(Location::class)->findCitiesAsArray($this->period);
 
-                       //Queue application save
-                       $manager->persist($application);
+               //Add calendar
+               $this->context['calendar'] = $this->doctrine->getRepository(Session::class)->findAllByPeriodAsCalendarArray($this->period, !$this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED'), null, null, 1);
 
-                       //Add notice in flash message
-                       $this->addFlash('notice', $this->translator->trans('Application %id% updated', ['%id%' => $application->getId()]));
+               //Add dances
+               $this->context['dances'] = $this->doctrine->getRepository(Dance::class)->findNamesAsArray();
 
-                       //Unattribute session
-                       $session->setApplication($application);
+               //Set modified
+               $this->modified = max(array_map(function ($v) { return $v['modified']; }, array_merge($this->context['calendar'], $this->context['cities'], $this->context['dances'])));
 
-                       //Update time
-                       $session->setUpdated($datetime);
+               //Create response
+               $response = new Response();
 
-                       //Queue session save
-                       $manager->persist($session);
+               //With logged user
+               if ($this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
+                       //Set last modified
+                       $response->setLastModified(new \DateTime('-1 year'));
 
-                       //Add notice in flash message
-                       $this->addFlash('notice', $this->translator->trans('Session %id% updated', ['%id%' => $id]));
-               //With autoattribute
-               } elseif ($action['autoattribute']) {
-                       //Get best application
-                       //XXX: best application may not issue result while grace time or bad behaviour
-                       if (!empty($application = $doctrine->getRepository(Session::class)->findBestApplicationById($id))) {
-                               //Attribute session
-                               $session->setApplication($application);
+                       //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']))));
 
-                               //Update time
-                               $session->setUpdated($datetime);
+                       //Set last modified
+                       $response->setLastModified($this->modified);
 
-                               //Queue session save
-                               $manager->persist($session);
+                       //Set as public
+                       $response->setPublic();
 
-                               //Add notice in flash message
-                               $this->addFlash('notice', $this->translator->trans('Session %id% auto attributed', ['%id%' => $id]));
-                       //No application
-                       } else {
-                               //Add warning in flash message
-                               $this->addFlash('warning', $this->translator->trans('Session %id% not auto attributed', ['%id%' => $id]));
+                       //Without role and modification
+                       if ($response->isNotModified($request)) {
+                               //Return 304 response
+                               return $response;
                        }
-               //With lock
-               } elseif ($action['lock']) {
-                       //Already locked
-                       if ($session->getLocked() !== null) {
-                               //Set uncanceled
-                               $canceled = null;
-
-                               //Unlock session
-                               $session->setLocked(null);
-                       //Not locked
-                       } else {
-                               //Get application
-                               if ($application = $session->getApplication()) {
-                                       //Set score
-                                       //XXX: magic cheat score 42
-                                       $application->setScore(42);
-
-                                       //Update time
-                                       $application->setUpdated($datetime);
-
-                                       //Queue application save
-                                       $manager->persist($application);
+               }
 
-                                       //Add notice in flash message
-                                       $this->addFlash('notice', $this->translator->trans('Application %id% updated', ['%id%' => $application->getId()]));
+               //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;
                                }
-
-                               //Unattribute session
-                               $session->setApplication(null);
-
-                               //Lock session
-                               $session->setLocked($datetime);
                        }
 
-                       //Update time
-                       $session->setUpdated($datetime);
+                       //Add multi
+                       $this->context['multimap'] = $this->map->getMultiMap($this->translator->trans('Libre Air cities sector map'), $this->modified->getTimestamp(), $locations);
 
-                       //Queue session save
-                       $manager->persist($session);
+                       //Set cities
+                       $cities = array_map(function ($v) { return $v['in']; }, $this->context['cities']);
 
-                       //Add notice in flash message
-                       $this->addFlash('notice', $this->translator->trans('Session %id% updated', ['%id%' => $id]));
-               //Unknown action
+                       //Set dances
+                       $dances = array_map(function ($v) { return $v['name']; }, $this->context['dances']);
                } else {
-                       //Add warning in flash message
-                       $this->addFlash('warning', $this->translator->trans('Session %id% not updated', ['%id%' => $id]));
-               }
-
-               //Flush to get the ids
-               $manager->flush();
-
-               //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();
+                       //Set cities
+                       $cities = [];
 
-                       //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']);
-
-                               //Generate url
-                               return $this->redirectToRoute($name, $route);
-                       //No route matched
-                       } catch(MethodNotAllowedException|ResourceNotFoundException $e) {
-                               //Unset referer to fallback to default route
-                               unset($referer);
-                       }
+                       //Set dances
+                       $dances = [];
                }
 
-               //Redirect to cleanup the form
-               return $this->redirectToRoute('rapsys_air_session_view', ['id' => $id]);
-       }
-
-       /**
-        * List all sessions
-        *
-        * @desc Display all 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();
-
-               //Set section
-               $section = $this->translator->trans('Sessions');
-
-               //Set description
-               $this->context['description'] = $this->translator->trans('Libre Air session list');
-
                //Set keywords
-               $this->context['keywords'] = [
-                       $this->translator->trans('sessions'),
-                       $this->translator->trans('session list'),
-                       $this->translator->trans('listing'),
-                       $this->translator->trans('Libre Air')
-               ];
-
-               //Set title
-               $title = $this->translator->trans($this->config['site']['title']).' - '.$section;
-
-               //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'
+               //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('sessions'),
+                                       $this->translator->trans('session list'),
+                                       $this->translator->trans('listing'),
+                                       $this->translator->trans('Libre Air')
+                               ]
                        )
                );
 
-               //Fetch calendar
-               //TODO: highlight with current session route parameter
-               $calendar = $doctrine->getRepository(Session::class)->fetchCalendarByDatePeriod($this->translator, $period, null, $request->get('session'), !$this->isGranted('IS_AUTHENTICATED_REMEMBERED'), $request->getLocale());
+               //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'));
+
+               //Set title
+               $this->context['title']['page'] = $this->translator->trans('%dances% %cities% sessions', ['%dances%' => $dances, '%cities%' => $cities]);
+
+               //Set description
+               $this->context['description'] = $this->translator->trans('%dances% indoor and outdoor session calendar %cities%', ['%dances%' => $dances, '%cities%' => $cities]);
 
                //Render the view
-               return $this->render('@RapsysAir/session/index.html.twig', ['title' => $title, 'section' => $section, 'calendar' => $calendar, 'locations' => $locations]+$this->context);
+               return $this->render('@RapsysAir/session/index.html.twig', $this->context);
        }
 
        /**
         * List all sessions for tango argentin
         *
-        * @desc Display all sessions in tango argentin json format
+        * Display all sessions in tango argentin json format
         *
         * @todo Drop it if unused by tangoargentin ???
         *
@@ -530,72 +160,40 @@ class SessionController extends AbstractController {
         * @return Response The rendered view or redirection
         */
        public function tangoargentin(Request $request): Response {
-               //Fetch doctrine
-               $doctrine = $this->getDoctrine();
-
-               //Compute period
-               $period = new \DatePeriod(
-                       //Start from first monday of week
-                       new \DateTime('today'),
-                       //Iterate on each day
-                       new \DateInterval('P1D'),
-                       //End with next sunday and 4 weeks
-                       new \DateTime('+2 week')
-               );
-
                //Retrieve events to update
-               $sessions = $doctrine->getRepository(Session::class)->fetchAllByDatePeriod($period, $request->getLocale());
+               $sessions = $this->doctrine->getRepository(Session::class)->findAllByPeriodAsCalendarArray($this->period);
 
                //Init return array
                $ret = [];
 
+               //Flatten sessions tree
+               $sessions = array_reduce($sessions, function ($c, $v) { return array_merge($c, $v['sessions']); }, []);
+
                //Iterate on sessions
                foreach($sessions as $sessionId => $session) {
-                       //Set title
-                       $title = $session['au_pseudonym'].' '.$this->translator->trans('at '.$session['l_title']);
-
-                       //Use Transliterator if available
-                       if (class_exists('Transliterator')) {
-                               $trans = \Transliterator::create('Any-Latin; Latin-ASCII; Upper()');
-                               $title = $trans->transliterate($title);
-                       } else {
-                               $title = strtoupper($title);
-                       }
+                       //Set route params
+                       $routeParams = $this->router->match($session['link']);
 
-                       //Set rate
-                       $rate = 'Au chapeau';
+                       //Set route
+                       $route = $routeParams['_route'];
 
-                       //Without hat
-                       if ($session['p_hat'] === null) {
-                               //Set rate
-                               $rate = 'Gratuit';
+                       //Drop _route from route params
+                       unset($routeParams['_route']);
 
-                               //With rate
-                               if ($session['p_rate'] !== null) {
-                                       //Set rate
-                                       $rate = $session['p_rate'].' €';
-                               }
-                       //With hat
-                       } else {
-                               //With rate
-                               if ($session['p_rate'] !== null) {
-                                       //Set rate
-                                       $rate .= ', idéalement '.$session['p_rate'].' €';
-                               }
-                       }
-
-                       //Store session data
-                       $ret[$sessionId] = [
+                       //Add session
+                       $ret[$session['id']] = [
                                'start' => $session['start']->format(\DateTime::ISO8601),
                                'stop' => $session['start']->format(\DateTime::ISO8601),
-                               'title' => $title,
-                               'short' => $session['p_short'],
-                               'rate' => $rate,
-                               'location' => implode(' ', [$session['l_address'], $session['l_zipcode'], $session['l_city']]),
-                               'status' => (empty($session['a_canceled']) && empty($session['locked']))?'confirmed':'cancelled',
-                               'updated' => $session['updated']->format(\DateTime::ISO8601),
-                               'organizer' => $session['au_forename'],
-                               'source' => $this->router->generate('rapsys_air_session_view', ['id' => $sessionId], UrlGeneratorInterface::ABSOLUTE_URL)
+                               'fromto' => $this->translator->trans('from %start% to %stop%', ['%start%' => $session['start']->format('H\hi'), '%stop%' => $session['stop']->format('H\hi')]),
+                               'title' => $this->slugger->latin($session['application']['user']['title'])/*.' '.$this->translator->trans('at '.$session['location']['title'])*/,
+                               'short' => $session['rate']['short'],
+                               'rate' => $session['rate']['title'],
+                               'location' => implode(' ', [$session['location']['address'], $session['location']['zipcode'], $session['location']['city']]),
+                               'status' => in_array('canceled', $session['class'])?'annulé':'confirmé',
+                               'modified' => $session['modified']->format(\DateTime::ISO8601),
+                               #'organizer' => $session['application']['user']['title'],
+                               #'source' => $this->router->generate('rapsysair_session_view', ['id' => $sessionId, 'location' => $this->translator->trans($session['l_title'])], UrlGeneratorInterface::ABSOLUTE_URL)
+                               'source' => $this->router->generate($route, $routeParams, UrlGeneratorInterface::ABSOLUTE_URL)
                        ];
                }
 
@@ -616,33 +214,39 @@ class SessionController extends AbstractController {
         * @todo XXX: TODO: like described in: https://www.alsacreations.com/article/lire/1400-attribut-rel-relations.html#xnf-rel-attribute
         * @todo XXX: TODO: or here: http://microformats.org/wiki/existing-rel-values#HTML5_link_type_extensions
         *
-        * @desc Display session by id with an application or login form
+        * @todo: generate a background from @RapsysAir/Resources/public/location/<location>.png or @RapsysAir/Resources/public/location/<user>/<location>.png when available
+        *
+        * @todo: generate a share picture @RapsysAir/seance/363/place-saint-sulpice/bal-et-cours-de-tango-argentin/milonga-raphael/share.jpeg ?
+        * (with date, organiser, type, location, times and logo ?)
+        *
+        * @todo: add picture stuff about location ???
         *
         * @param Request $request The request instance
         * @param int $id The session id
         *
         * @return Response The rendered view
+        *
+        * @throws NotFoundHttpException When session is not found
         */
-       public function view(Request $request, $id): Response {
-               //Fetch doctrine
-               $doctrine = $this->getDoctrine();
-
-               //Set locale
-               $locale = $request->getLocale();
-
+       public function view(Request $request, int $id): Response {
                //Fetch session
-               if (empty($session = $doctrine->getRepository(Session::class)->fetchOneById($id, $locale))) {
+               if (empty($this->context['session'] = $this->doctrine->getRepository(Session::class)->findOneByIdAsArray($id))) {
+                       //Session not found
                        throw $this->createNotFoundException($this->translator->trans('Unable to find session: %id%', ['%id%' => $id]));
                }
 
+               //Get locations at less than 1 km
+               $this->context['locations'] = $this->doctrine->getRepository(Location::class)->findAllByLatitudeLongitudeAsArray($this->context['session']['location']['latitude'], $this->context['session']['location']['longitude'], $this->period, 2);
+
+               //Set modified
+               //XXX: dance modified is already computed inside calendar modified
+               $this->modified = max(array_merge([$this->context['session']['modified']], array_map(function ($v) { return $v['modified']; }, $this->context['locations'])));
+
                //Create response
                $response = new Response();
 
-               //Set etag
-               $response->setEtag(md5(serialize($session)));
-
                //With logged user
-               if ($this->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
+               if ($this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
                        //Set last modified
                        $response->setLastModified(new \DateTime('-1 year'));
 
@@ -650,14 +254,12 @@ class SessionController extends AbstractController {
                        $response->setPrivate();
                //Without logged user
                } else {
-                       //Extract applications updated
-                       $session['sa_updated'] = array_map(function($v){return new \DateTime($v);}, explode("\n", $session['sa_updated']));
-
-                       //Get last modified
-                       $lastModified = max(array_merge([$session['updated'], $session['l_updated'], $session['t_updated'], $session['p_updated']], $session['sa_updated']));
+                       //Set etag
+                       //XXX: only for public to force revalidation by last modified
+                       $response->setEtag(md5(serialize(array_merge($this->context['session'], $this->context['locations']))));
 
                        //Set last modified
-                       $response->setLastModified($lastModified);
+                       $response->setLastModified($this->modified);
 
                        //Set as public
                        $response->setPublic();
@@ -669,216 +271,487 @@ class SessionController extends AbstractController {
                        }
                }
 
-               //Set localization date formater
-               $intl = new \IntlDateFormatter($locale, \IntlDateFormatter::GREGORIAN, \IntlDateFormatter::SHORT);
+               //Get route
+               $route = $request->attributes->get('_route');
 
-               //Set section
-               $this->context['section'] = $this->translator->trans($session['l_title']);
+               //Get route params
+               $routeParams = $request->attributes->get('_route_params');
 
-               //Set description
-               $this->context['description'] = $this->translator->trans('Outdoor Argentine Tango session the %date%', [ '%date%' => $intl->format($session['start']) ]);
+               //Disable redirect
+               $redirect = false;
 
-               //Set keywords
-               $this->context['keywords'] = [
-                       $this->translator->trans('outdoor'),
-                       $this->translator->trans('Argentine Tango'),
-               ];
+               //Without location or invalid location
+               if (empty($routeParams['location']) || $this->context['session']['location']['slug'] !== $routeParams['location']) {
+                       //Set location
+                       $routeParams['location'] = $this->context['session']['location']['slug'];
+
+                       //Enable redirect
+                       $redirect = true;
+               }
+
+               //With dance slug without dance or invalid dance
+               if (!empty($this->context['session']['application']['dance']['slug']) && (empty($routeParams['dance']) || $this->context['session']['application']['dance']['slug'] !== $routeParams['dance'])) {
+                       //Set dance
+                       $routeParams['dance'] = $this->context['session']['application']['dance']['slug'];
+
+                       //Enable redirect
+                       $redirect = true;
+               //Without dance slug with dance
+               } elseif (empty($this->context['session']['application']['dance']['slug']) && !empty($routeParams['dance'])) {
+                       //Set dance
+                       unset($routeParams['dance']);
+
+                       //Enable redirect
+                       $redirect = true;
+               }
+
+               //With user slug without user or invalid user
+               if (!empty($this->context['session']['application']['user']['slug']) && (empty($routeParams['user']) || $this->context['session']['application']['user']['slug'] !== $routeParams['user'])) {
+                       //Set user
+                       $routeParams['user'] = $this->context['session']['application']['user']['slug'];
+
+                       //Enable redirect
+                       $redirect = true;
+               //Without user slug with user
+               } elseif (empty($this->context['session']['application']['user']['slug']) && !empty($routeParams['user'])) {
+                       //Set user
+                       unset($routeParams['user']);
+
+                       //Enable redirect
+                       $redirect = true;
+               }
+
+               //With redirect
+               if ($redirect) {
+                       //Redirect to route
+                       return $this->redirectToRoute($route, $routeParams, $this->context['session']['stop'] <= new \DateTime('now') ? Response::HTTP_MOVED_PERMANENTLY : Response::HTTP_FOUND);
+               }
+
+               //Add map
+               $this->context['map'] = $this->map->getMap($this->context['session']['location']['map'], $this->modified->getTimestamp(), $this->context['session']['location']['latitude'], $this->context['session']['location']['longitude']);
+
+               //Add multi map
+               $this->context['multimap'] = $this->map->getMultiMap($this->context['session']['location']['multimap'], $this->modified->getTimestamp(), $this->context['locations']);
+
+               //Set canonical
+               $this->context['canonical'] = $this->context['session']['canonical'];
+
+               //Set alternates
+               $this->context['alternates'] = $this->context['session']['alternates'];
 
                //Set localization date formater
-               $intlDate = new \IntlDateFormatter($locale, \IntlDateFormatter::TRADITIONAL, \IntlDateFormatter::NONE);
+               $intlDate = new \IntlDateFormatter($this->locale, \IntlDateFormatter::TRADITIONAL, \IntlDateFormatter::NONE);
 
                //Set localization time formater
-               $intlTime = new \IntlDateFormatter($locale, \IntlDateFormatter::NONE, \IntlDateFormatter::SHORT);
+               $intlTime = new \IntlDateFormatter($this->locale, \IntlDateFormatter::NONE, \IntlDateFormatter::SHORT);
+
+               //With application
+               if (!empty($this->context['session']['application'])) {
+                       //Set title
+                       $this->context['title']['page'] = $this->translator->trans('%dance% %id% by %pseudonym%', ['%id%' => $id, '%dance%' => $this->context['session']['application']['dance']['title'], '%pseudonym%' => $this->context['session']['application']['user']['title']]);
+
+                       //Set description
+                       $this->context['description'] = ucfirst($this->translator->trans('%dance% %location% %city% %slot% on %date% at %time%', [
+                               '%dance%' => $this->context['session']['application']['dance']['title'],
+                               '%location%' => $this->context['session']['location']['at'],
+                               '%city%' => $this->context['session']['location']['in'],
+                               '%slot%' => $this->context['session']['slot']['the'],
+                               '%date%' => $intlDate->format($this->context['session']['start']),
+                               '%time%' => $intlTime->format($this->context['session']['start']),
+                       ]));
+
+                       //Set keywords
+                       //TODO: readd outdoor ???
+                       $this->context['keywords'] = [
+                               $this->context['session']['application']['dance']['type'],
+                               $this->context['session']['application']['dance']['name'],
+                               $this->context['session']['location']['title'],
+                               $this->context['session']['application']['user']['title'],
+                               $this->translator->trans($this->context['session']['location']['indoor']?'indoor':'outdoor')
+                       ];
+               //Without application
+               } else {
+                       //Set title
+                       $this->context['title']['page'] = $this->translator->trans('Session %id%', ['%id%' => $id]);
+
+                       //Set description
+                       $this->context['description'] = ucfirst($this->translator->trans('%location% %city% %slot% on %date% at %time%', [
+                               '%city%' => ucfirst($this->context['session']['location']['in']),
+                               '%location%' => $this->context['session']['location']['at'],
+                               '%slot%' => $this->context['session']['slot']['the'],
+                               '%date%' => $intlDate->format($this->context['session']['start']),
+                               '%time%' => $intlTime->format($this->context['session']['start'])
+                       ]));
+
+                       //Add dance type
+                       //TODO: readd outdoor ???
+                       $this->context['keywords'] = [
+                               $this->context['session']['location']['title'],
+                               $this->translator->trans($this->context['session']['location']['indoor']?'indoor':'outdoor')
+                       ];
+               }
+
+               //Set section
+               $this->context['section'] = $this->context['session']['location']['title'];
+
+               //Set facebook title
+               $this->context['facebook']['og:title'] = $this->context['title']['page'].' '.$this->context['session']['location']['at'];
 
                //Set facebook image
-               $this->context['facebook'] = [
+               $this->context['fbimage'] = [
                        'texts' => [
-                               $session['au_pseudonym'] => [
+                               $this->context['session']['application']['user']['title']??$this->context['title']['page'] => [
                                        'font' => 'irishgrover',
                                        'size' => 110
                                ],
-                               ucfirst($intlDate->format($session['start']))."\n".$this->translator->trans('From %start% to %stop%', ['%start%' => $intlTime->format($session['start']), '%stop%' => $intlTime->format($session['stop'])]) => [
+                               ucfirst($intlDate->format($this->context['session']['start']))."\n".$this->translator->trans('Around %start% until %stop%', ['%start%' => $intlTime->format($this->context['session']['start']), '%stop%' => $intlTime->format($this->context['session']['stop'])]) => [
+                                       'font' => 'irishgrover',
                                        'align' => 'left'
                                ],
-                               $this->translator->trans('at '.$session['l_title']) => [
+                               $this->context['session']['location']['at'] => [
                                        'align' => 'right',
                                        'font' => 'labelleaurore',
                                        'size' => 75
                                ]
                        ],
-                       'updated' => $session['updated']->format('U')
+                       'updated' => $this->context['session']['updated']->format('U')
                ]+$this->context['facebook'];
 
-               //With granted session
-               if (!empty($session['au_id'])) {
-                       array_unshift($this->context['keywords'], $session['au_pseudonym']);
-               }
-
-               //Set page
-               $this->context['title'] = $this->translator->trans(!empty($session['au_id'])?'Session %id% by %pseudonym%':'Session %id%', ['%id%' => $id, '%pseudonym%' => $session['au_pseudonym']]);
-
-               //Set facebook title
-               $this->context['facebook']['metas']['og:title'] = $this->context['title'].' '.$this->translator->trans('at '.$session['l_title']);
-
                //Create application form for role_guest
-               if ($this->isGranted('ROLE_GUEST')) {
+               if ($this->checker->isGranted('ROLE_GUEST')) {
                        //Set now
                        $now = new \DateTime('now');
 
+                       //Default favorites and dances
+                       $danceFavorites = $dances = [];
+                       //Default dance
+                       $danceDefault = null;
+
+                       //With admin
+                       if ($this->checker->isGranted('ROLE_ADMIN')) {
+                               //Get favorites dances
+                               $danceFavorites = $this->doctrine->getRepository(Dance::class)->findByUserId($this->security->getUser()->getId());
+
+                               //Get dances
+                               $dances = $this->doctrine->getRepository(Dance::class)->findAllIndexed();
+
+                               //Set dance default
+                               $danceDefault = !empty($this->context['session']['application'])?$dances[$this->context['session']['application']['dance']['id']]:null;
+                       }
+
                        //Create SessionType form
-                       $sessionForm = $this->createForm('Rapsys\AirBundle\Form\SessionType', null, [
+                       //TODO: move to named form ???
+                       $sessionForm = $this->factory->create('Rapsys\AirBundle\Form\SessionType', null, [
                                //Set the action
-                               'action' => $this->generateUrl('rapsys_air_session_edit', [ 'id' => $id ]),
+                               'action' => $this->generateUrl('rapsysair_session_view', ['id' => $id, 'location' => $this->context['session']['location']['slug'], 'dance' => $this->context['session']['application']['dance']['slug']??null, 'user' => $this->context['session']['application']['user']['slug']??null]),
                                //Set the form attribute
                                'attr' => [ 'class' => 'col' ],
                                //Set admin
-                               'admin' => $this->isGranted('ROLE_ADMIN'),
+                               'admin' => $this->checker->isGranted('ROLE_ADMIN'),
+                               //Set dance choices
+                               'dance_choices' => $dances,
+                               //Set dance default
+                               'dance_default' => $danceDefault,
+                               //Set dance favorites
+                               'dance_favorites' => $danceFavorites,
+                               //Set to session slot or evening by default
+                               //XXX: default to Evening (3)
+                               'slot_default' => $this->doctrine->getRepository(Slot::class)->findOneById($this->context['session']['slot']['id']??3),
                                //Set default user to current
-                               'user' => $this->getUser()->getId(),
+                               'user' => $this->security->getUser()->getId(),
                                //Set date
-                               'date' => $session['date'],
+                               'date' => $this->context['session']['date'],
                                //Set begin
-                               'begin' => $session['begin'],
+                               'begin' => $this->context['session']['begin'],
                                //Set length
-                               'length' => $session['length'],
+                               'length' => $this->context['session']['length'],
                                //Set raincancel
-                               'raincancel' => ($this->isGranted('ROLE_ADMIN') || $this->getUser()->getId() == $session['au_id']) && $session['rainfall'] >= 2,
+                               'raincancel' => ($this->checker->isGranted('ROLE_ADMIN') || !empty($this->context['session']['application']['user']['id']) && $this->security->getUser()->getId() == $this->context['session']['application']['user']['id']) && $this->context['session']['rainfall'] >= 2,
                                //Set cancel
-                               'cancel' => $this->isGranted('ROLE_ADMIN') || in_array($this->getUser()->getId(), explode("\n", $session['sau_id'])),
+                               'cancel' => $this->checker->isGranted('ROLE_ADMIN') || in_array($this->security->getUser()->getId(), explode("\n", $this->context['session']['sau_id'])),
                                //Set modify
-                               'modify' => $this->isGranted('ROLE_ADMIN') || $this->getUser()->getId() == $session['au_id'] && $session['stop'] >= $now && $this->isGranted('ROLE_REGULAR'),
+                               'modify' => $this->checker->isGranted('ROLE_ADMIN') || !empty($this->context['session']['application']['user']['id']) && $this->security->getUser()->getId() == $this->context['session']['application']['user']['id'] && $this->context['session']['stop'] >= $now && $this->checker->isGranted('ROLE_REGULAR'),
                                //Set move
-                               'move' => $this->isGranted('ROLE_ADMIN') || $this->getUser()->getId() == $session['au_id'] && $session['stop'] >= $now && $this->isGranted('ROLE_SENIOR'),
+                               'move' => $this->checker->isGranted('ROLE_ADMIN') || !empty($this->context['session']['application']['user']['id']) && $this->security->getUser()->getId() == $this->context['session']['application']['user']['id'] && $this->context['session']['stop'] >= $now && $this->checker->isGranted('ROLE_SENIOR'),
                                //Set attribute
-                               'attribute' => $this->isGranted('ROLE_ADMIN') && $session['locked'] === null,
+                               'attribute' => $this->checker->isGranted('ROLE_ADMIN') && $this->context['session']['locked'] === null,
                                //Set session
-                               'session' => $session['id']
+                               'session' => $this->context['session']['id']
                        ]);
 
-                       //Add form to context
-                       $this->context['forms']['session'] = $sessionForm->createView();
-               }
+                       //Refill the fields in case of invalid form
+                       $sessionForm->handleRequest($request);
 
-               //Add session in context
-               $this->context['session'] = [
-                       'id' => $id,
-                       'date' => $session['date'],
-                       'begin' => $session['begin'],
-                       'start' => $session['start'],
-                       'length' => $session['length'],
-                       'stop' => $session['stop'],
-                       'rainfall' => $session['rainfall'] !== null ? $session['rainfall'].' mm' : $session['rainfall'],
-                       'rainrisk' => $session['rainrisk'] !== null ? ($session['rainrisk']*100).' %' : $session['rainrisk'],
-                       'realfeel' => $session['realfeel'] !== null ? $session['realfeel'].' °C' : $session['realfeel'],
-                       'realfeelmin' => $session['realfeelmin'] !== null ? $session['realfeelmin'].' °C' : $session['realfeelmin'],
-                       'realfeelmax' => $session['realfeelmax'] !== null ? $session['realfeelmax'].' °C' : $session['realfeelmax'],
-                       'temperature' => $session['temperature'] !== null ? $session['temperature'].' °C' : $session['temperature'],
-                       'temperaturemin' => $session['temperaturemin'] !== null ? $session['temperaturemin'].' °C' : $session['temperaturemin'],
-                       'temperaturemax' => $session['temperaturemax'] !== null ? $session['temperaturemax'].' °C' : $session['temperaturemax'],
-                       'locked' => $session['locked'],
-                       'created' => $session['created'],
-                       'updated' => $session['updated'],
-                       'title' => $this->translator->trans('Session %id%', ['%id%' => $id]),
-                       'application' => null,
-                       'location' => [
-                               'id' => $session['l_id'],
-                               'at' => $this->translator->trans('at '.$session['l_title']),
-                               'title' => $this->translator->trans($session['l_title']),
-                               'address' => $session['l_address'],
-                               'zipcode' => $session['l_zipcode'],
-                               'city' => $session['l_city'],
-                               'latitude' => $session['l_latitude'],
-                               'longitude' => $session['l_longitude']
-                       ],
-                       'slot' => [
-                               'id' => $session['t_id'],
-                               'title' => $this->translator->trans($session['t_title'])
-                       ],
-                       'snippet' => [
-                               'id' => $session['p_id'],
-                               'description' => $session['p_description'],
-                               'class' => $session['p_class'],
-                               'contact' => $session['p_contact'],
-                               'donate' => $session['p_donate'],
-                               'link' => $session['p_link'],
-                               'profile' => $session['p_profile'],
-                               'rate' => $session['p_rate'],
-                               'hat' => $session['p_hat']
-                       ],
-                       'applications' => null
-               ];
+                       //With submitted form
+                       if ($sessionForm->isSubmitted() && $sessionForm->isValid()) {
+                               //Get data
+                               $data = $sessionForm->getData();
 
-               //With application
-               if (!empty($session['a_id'])) {
-                       $this->context['session']['application'] = [
-                               'user' => [
-                                       'id' => $session['au_id'],
-                                       'by' => $this->translator->trans('by %pseudonym%', [ '%pseudonym%' => $session['au_pseudonym'] ]),
-                                       'title' => $session['au_pseudonym']
-                               ],
-                               'id' => $session['a_id'],
-                               'canceled' => $session['a_canceled'],
-                               'title' => $this->translator->trans('Application %id%', [ '%id%' => $session['a_id'] ]),
-                       ];
-               }
+                               //Fetch session
+                               $sessionObject = $this->doctrine->getRepository(Session::class)->findOneById($id);
+
+                               //Set user
+                               $userObject = $this->security->getUser();
+
+                               //Replace with requested user for admin
+                               if ($this->checker->isGranted('ROLE_ADMIN') && !empty($data['user'])) {
+                                       $userObject = $this->doctrine->getRepository(User::class)->findOneById($data['user']);
+                               }
 
-               //With applications
-               if (!empty($session['sa_id'])) {
-                       //Extract applications id
-                       $session['sa_id'] = explode("\n", $session['sa_id']);
-                       //Extract applications score
-                       //XXX: score may be null before grant or for bad behaviour, replace NULL with 'NULL' to avoid silent drop in mysql
-                       $session['sa_score'] = array_map(function($v){return $v==='NULL'?null:$v;}, explode("\n", $session['sa_score']));
-                       //Extract applications created
-                       $session['sa_created'] = array_map(function($v){return new \DateTime($v);}, explode("\n", $session['sa_created']));
-                       //Extract applications updated
-                       //XXX: done earlied when computing last modified
-                       #$session['sa_updated'] = array_map(function($v){return new \DateTime($v);}, explode("\n", $session['sa_updated']));
-                       //Extract applications canceled
-                       //XXX: canceled is null before cancelation, replace NULL with 'NULL' to avoid silent drop in mysql
-                       $session['sa_canceled'] = array_map(function($v){return $v==='NULL'?null:new \DateTime($v);}, explode("\n", $session['sa_canceled']));
-
-                       //Extract applications user id
-                       $session['sau_id'] = explode("\n", $session['sau_id']);
-                       //Extract applications user pseudonym
-                       $session['sau_pseudonym'] = explode("\n", $session['sau_pseudonym']);
-
-                       //Init applications
-                       $this->context['session']['applications'] = [];
-                       foreach($session['sa_id'] as $i => $sa_id) {
-                               $this->context['session']['applications'][$sa_id] = [
-                                       'user' => null,
-                                       'score' => $session['sa_score'][$i],
-                                       'created' => $session['sa_created'][$i],
-                                       'updated' => $session['sa_updated'][$i],
-                                       'canceled' => $session['sa_canceled'][$i]
+                               //Set datetime
+                               $datetime = new \DateTime('now');
+
+                               //Set canceled time at start minus one day
+                               $canceled = (clone $sessionObject->getStart())->sub(new \DateInterval('P1D'));
+
+                               //Set action
+                               $action = [
+                                       'raincancel' => $sessionForm->has('raincancel') && $sessionForm->get('raincancel')->isClicked(),
+                                       'modify' => $sessionForm->has('modify') && $sessionForm->get('modify')->isClicked(),
+                                       'move' => $sessionForm->has('move') && $sessionForm->get('move')->isClicked(),
+                                       'cancel' => $sessionForm->has('cancel') && $sessionForm->get('cancel')->isClicked(),
+                                       'forcecancel' => $sessionForm->has('forcecancel') && $sessionForm->get('forcecancel')->isClicked(),
+                                       'attribute' => $sessionForm->has('attribute') && $sessionForm->get('attribute')->isClicked(),
+                                       'autoattribute' => $sessionForm->has('autoattribute') && $sessionForm->get('autoattribute')->isClicked(),
+                                       'lock' => $sessionForm->has('lock') && $sessionForm->get('lock')->isClicked(),
                                ];
-                               if (!empty($session['sau_id'][$i])) {
-                                       $this->context['session']['applications'][$sa_id]['user'] = [
-                                               'id' => $session['sau_id'][$i],
-                                               'title' => $session['sau_pseudonym'][$i]
-                                       ];
+
+                               //With raincancel and application and (rainfall or admin)
+                               if ($action['raincancel'] && ($application = $sessionObject->getApplication()) && ($sessionObject->getRainfall() >= 2 || $this->checker->isGranted('ROLE_ADMIN'))) {
+                                       //Cancel application at start minus one day
+                                       $application->setCanceled($canceled);
+
+                                       //Update time
+                                       $application->setUpdated($datetime);
+
+                                       //Insufficient rainfall
+                                       //XXX: is admin
+                                       if ($sessionObject->getRainfall() < 2) {
+                                               //Set score
+                                               //XXX: magic cheat score 42
+                                               $application->setScore(42);
+                                       }
+
+                                       //Queue application save
+                                       $this->manager->persist($application);
+
+                                       //Add notice in flash message
+                                       $this->addFlash('notice', $this->translator->trans('Application %id% updated', ['%id%' => $application->getId()]));
+
+                                       //Update time
+                                       $sessionObject->setUpdated($datetime);
+
+                                       //Queue session save
+                                       $this->manager->persist($sessionObject);
+
+                                       //Add notice in flash message
+                                       $this->addFlash('notice', $this->translator->trans('Session %id% updated', ['%id%' => $id]));
+                               //With modify
+                               } elseif ($action['modify']) {
+                                       //With admin
+                                       if ($this->checker->isGranted('ROLE_ADMIN')) {
+                                               //Get application
+                                               $application = $this->doctrine->getRepository(Application::class)->findOneBySessionUser($sessionObject, $userObject);
+
+                                               //Set dance
+                                               $application->setDance($data['dance']);
+
+                                               //Queue session save
+                                               $this->manager->persist($application);
+
+                                               //Set slot
+                                               $sessionObject->setSlot($data['slot']);
+
+                                               //Set date
+                                               $sessionObject->setDate($data['date']);
+                                       }
+
+                                       //Set begin
+                                       $sessionObject->setBegin($data['begin']);
+
+                                       //Set length
+                                       $sessionObject->setLength($data['length']);
+
+                                       //Update time
+                                       $sessionObject->setUpdated($datetime);
+
+                                       //Queue session save
+                                       $this->manager->persist($sessionObject);
+
+                                       //Add notice in flash message
+                                       $this->addFlash('notice', $this->translator->trans('Session %id% updated', ['%id%' => $id]));
+                               //With move
+                               } elseif ($action['move']) {
+                                       //Set location
+                                       $sessionObject->setLocation($this->doctrine->getRepository(Location::class)->findOneById($data['location']));
+
+                                       //Update time
+                                       $sessionObject->setUpdated($datetime);
+
+                                       //Queue session save
+                                       $this->manager->persist($sessionObject);
+
+                                       //Add notice in flash message
+                                       $this->addFlash('notice', $this->translator->trans('Session %id% updated', ['%id%' => $id]));
+                               //With cancel or forcecancel
+                               } elseif ($action['cancel'] || $action['forcecancel']) {
+                                       //Get application
+                                       $application = $this->doctrine->getRepository(Application::class)->findOneBySessionUser($sessionObject, $userObject);
+
+                                       //Not already canceled
+                                       if ($application->getCanceled() === null) {
+                                               //Cancel application
+                                               $application->setCanceled($datetime);
+
+                                               //Check if application is session application and (canceled 24h before start or forcecancel (as admin))
+                                               #if ($sessionObject->getApplication() == $application && ($datetime < $canceled || $action['forcecancel'])) {
+                                               if ($sessionObject->getApplication() == $application && $action['forcecancel']) {
+                                                       //Set score
+                                                       //XXX: magic cheat score 42
+                                                       $application->setScore(42);
+
+                                                       //Unattribute session
+                                                       $sessionObject->setApplication(null);
+
+                                                       //Update time
+                                                       $sessionObject->setUpdated($datetime);
+
+                                                       //Queue session save
+                                                       $this->manager->persist($sessionObject);
+
+                                                       //Add notice in flash message
+                                                       $this->addFlash('notice', $this->translator->trans('Session %id% updated', ['%id%' => $id]));
+                                               }
+                                       //Already canceled
+                                       } else {
+                                               //Uncancel application
+                                               $application->setCanceled(null);
+                                       }
+
+                                       //Update time
+                                       $application->setUpdated($datetime);
+
+                                       //Queue application save
+                                       $this->manager->persist($application);
+
+                                       //Add notice in flash message
+                                       $this->addFlash('notice', $this->translator->trans('Application %id% updated', ['%id%' => $application->getId()]));
+                               //With attribute
+                               } elseif ($action['attribute']) {
+                                       //Get application
+                                       $application = $this->doctrine->getRepository(Application::class)->findOneBySessionUser($sessionObject, $userObject);
+
+                                       //Already canceled
+                                       if ($application->getCanceled() !== null) {
+                                               //Uncancel application
+                                               $application->setCanceled(null);
+                                       }
+
+                                       //Set score
+                                       //XXX: magic cheat score 42
+                                       $application->setScore(42);
+
+                                       //Update time
+                                       $application->setUpdated($datetime);
+
+                                       //Queue application save
+                                       $this->manager->persist($application);
+
+                                       //Add notice in flash message
+                                       $this->addFlash('notice', $this->translator->trans('Application %id% updated', ['%id%' => $application->getId()]));
+
+                                       //Unattribute session
+                                       $sessionObject->setApplication($application);
+
+                                       //Update time
+                                       $sessionObject->setUpdated($datetime);
+
+                                       //Queue session save
+                                       $this->manager->persist($sessionObject);
+
+                                       //Add notice in flash message
+                                       $this->addFlash('notice', $this->translator->trans('Session %id% updated', ['%id%' => $id]));
+                               //With autoattribute
+                               } elseif ($action['autoattribute']) {
+                                       //Get best application
+                                       //XXX: best application may not issue result while grace time or bad behaviour
+                                       if (!empty($application = $this->doctrine->getRepository(Session::class)->findBestApplicationById($id))) {
+                                               //Attribute session
+                                               $sessionObject->setApplication($application);
+
+                                               //Update time
+                                               $sessionObject->setUpdated($datetime);
+
+                                               //Queue session save
+                                               $this->manager->persist($sessionObject);
+
+                                               //Add notice in flash message
+                                               $this->addFlash('notice', $this->translator->trans('Session %id% auto attributed', ['%id%' => $id]));
+                                       //No application
+                                       } else {
+                                               //Add warning in flash message
+                                               $this->addFlash('warning', $this->translator->trans('Session %id% not auto attributed', ['%id%' => $id]));
+                                       }
+                               //With lock
+                               } elseif ($action['lock']) {
+                                       //Already locked
+                                       if ($sessionObject->getLocked() !== null) {
+                                               //Set uncanceled
+                                               $canceled = null;
+
+                                               //Unlock session
+                                               $sessionObject->setLocked(null);
+                                       //Not locked
+                                       } else {
+                                               //Get application
+                                               if ($application = $sessionObject->getApplication()) {
+                                                       //Set score
+                                                       //XXX: magic cheat score 42
+                                                       $application->setScore(42);
+
+                                                       //Update time
+                                                       $application->setUpdated($datetime);
+
+                                                       //Queue application save
+                                                       $this->manager->persist($application);
+
+                                                       //Add notice in flash message
+                                                       $this->addFlash('notice', $this->translator->trans('Application %id% updated', ['%id%' => $application->getId()]));
+                                               }
+
+                                               //Unattribute session
+                                               $sessionObject->setApplication(null);
+
+                                               //Lock session
+                                               $sessionObject->setLocked($datetime);
+                                       }
+
+                                       //Update time
+                                       $sessionObject->setUpdated($datetime);
+
+                                       //Queue session save
+                                       $this->manager->persist($sessionObject);
+
+                                       //Add notice in flash message
+                                       $this->addFlash('notice', $this->translator->trans('Session %id% updated', ['%id%' => $id]));
+                               //Unknown action
+                               } else {
+                                       //Add warning in flash message
+                                       $this->addFlash('warning', $this->translator->trans('Session %id% not updated', ['%id%' => $id]));
                                }
-                       }
-               }
 
-               //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'
-                       )
-               );
+                               //Flush to get the ids
+                               $this->manager->flush();
 
-               //Fetch locations
-               //XXX: we want to display all active locations anyway
-               $locations = $doctrine->getRepository(Location::class)->findTranslatedSortedByPeriod($this->translator, $period, $session['au_id']);
+                               //Redirect to cleanup the form
+                               return $this->redirectToRoute('rapsysair_session_view', ['id' => $id, 'location' => $this->context['session']['location']['slug'], 'dance' => $this->context['session']['application']['dance']['slug']??null, 'user' => $this->context['session']['application']['user']['slug']??null]);
+                       }
+
+                       //Add form to context
+                       $this->context['forms']['session'] = $sessionForm->createView();
+               }
 
                //Render the view
-               return $this->render('@RapsysAir/session/view.html.twig', ['locations' => $locations]+$this->context, $response);
+               return $this->render('@RapsysAir/session/view.html.twig', $this->context, $response);
        }
 }
index b70ec21cf6725c4f9d3044365f1ee463c7cf1c95..acb0b9629ee244235091f8f724f94736cc2c4907 100644 (file)
@@ -19,7 +19,7 @@ class SnippetController extends DefaultController {
        /**
         * Add snippet
         *
-        * @desc Persist snippet in database
+        * Persist snippet in database
         *
         * @param Request $request The request instance
         *
@@ -28,8 +28,11 @@ class SnippetController extends DefaultController {
         * @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')]));
+               //Without guest role
+               if (!$this->checker->isGranted('ROLE_GUEST')) {
+                       //Throw 403
+                       throw $this->createAccessDeniedException($this->translator->trans('Unable to access this page without role %role%!', ['%role%' => $this->translator->trans('Guest')]));
+               }
 
                //Create SnippetType form
                $form = $this->container->get('form.factory')->createNamed(
@@ -42,7 +45,7 @@ class SnippetController extends DefaultController {
                        //Set options
                        [
                                //Set the action
-                               'action' => $this->generateUrl('rapsys_air_snippet_add', ['location' => $request->get('location')]),
+                               'action' => $this->generateUrl('rapsysair_snippet_add', ['location' => $request->get('location')]),
                                //Set the form attribute
                                'attr' => []
                        ]
@@ -53,8 +56,11 @@ class SnippetController extends DefaultController {
 
                //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')]));
+                       //Without admin role
+                       if (!$this->checker->isGranted('ROLE_ADMIN')) {
+                               //Throw 403
+                               throw $this->createAccessDeniedException($this->translator->trans('Unable to access this page without role %role%!', ['%role%' => $this->translator->trans('Admin')]));
+                       }
                }
 
                //Handle invalid form
@@ -132,7 +138,7 @@ class SnippetController extends DefaultController {
                                unset($route['_route'], $route['_controller']);
 
                                //Check if snippet view route
-                               if ($name == 'rapsys_air_user_view' && !empty($route['id'])) {
+                               if ($name == 'rapsysair_user_view' && !empty($route['id'])) {
                                        //Replace id
                                        $route['id'] = $snippet->getUser()->getId();
                                //Other routes
@@ -151,13 +157,13 @@ class SnippetController extends DefaultController {
                }
 
                //Redirect to cleanup the form
-               return $this->redirectToRoute('rapsys_air', ['snippet' => $snippet->getId()]);
+               return $this->redirectToRoute('rapsysair', ['snippet' => $snippet->getId()]);
        }
 
        /**
         * Edit snippet
         *
-        * @desc Persist snippet in database
+        * Persist snippet in database
         *
         * @param Request $request The request instance
         *
@@ -166,8 +172,11 @@ class SnippetController extends DefaultController {
         * @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')]));
+               //Without guest role
+               if (!$this->checker->isGranted('ROLE_GUEST')) {
+                       //Throw 403
+                       throw $this->createAccessDeniedException($this->translator->trans('Unable to access this page without role %role%!', ['%role%' => $this->translator->trans('Guest')]));
+               }
 
                //Get doctrine
                $doctrine = $this->getDoctrine();
@@ -188,7 +197,7 @@ class SnippetController extends DefaultController {
                        //Set options
                        [
                                //Set the action
-                               'action' => $this->generateUrl('rapsys_air_snippet_edit', ['id' => $id]),
+                               'action' => $this->generateUrl('rapsysair_snippet_edit', ['id' => $id]),
                                //Set the form attribute
                                'attr' => []
                        ]
@@ -199,8 +208,11 @@ class SnippetController extends DefaultController {
 
                //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')]));
+                       //Without admin role
+                       if (!$this->checker->isGranted('ROLE_ADMIN')) {
+                               //Throw 403
+                               throw $this->createAccessDeniedException($this->translator->trans('Unable to access this page without role %role%!', ['%role%' => $this->translator->trans('Admin')]));
+                       }
                }
 
                //Handle invalid form
@@ -310,7 +322,7 @@ class SnippetController extends DefaultController {
                                unset($route['_route'], $route['_controller']);
 
                                //Check if snippet view route
-                               if ($name == 'rapsys_air_user_view' && !empty($route['id'])) {
+                               if ($name == 'rapsysair_user_view' && !empty($route['id'])) {
                                        //Replace id
                                        $route['id'] = $snippet->getUser()->getId();
                                //Other routes
@@ -329,6 +341,6 @@ class SnippetController extends DefaultController {
                }
 
                //Redirect to cleanup the form
-               return $this->redirectToRoute('rapsys_air', ['snippet' => $snippet->getId()]);
+               return $this->redirectToRoute('rapsysair', ['snippet' => $snippet->getId()]);
        }
 }
index b4f57657a6c804c4f5cc07ada6676e31d30e6939..5010e6468b867de016c3df5b8c4b93138d7be317 100644 (file)
 
 namespace Rapsys\AirBundle\Controller;
 
-use Doctrine\Bundle\DoctrineBundle\Registry;
 use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\Persistence\ManagerRegistry;
+
+use Google\Client;
+
+use Psr\Container\ContainerInterface;
+use Psr\Log\LoggerInterface;
+
+use Symfony\Bundle\SecurityBundle\Security;
+use Symfony\Component\Form\FormFactoryInterface;
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestStack;
 use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
+use Symfony\Component\Mailer\MailerInterface;
+use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+use Symfony\Component\Routing\RouterInterface;
+use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
+use Symfony\Contracts\Cache\CacheInterface;
+use Symfony\Contracts\Cache\ItemInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
+
+use Rapsys\UserBundle\Controller\UserController as BaseUserController;
+
+use Rapsys\AirBundle\Entity\Dance;
+use Rapsys\AirBundle\Entity\GoogleCalendar;
+use Rapsys\AirBundle\Entity\GoogleToken;
+use Rapsys\AirBundle\Entity\User;
 
 use Rapsys\PackBundle\Util\SluggerUtil;
 
-use Rapsys\UserBundle\Controller\DefaultController;
+use Twig\Environment;
 
-class UserController extends DefaultController {
+/**
+ * {@inheritdoc}
+ */
+class UserController extends BaseUserController {
        /**
         * {@inheritdoc}
+        *
+        * @param CacheInterface $cache The cache instance
+        * @param AuthorizationCheckerInterface $checker The checker instance
+        * @param ContainerInterface $container The container instance
+        * @param ManagerRegistry $doctrine The doctrine instance
+        * @param FormFactoryInterface $factory The factory instance
+        * @param UserPasswordHasherInterface $hasher The password hasher instance
+        * @param LoggerInterface $logger The logger instance
+        * @param MailerInterface $mailer The mailer instance
+        * @param EntityManagerInterface $manager The manager instance
+        * @param RouterInterface $router The router instance
+        * @param Security $security The security instance
+        * @param SluggerUtil $slugger The slugger instance
+        * @param RequestStack $stack The stack instance
+        * @param TranslatorInterface $translator The translator instance
+        * @param Environment $twig The twig environment instance
+        * @param Client $google The google client instance
+        * @param integer $limit The page limit
         */
-       public function edit(Request $request, Registry $doctrine, UserPasswordEncoderInterface $encoder, EntityManagerInterface $manager, SluggerUtil $slugger, $mail, $hash): Response {
+       public function __construct(protected CacheInterface $cache, protected AuthorizationCheckerInterface $checker, protected ContainerInterface $container, protected ManagerRegistry $doctrine, protected FormFactoryInterface $factory, protected UserPasswordHasherInterface $hasher, protected LoggerInterface $logger, protected MailerInterface $mailer, protected EntityManagerInterface $manager, protected RouterInterface $router, protected Security $security, protected SluggerUtil $slugger, protected RequestStack $stack, protected TranslatorInterface $translator, protected Environment $twig, protected Client $google, protected int $limit = 5) {
+               //Call parent constructor
+               parent::__construct($this->cache, $this->checker, $this->container, $this->doctrine, $this->factory, $this->hasher, $this->logger, $this->mailer, $this->manager, $this->router, $this->security, $this->slugger, $this->stack, $this->translator, $this->twig, $this->limit);
+
+               //Replace google client redirect uri
+               $this->google->setRedirectUri($this->router->generate($this->google->getRedirectUri(), [], UrlGeneratorInterface::ABSOLUTE_URL));
+       }
+
+       /**
+        * {@inheritdoc}
+        */
+       public function edit(Request $request, string $hash, string $mail): Response {
                //With invalid hash
-               if ($hash != $slugger->hash($mail)) {
+               if ($hash != $this->slugger->hash($mail)) {
                        //Throw bad request
                        throw new BadRequestHttpException($this->translator->trans('Invalid %field% field: %value%', ['%field%' => 'hash', '%value%' => $hash]));
                }
 
                //Get mail
-               $mail = $slugger->unshort($smail = $mail);
+               $mail = $this->slugger->unshort($smail = $mail);
 
                //With existing subscriber
-               if (empty($user = $doctrine->getRepository($this->config['class']['user'])->findOneByMail($mail))) {
+               if (empty($user = $this->doctrine->getRepository($this->config['class']['user'])->findOneByMail($mail))) {
                        //Throw not found
                        //XXX: prevent slugger reverse engineering by not displaying decoded mail
                        throw $this->createNotFoundException($this->translator->trans('Unable to find account %mail%', ['%mail%' => $smail]));
                }
 
                //Prevent access when not admin, user is not guest and not currently logged user
-               if (!$this->isGranted('ROLE_ADMIN') && $user != $this->getUser() || !$this->isGranted('IS_AUTHENTICATED_FULLY')) {
+               if (!$this->checker->isGranted('ROLE_ADMIN') && $user != $this->security->getUser() || !$this->checker->isGranted('IS_AUTHENTICATED_FULLY')) {
                        //Throw access denied
                        //XXX: prevent slugger reverse engineering by not displaying decoded mail
                        throw $this->createAccessDeniedException($this->translator->trans('Unable to access user: %mail%', ['%mail%' => $smail]));
                }
 
                //Create the RegisterType form and give the proper parameters
-               $edit = $this->createForm($this->config['edit']['view']['edit'], $user, [
+               $edit = $this->factory->create($this->config['edit']['view']['edit'], $user, [
                        //Set action to register route name and context
-                       'action' => $this->generateUrl($this->config['route']['edit']['name'], ['mail' => $smail, 'hash' => $slugger->hash($smail)]+$this->config['route']['edit']['context']),
+                       'action' => $this->generateUrl($this->config['route']['edit']['name'], ['mail' => $smail, 'hash' => $hash]+$this->config['route']['edit']['context']),
                        //Set civility class
                        'civility_class' => $this->config['class']['civility'],
                        //Set civility default
-                       'civility_default' => $doctrine->getRepository($this->config['class']['civility'])->findOneByTitle($this->config['default']['civility']),
+                       'civility_default' => $this->doctrine->getRepository($this->config['class']['civility'])->findOneByTitle($this->config['default']['civility']),
+                       //Set country class
+                       'country_class' => $this->config['class']['country'],
+                       //Set country default
+                       'country_default' => $this->doctrine->getRepository($this->config['class']['country'])->findOneByTitle($this->config['default']['country']),
+                       //Set country favorites
+                       'country_favorites' => $this->doctrine->getRepository($this->config['class']['country'])->findByTitle($this->config['default']['country_favorites']),
+                       //Set dance
+                       'dance' => $this->checker->isGranted('ROLE_ADMIN'),
+                       //Set dance choices
+                       'dance_choices' => $danceChoices = $this->doctrine->getRepository($this->config['class']['dance'])->findChoicesAsArray(),
+                       //Set dance default
+                       #'dance_default' => /*$this->doctrine->getRepository($this->config['class']['dance'])->findOneByNameType($this->config['default']['dance'])*/null,
+                       //Set dance favorites
+                       'dance_favorites' => $this->doctrine->getRepository($this->config['class']['dance'])->findIdByNameTypeAsArray($this->config['default']['dance_favorites']),
+                       //Set subscription
+                       'subscription' => $this->checker->isGranted('ROLE_ADMIN'),
+                       //Set subscription choices
+                       'subscription_choices' => $subscriptionChoices = $this->doctrine->getRepository($this->config['class']['user'])->findChoicesAsArray(),
+                       //Set subscription default
+                       #'subscription_default' => /*$this->doctrine->getRepository($this->config['class']['user'])->findOneByPseudonym($this->config['default']['subscription'])*/null,
+                       //Set subscription favorites
+                       'subscription_favorites' => $this->doctrine->getRepository($this->config['class']['user'])->findIdByPseudonymAsArray($this->config['default']['subscription_favorites']),
                        //Disable mail
-                       'mail' => $this->isGranted('ROLE_ADMIN'),
+                       'mail' => $this->checker->isGranted('ROLE_ADMIN'),
                        //Disable pseudonym
-                       'pseudonym' => $this->isGranted('ROLE_GUEST'),
-                       //Disable slug
-                       'slug' => $this->isGranted('ROLE_ADMIN'),
+                       'pseudonym' => $this->checker->isGranted('ROLE_GUEST'),
                        //Disable password
                        'password' => false,
                        //Set method
@@ -70,11 +145,11 @@ class UserController extends DefaultController {
                ]+$this->config['edit']['field']);
 
                //With admin role
-               if ($this->isGranted('ROLE_ADMIN')) {
-                       //Create the LoginType form and give the proper parameters
-                       $reset = $this->createForm($this->config['edit']['view']['reset'], $user, [
+               if ($this->checker->isGranted('ROLE_ADMIN')) {
+                       //Create the ResetType form and give the proper parameters
+                       $reset = $this->factory->create($this->config['edit']['view']['reset'], $user, [
                                //Set action to register route name and context
-                               'action' => $this->generateUrl($this->config['route']['edit']['name'], ['mail' => $smail, 'hash' => $slugger->hash($smail)]+$this->config['route']['edit']['context']),
+                               'action' => $this->generateUrl($this->config['route']['edit']['name'], ['mail' => $smail, 'hash' => $hash]+$this->config['route']['edit']['context']),
                                //Disable mail
                                'mail' => false,
                                //Set method
@@ -92,24 +167,418 @@ class UserController extends DefaultController {
                                        $data = $reset->getData();
 
                                        //Set password
-                                       $data->setPassword($encoder->encodePassword($data, $data->getPassword()));
+                                       $data->setPassword($this->hasher->hashPassword($data, $data->getPassword()));
 
-                                       //Queue snippet save
-                                       $manager->persist($data);
+                                       //Queue user password save
+                                       $this->manager->persist($data);
 
                                        //Flush to get the ids
-                                       $manager->flush();
+                                       $this->manager->flush();
 
                                        //Add notice
-                                       $this->addFlash('notice', $this->translator->trans('Account %mail% password updated', ['%mail%' => $mail = $data->getMail()]));
+                                       $this->addFlash('notice', $this->translator->trans('Account %mail% password updated', ['%mail%' => $mail]));
 
                                        //Redirect to cleanup the form
-                                       return $this->redirectToRoute($this->config['route']['edit']['name'], ['mail' => $smail = $slugger->short($mail), 'hash' => $slugger->hash($smail)]+$this->config['route']['edit']['context']);
+                                       return $this->redirectToRoute($this->config['route']['edit']['name'], ['mail' => $smail, 'hash' => $hash]+$this->config['route']['edit']['context']);
                                }
                        }
 
                        //Add reset view
                        $this->config['edit']['view']['context']['reset'] = $reset->createView();
+
+                       //Add google calendar array
+                       $this->config['edit']['view']['context']['calendar'] = [
+                               //Form by mail
+                               'form' => [],
+                               //Uri to link account
+                               'link' => null,
+                               //Logo
+                               'logo' => [
+                                       'png' => '@RapsysAir/png/calendar.png',
+                                       'svg' => '@RapsysAir/svg/calendar.svg'
+                               ]
+                       ];
+
+                       //Set login hint
+                       $this->google->setLoginHint($user->getMail());
+
+                       //With user tokens
+                       if (!($googleTokens = $user->getGoogleTokens())->isEmpty()) {
+                               //Iterate on each google token
+                               //XXX: either we finish with a valid token set or a logic exception after token removal
+                               foreach($googleTokens as $googleToken) {
+                                       //Clear client cache before changing access token
+                                       //TODO: set a per token cache ?
+                                       $this->google->getCache()->clear();
+
+                                       //Set access token
+                                       $this->google->setAccessToken(
+                                               [
+                                                       'access_token' => $googleToken->getAccess(),
+                                                       'refresh_token' => $googleToken->getRefresh(),
+                                                       'created' => $googleToken->getCreated()->getTimestamp(),
+                                                       'expires_in' => $googleToken->getExpired()->getTimestamp() - (new \DateTime('now'))->getTimestamp(),
+                                               ]
+                                       );
+
+                                       //With expired token
+                                       if ($this->google->isAccessTokenExpired()) {
+                                               //Refresh token
+                                               if (($refresh = $this->google->getRefreshToken()) && ($token = $this->google->fetchAccessTokenWithRefreshToken($refresh)) && empty($token['error'])) {
+                                                       //Set access token
+                                                       $googleToken->setAccess($token['access_token']);
+
+                                                       //Set expires
+                                                       $googleToken->setExpired(new \DateTime('+'.$token['expires_in'].' second'));
+
+                                                       //Set refresh
+                                                       $googleToken->setRefresh($token['refresh_token']);
+
+                                                       //Queue google token save
+                                                       $this->manager->persist($googleToken);
+
+                                                       //Flush to get the ids
+                                                       $this->manager->flush();
+                                               //Refresh failed
+                                               } else {
+                                                       //Add error in flash message
+                                                       $this->addFlash(
+                                                               'error',
+                                                               $this->translator->trans(
+                                                                       empty($token['error'])?'Unable to refresh token':'Unable to refresh token: %error%',
+                                                                       empty($token['error'])?[]:['%error%' => str_replace('_', ' ', $token['error'])]
+                                                               )
+                                                       );
+
+                                                       //Set calendar mails
+                                                       $cmails = [];
+
+                                                       //Iterate on each google token calendars
+                                                       foreach($googleToken->getGoogleCalendars() as $googleCalendar) {
+                                                               //Add calendar mail
+                                                               $cmails[] = $googleCalendar->getMail();
+
+                                                               //Remove google token calendar
+                                                               $this->manager->remove($googleCalendar);
+                                                       }
+
+                                                       //Log unlinked google token infos
+                                                       $this->logger->emergency(
+                                                               $this->translator->trans(
+                                                                       'expired: mail=%mail% gmail=%gmail% cmails=%cmails% locale=%locale%',
+                                                                       [
+                                                                               '%mail%' => $googleToken->getUser()->getMail(),
+                                                                               '%gmail%' => $googleToken->getMail(),
+                                                                               '%cmails' => implode(',', $cmails),
+                                                                               '%locale%' => $request->getLocale()
+                                                                       ]
+                                                               )
+                                                       );
+
+                                                       //Remove google token
+                                                       $this->manager->remove($googleToken);
+
+                                                       //Flush to delete it
+                                                       $this->manager->flush();
+
+                                                       //TODO: warn user by mail ?
+
+                                                       //Skip to next token
+                                                       continue;
+                                               }
+                                       }
+
+                                       //XXX: TODO: remove DEBUG
+                                       #$this->cache->delete('user.edit.calendar.'.$this->slugger->short($googleToken->getMail()));
+
+                                       //Retrieve calendar
+                                       try {
+                                               //Get calendars
+                                               $calendars = $this->cache->get(
+                                                       //Set key to user.edit.$mail
+                                                       ($calendarKey = 'user.edit.calendar.'.($googleShortMail = $this->slugger->short($googleMail = $googleToken->getMail()))),
+                                                       //Fetch mail calendar list
+                                                       function (ItemInterface $item): array {
+                                                               //Expire after 1h
+                                                               $item->expiresAfter(3600);
+
+                                                               //Get google calendar service
+                                                               $service = new \Google\Service\Calendar($this->google);
+
+                                                               //Init calendars
+                                                               $calendars = [];
+
+                                                               //Init counter
+                                                               $count = 0;
+
+                                                               //Set page token
+                                                               $pageToken = null;
+
+                                                               //Iterate until next page token is null
+                                                               do {
+                                                                       //Get token calendar list
+                                                                       //XXX: require permission to read and write events
+                                                                       $calendarList = $service->calendarList->listCalendarList(['pageToken' => $pageToken, 'minAccessRole' => 'writer', 'showHidden' => true]);
+
+                                                                       //Iterate on items
+                                                                       foreach($calendarList->getItems() as $calendarItem) {
+                                                                               //With primary calendar
+                                                                               if ($calendarItem->getPrimary()) {
+                                                                                       //Add primary calendar
+                                                                                       //XXX: use primary as key as described in google api documentation
+                                                                                       $calendars = ['primary' => $this->translator->trans('Primary') /*$calendarItem->getSummary()*/] + $calendars;
+                                                                               //With secondary calendar
+                                                                               } else {
+                                                                                       //Add secondary calendar
+                                                                                       //XXX: Append counter to make sure summary is unique for later array_flip call
+                                                                                       $calendars += [$calendarItem->getId() => $calendarItem->getSummary().' ('.++$count.')'];
+                                                                               }
+                                                                       }
+                                                               } while ($pageToken = $calendarList->getNextPageToken());
+
+                                                               //Cache calendars
+                                                               return $calendars;
+                                                       }
+                                               );
+                                       //Catch exception
+                                       } catch(\Google\Service\Exception $e) {
+                                               //With 401 or code
+                                               //XXX: see https://cloud.google.com/apis/design/errors
+                                               if ($e->getCode() == 401 || $e->getCode() == 403) {
+                                                       //Add error in flash message
+                                                       $this->addFlash(
+                                                               'error',
+                                                               $this->translator->trans(
+                                                                       'Unable to list calendars: %error%',
+                                                                       ['%error%' => $e->getMessage()]
+                                                               )
+                                                       );
+
+                                                       //Set calendar mails
+                                                       $cmails = [];
+
+                                                       //Iterate on each google token calendars
+                                                       foreach($googleToken->getGoogleCalendars() as $googleCalendar) {
+                                                               //Add calendar mail
+                                                               $cmails[] = $googleCalendar->getMail();
+
+                                                               //Remove google token calendar
+                                                               $this->manager->remove($googleCalendar);
+                                                       }
+
+                                                       //Log unlinked google token infos
+                                                       $this->logger->emergency(
+                                                               $this->translator->trans(
+                                                                       'denied: mail=%mail% gmail=%gmail% cmails=%cmails% locale=%locale%',
+                                                                       [
+                                                                               '%mail%' => $googleToken->getUser()->getMail(),
+                                                                               '%gmail%' => $googleToken->getMail(),
+                                                                               '%cmails' => implode(',', $cmails),
+                                                                               '%locale%' => $request->getLocale()
+                                                                       ]
+                                                               )
+                                                       );
+
+                                                       //Remove google token
+                                                       $this->manager->remove($googleToken);
+
+                                                       //Flush to delete it
+                                                       $this->manager->flush();
+
+                                                       //TODO: warn user by mail ?
+
+                                                       //Skip to next token
+                                                       continue;
+                                               }
+
+                                               //Throw error
+                                               throw new \LogicException('Calendar list failed', 0, $e);
+                                       }
+
+                                       //Set formData array
+                                       $formData = ['calendar' => []];
+
+                                       //With google calendars
+                                       if (!($googleCalendars = $googleToken->getGoogleCalendars())->isEmpty()) {
+                                               //Iterate on each google calendars
+                                               foreach($googleCalendars as $googleCalendar) {
+                                                       //With existing google calendar
+                                                       if (isset($calendars[$googleCalendar->getMail()])) {
+                                                               //Add google calendar to form data
+                                                               $formData['calendar'][] = $googleCalendar->getMail();
+                                                       } else {
+                                                               //Remove google calendar from database
+                                                               $this->manager->remove($googleCalendar);
+
+                                                               //Flush to persist ids
+                                                               $this->manager->flush();
+                                                       }
+                                               }
+                                       }
+
+                                       //TODO: add feature for alerts (-30min/-1h) ?
+                                       //TODO: [Direct link to calendar ?][Direct link to calendar settings ?][Alerts][Remove]
+
+                                       //Create the CalendarType form and give the proper parameters
+                                       $form = $this->factory->createNamed('calendar_'.$googleShortMail, 'Rapsys\AirBundle\Form\CalendarType', $formData, [
+                                               //Set action to register route name and context
+                                               'action' => $this->generateUrl($this->config['route']['edit']['name'], ['mail' => $smail, 'hash' => $hash]+$this->config['route']['edit']['context']),
+                                               //Set calendar choices
+                                               //XXX: unique calendar summary required by choice widget is guaranteed by appending ' (x)' to secondary calendars earlier
+                                               'calendar_choices' => array_flip($calendars),
+                                               //Set method
+                                               'method' => 'POST'
+                                       ]);
+
+                                       //With post method
+                                       if ($request->isMethod('POST')) {
+                                               //Refill the fields in case the form is not valid.
+                                               $form->handleRequest($request);
+
+                                               //With reset submitted and valid
+                                               if ($form->isSubmitted() && $form->isValid()) {
+                                                       //Set data
+                                                       $data = $form->getData();
+
+                                                       //Refresh button
+                                                       if (($clicked = $form->getClickedButton()->getName()) == 'refresh') {
+                                                               //Remove calendar key
+                                                               $this->cache->delete($calendarKey);
+
+                                                               //Add notice
+                                                               $this->addFlash('notice', $this->translator->trans('Account %mail% calendars updated', ['%mail%' => $googleMail]));
+                                                       //Add button
+                                                       } elseif ($clicked == 'add') {
+                                                               //Get google calendar service
+                                                               $service = new \Google\Service\Calendar($this->google);
+
+                                                               //Add calendar
+                                                               try {
+                                                                       //Instantiate calendar
+                                                                       $calendar = new \Google\Service\Calendar\Calendar(
+                                                                               [
+                                                                                       'summary' => $this->translator->trans($this->config['context']['site']['title']),
+                                                                                       'timeZone' => date_default_timezone_get()
+                                                                               ]
+                                                                       );
+
+                                                                       //Insert calendar
+                                                                       $service->calendars->insert($calendar);
+                                                               //Catch exception
+                                                               } catch(\Google\Service\Exception $e) {
+                                                                       //Throw error
+                                                                       throw new \LogicException('Calendar insert failed', 0, $e);
+                                                               }
+
+                                                               //Remove calendar key
+                                                               $this->cache->delete($calendarKey);
+
+                                                               //Add notice
+                                                               $this->addFlash('notice', $this->translator->trans('Account %mail% calendar added', ['%mail%' => $googleMail]));
+                                                       //Delete button
+                                                       } elseif ($clicked == 'delete') {
+                                                               //Get google calendar service
+                                                               $service = new \Google\Service\Calendar($this->google);
+
+                                                               //Remove calendar
+                                                               try {
+                                                                       //Set site title
+                                                                       $siteTitle = $this->translator->trans($this->config['context']['site']['title']);
+
+                                                                       //Iterate on calendars
+                                                                       foreach($calendars as $calendarId => $calendarSummary) {
+                                                                               //With calendar matching site title
+                                                                               if (substr($calendarSummary, 0, strlen($siteTitle)) == $siteTitle) {
+                                                                                       //Delete the calendar
+                                                                                       $service->calendars->delete($calendarId);
+                                                                               }
+                                                                       }
+                                                               //Catch exception
+                                                               } catch(\Google\Service\Exception $e) {
+                                                                       //Throw error
+                                                                       throw new \LogicException('Calendar delete failed', 0, $e);
+                                                               }
+
+                                                               //Remove calendar key
+                                                               $this->cache->delete($calendarKey);
+
+                                                               //Add notice
+                                                               $this->addFlash('notice', $this->translator->trans('Account %mail% calendars deleted', ['%mail%' => $googleMail]));
+                                                       //Unlink button
+                                                       } elseif ($clicked == 'unlink') {
+                                                               //Iterate on each google calendars
+                                                               foreach($googleCalendars as $googleCalendar) {
+                                                                       //Remove google calendar from database
+                                                                       $this->manager->remove($googleCalendar);
+                                                               }
+
+                                                               //Remove google token from database
+                                                               $this->manager->remove($googleToken);
+
+                                                               //Flush to persist
+                                                               $this->manager->flush();
+
+                                                               //Revoke access token
+                                                               $this->google->revokeToken($googleToken->getAccess());
+
+                                                               //With refresh token
+                                                               if ($refresh = $googleToken->getRefresh()) {
+                                                                       //Revoke refresh token
+                                                                       $this->google->revokeToken($googleToken->getRefresh());
+                                                               }
+
+                                                               //Remove calendar key
+                                                               $this->cache->delete($calendarKey);
+
+                                                               //Add notice
+                                                               $this->addFlash('notice', $this->translator->trans('Account %mail% calendars unlinked', ['%mail%' => $googleMail]));
+                                                       //Submit button
+                                                       } else {
+                                                               //Flipped calendar data
+                                                               $dataCalendarFlip = array_flip($data['calendar']);
+
+                                                               //Iterate on each google calendars
+                                                               foreach($googleCalendars as $googleCalendar) {
+                                                                       //Without calendar in flipped data
+                                                                       if (!isset($dataCalendarFlip[$googleCalendarMail = $googleCalendar->getMail()])) {
+                                                                               //Remove google calendar from database
+                                                                               $this->manager->remove($googleCalendar);
+                                                                       //With calendar in flipped data
+                                                                       } else {
+                                                                               //Remove google calendar from calendar data
+                                                                               unset($data['calendar'][$dataCalendarFlip[$googleCalendarMail]]);
+                                                                       }
+                                                               }
+
+                                                               //Iterate on remaining calendar data
+                                                               foreach($data['calendar'] as $googleCalendarMail) {
+                                                                       //Create new google calendar
+                                                                       //XXX: remove trailing ' (x)' from summary
+                                                                       $googleCalendar = new GoogleCalendar($googleToken, $googleCalendarMail, preg_replace('/ \([0-9]\)$/', '', $calendars[$googleCalendarMail]));
+
+                                                                       //Queue google calendar save
+                                                                       $this->manager->persist($googleCalendar);
+                                                               }
+
+                                                               //Flush to persist ids
+                                                               $this->manager->flush();
+
+                                                               //Add notice
+                                                               $this->addFlash('notice', $this->translator->trans('Account %mail% calendars updated', ['%mail%' => $googleMail]));
+                                                       }
+
+                                                       //Redirect to cleanup the form
+                                                       return $this->redirectToRoute($this->config['route']['edit']['name'], ['mail' => $smail, 'hash' => $hash]+$this->config['route']['edit']['context']);
+                                               }
+                                       }
+
+                                       //Add form view
+                                       $this->config['edit']['view']['context']['calendar']['form'][$googleToken->getMail()] = $form->createView();
+                               }
+                       }
+
+                       //Add google calendar auth url
+                       $this->config['edit']['view']['context']['calendar']['link'] = $this->google->createAuthUrl();
                }
 
                //With post method
@@ -122,28 +591,20 @@ class UserController extends DefaultController {
                                //Set data
                                $data = $edit->getData();
 
-                               //With admin
-                               if ($this->isGranted('ROLE_ADMIN')) {
-                                       //With pseudonym and without slug
-                                       if (!empty($pseudonym = $data->getPseudonym()) && empty($data->getSlug())) {
-                                               //Set slug
-                                               $data->setSlug($slugger->slug($pseudonym));
-                                       }
-                               }
-
-                               //Queue snippet save
-                               $manager->persist($data);
+                               //Queue user save
+                               $this->manager->persist($data);
 
                                //Try saving in database
                                try {
                                        //Flush to get the ids
-                                       $manager->flush();
+                                       $this->manager->flush();
 
                                        //Add notice
+                                       //XXX: get mail from data as it may change
                                        $this->addFlash('notice', $this->translator->trans('Account %mail% updated', ['%mail%' => $mail = $data->getMail()]));
 
                                        //Redirect to cleanup the form
-                                       return $this->redirectToRoute($this->config['route']['edit']['name'], ['mail' => $smail = $slugger->short($mail), 'hash' => $slugger->hash($smail)]+$this->config['route']['edit']['context']);
+                                       return $this->redirectToRoute($this->config['route']['edit']['name'], ['mail' => $smail = $this->slugger->short($mail), 'hash' => $this->slugger->hash($smail)]+$this->config['route']['edit']['context']);
                                //Catch double slug or mail
                                } catch (UniqueConstraintViolationException $e) {
                                        //Add error message mail already exists
@@ -152,7 +613,7 @@ class UserController extends DefaultController {
                        }
                //Without admin role
                //XXX: prefer a reset on login to force user unspam action
-               } elseif (!$this->isGranted('ROLE_ADMIN')) {
+               } elseif (!$this->checker->isGranted('ROLE_ADMIN')) {
                        //Add notice
                        $this->addFlash('notice', $this->translator->trans('To change your password login with your mail and any password then follow the procedure'));
                }
@@ -165,4 +626,114 @@ class UserController extends DefaultController {
                        ['edit' => $edit->createView(), 'sent' => $request->query->get('sent', 0)]+$this->config['edit']['view']['context']
                );
        }
+
+       /**
+        * Handle google callback
+        *
+        * @param Request $request The request
+        * @return Response The response
+        */
+       public function googleCallback(Request $request): Response {
+               //Without code
+               if (empty($code = $request->query->get('code', ''))) {
+                       throw new \InvalidArgumentException('Query parameter code is empty');
+               }
+
+               //Without user
+               if (empty($user = $this->security->getUser())) {
+                       throw new \LogicException('User is empty');
+               }
+
+               //Set google client login hint
+               $this->google->setLoginHint($user->getMail());
+
+               //Set google client scopes
+               $googleScopes = [\Google\Service\Calendar::CALENDAR_EVENTS, \Google\Service\Calendar::CALENDAR, \Google\Service\Oauth2::USERINFO_EMAIL];
+
+               //Protect to extract failure
+               try {
+                       //Authenticate with code
+                       if (!empty($token = $this->google->authenticate($code))) {
+                               //With error
+                               if (!empty($token['error'])) {
+                                       throw new \LogicException('Client authenticate failed: '.str_replace('_', ' ', $token['error']));
+                               //Without refresh token
+                               } elseif (empty($token['refresh_token'])) {
+                                       throw new \LogicException('Refresh token is empty');
+                               //Without expires in
+                               } elseif (empty($token['expires_in'])) {
+                                       throw new \LogicException('Expires in is empty');
+                               //Without scope
+                               } elseif (empty($token['scope'])) {
+                                       throw new \LogicException('Scope in is empty');
+                               //Without valid scope
+                               } elseif (array_intersect($googleScopes, explode(' ', $token['scope'])) != $googleScopes) {
+                                       throw new \LogicException('Scope in is not valid');
+                               }
+
+                               //Get Oauth2 object
+                               $oauth2 = new \Google\Service\Oauth2($this->google);
+
+                               //Protect user info get call
+                               try {
+                                       //Retrieve user info
+                                       $userInfo = $oauth2->userinfo->get();
+                               //Catch exception
+                               } catch(\Google\Service\Exception $e) {
+                                       //Throw error
+                                       throw new \LogicException('Userinfo get failed', 0, $e);
+                               }
+
+                               //With existing token
+                               if (
+                                       //If available retrieve google token with matching mail
+                                       $googleToken = array_reduce(
+                                               $user->getGoogleTokens()->getValues(),
+                                               function ($c, $i) use ($userInfo) {
+                                                       if ($i->getMail() == $userInfo['email']) {
+                                                               return $i;
+                                                       }
+                                               }
+                                       )
+                               ) {
+                                       //Set mail
+                                       //XXX: TODO: should already be set and not change, remove ?
+                                       //XXX: TODO: store picture as well ?
+                                       $googleToken->setMail($userInfo['email']);
+
+                                       //Set access token
+                                       $googleToken->setAccess($token['access_token']);
+
+                                       //Set expires
+                                       $googleToken->setExpired(new \DateTime('+'.$token['expires_in'].' second'));
+
+                                       //Set refresh
+                                       $googleToken->setRefresh($token['refresh_token']);
+                               } else {
+                                       //Create new token
+                                       //XXX: TODO: store picture as well ?
+                                       $googleToken = new GoogleToken($user, $userInfo['email'], $token['access_token'], new \DateTime('+'.$token['expires_in'].' second'), $token['refresh_token']);
+                               }
+
+                               //Queue google token save
+                               $this->manager->persist($googleToken);
+
+                               //Flush to get the ids
+                               $this->manager->flush();
+
+                               //Add notice
+                               $this->addFlash('notice', $this->translator->trans('Account %mail% google token updated', ['%mail%' => $user->getMail()]));
+                       //With failed authenticate
+                       } else {
+                               throw new \LogicException('Client authenticate failed');
+                       }
+               //Catch exception
+               } catch(\Exception $e) {
+                       //Add notice
+                       $this->addFlash('error', $this->translator->trans('Account %mail% google token rejected: %error%', ['%mail%' => $user->getMail(), '%error%' => $e->getMessage()]));
+               }
+
+               //Redirect to user
+               return $this->redirectToRoute('rapsysuser_edit', ['mail' => $short = $this->slugger->short($user->getMail()), 'hash' => $this->slugger->hash($short)]);
+       }
 }
index 8ccf57266b668fd4e3c37cbeaf8d4aefaf485048..89593f07555e5a8174cb766da9640be5ab8b1c52 100644 (file)
@@ -1,64 +1,92 @@
-<?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\DataFixtures;
 
+use Doctrine\Bundle\FixturesBundle\Fixture;
+use Doctrine\Persistence\ObjectManager;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
+
 use Rapsys\AirBundle\Entity\Civility;
 use Rapsys\AirBundle\Entity\Group;
 use Rapsys\AirBundle\Entity\User;
 use Rapsys\AirBundle\Entity\Location;
 use Rapsys\AirBundle\Entity\Slot;
 
-class AirFixtures extends \Doctrine\Bundle\FixturesBundle\Fixture implements \Symfony\Component\DependencyInjection\ContainerAwareInterface {
+/**
+ * {@inheritdoc}
+ */
+class AirFixtures extends Fixture {
        /**
-        * @var ContainerInterface
+        * Air fixtures constructor
         */
-       private $container;
-
-       public function setContainer(\Symfony\Component\DependencyInjection\ContainerInterface $container = null) {
-               $this->container = $container;
+       public function __construct(protected ContainerInterface $container, protected UserPasswordHasherInterface $hasher) {
        }
 
        /**
         * {@inheritDoc}
         */
-       public function load(\Doctrine\Common\Persistence\ObjectManager $manager) {
-               $encoder = $this->container->get('security.password_encoder');
-
+       public function load(ObjectManager $manager) {
                //Civility tree
-               $civilityTree = array(
-                       'Mr.' => 'Mister',
-                       'Mrs.' => 'Madam',
-                       'Ms.' => 'Miss'
-               );
+               $civilityTree = [
+                       'Mister',
+                       'Madam',
+                       'Miss'
+               ];
 
                //Create titles
-               $civilitys = array();
-               foreach($civilityTree as $shortData => $civilityData) {
-                       $civility = new Title($civilityData);
-                       $civility->setShort($shortData);
-                       $civility->setCreated(new \DateTime('now'));
-                       $civility->setUpdated(new \DateTime('now'));
+               $civilitys = [];
+               foreach($civilityTree as $civilityData) {
+                       $civility = new Civility($civilityData);
                        $manager->persist($civility);
-                       $civilitys[$shortData] = $civility;
+                       $civilitys[$civilityData] = $civility;
                        unset($civility);
                }
 
+               //TODO: insert countries from https://raw.githubusercontent.com/raramuridesign/mysql-country-list/master/country-lists/mysql-country-list-detailed-info.sql
+               #CREATE TABLE `countries` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `code` varchar(2) NOT NULL, `alpha` varchar(3) NOT NULL, `title` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, `created` datetime NOT NULL, `updated` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `code` (`code`), UNIQUE KEY `alpha` (`alpha`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+               #insert into countries (code, alpha, title, created, updated) select countryCode, isoAlpha3, countryName, NOW(), NOW() FROM apps_countries_detailed ORDER BY countryCode ASC, isoAlpha3 ASC;
+
+               //Dance tree
+               $danceTree = [
+                       'Argentine Tango' => [
+                          'Milonga', 'Class and milonga', 'Public class', 'Private class'
+                       ]
+               ];
+
+               //Create titles
+               $dances = [];
+               foreach($danceTree as $danceTitle => $danceData) {
+                       foreach($danceData as $danceType) {
+                               $dance = new Dance($danceTitle, $danceType);
+                               $manager->persist($dance);
+                               unset($dance);
+                       }
+               }
+
                //Group tree
                //XXX: ROLE_XXX is required by
-               $groupTree = array(
+               $groupTree = [
                        'User',
                        'Guest',
                        'Regular',
                        'Senior',
                        'Admin'
-               );
+               ];
 
                //Create groups
-               $groups = array();
+               $groups = [];
                foreach($groupTree as $groupData) {
                        $group = new Group($groupData);
-                       $group->setCreated(new \DateTime('now'));
-                       $group->setUpdated(new \DateTime('now'));
                        $manager->persist($group);
                        $groups[$groupData] = $group;
                        unset($group);
@@ -68,8 +96,8 @@ class AirFixtures extends \Doctrine\Bundle\FixturesBundle\Fixture implements \Sy
                $manager->flush();
 
                //User tree
-               $userTree = array(
-                       array(
+               $userTree = [
+                       [
                                'short' => 'Mr.',
                                'group' => 'Admin',
                                'mail' => 'tango@rapsys.eu',
@@ -78,8 +106,8 @@ class AirFixtures extends \Doctrine\Bundle\FixturesBundle\Fixture implements \Sy
                                'surname' => 'Gertz',
                                'phone' => '+33677952829',
                                'password' => 'test'
-                       ),
-                       /*array(
+                       ],
+                       /*[
                                'short' => 'Mr.',
                                'group' => 'Senior',
                                'mail' => 'denis.courvoisier@wanadoo.fr',
@@ -88,8 +116,8 @@ class AirFixtures extends \Doctrine\Bundle\FixturesBundle\Fixture implements \Sy
                                'surname' => 'Courvoisier',
                                'phone' => '+33600000000',
                                'password' => 'test'
-                       ),*/
-                       array(
+                       ],*/
+                       [
                                'short' => 'Mr.',
                                'group' => 'Senior',
                                'mail' => 'rannou402@orange.fr',
@@ -98,8 +126,8 @@ class AirFixtures extends \Doctrine\Bundle\FixturesBundle\Fixture implements \Sy
                                'surname' => 'Rannou',
                                'phone' => '+33600000000',
                                'password' => 'test'
-                       ),
-                       /*array(
+                       ],
+                       /*[
                                'short' => 'Ms.',
                                'group' => 'Regular',
                                'mail' => 'roxmaps@gmail.com',
@@ -108,22 +136,18 @@ class AirFixtures extends \Doctrine\Bundle\FixturesBundle\Fixture implements \Sy
                                'surname' => 'Prado',
                                'phone' => '+33600000000',
                                'password' => 'test'
-                       ),*/
-               );
+                       ],*/
+               ];
 
                //Create users
-               $users = array();
+               $users = [];
                foreach($userTree as $userData) {
-                       $user = new User($userData['mail']);
+                       $user = new User($userData['mail'], $userData['password'], $civilitys[$userData['short']], $userData['forename'], $userData['surname']);
+                       #TODO: check that password is hashed correctly !!!
+                       #$user->setPassword($this->hasher->hashPassword($user, $userData['password']));
                        $user->setPseudonym($userData['pseudonym']);
-                       $user->setForename($userData['forename']);
-                       $user->setSurname($userData['surname']);
                        $user->setPhone($userData['phone']);
-                       $user->setPassword($encoder->encodePassword($user, $userData['password']));
-                       $user->setCivility($civilitys[$userData['short']]);
                        $user->addGroup($groups[$userData['group']]);
-                       $user->setCreated(new \DateTime('now'));
-                       $user->setUpdated(new \DateTime('now'));
                        $manager->persist($user);
                        $users[] = $user;
                        unset($user);
@@ -134,6 +158,7 @@ class AirFixtures extends \Doctrine\Bundle\FixturesBundle\Fixture implements \Sy
 
                //Location tree
                //XXX: adding a new zipcode here requires matching accuweather uris in Command/WeatherCommand.php
+               //TODO: add descriptions as well
                $locationTree = [
                        [
                                'title' => 'Garnier opera',
@@ -143,7 +168,8 @@ class AirFixtures extends \Doctrine\Bundle\FixturesBundle\Fixture implements \Sy
                                'city' => 'Paris',
                                'latitude' => 48.871268,
                                'longitude' => 2.331832,
-                               'hotspot' => true
+                               'hotspot' => true,
+                               'indoor' => false
                        ],
                        [
                                'title' => 'Tino-Rossi garden',
@@ -153,7 +179,8 @@ class AirFixtures extends \Doctrine\Bundle\FixturesBundle\Fixture implements \Sy
                                'city' => 'Paris',
                                'latitude' => 48.847736,
                                'longitude' => 2.360953,
-                               'hotspot' => true
+                               'hotspot' => true,
+                               'indoor' => false
                        ],
                        [
                                'title' => 'Trocadero esplanade',
@@ -164,7 +191,8 @@ class AirFixtures extends \Doctrine\Bundle\FixturesBundle\Fixture implements \Sy
                                'city' => 'Paris',
                                'latitude' => 48.861888,
                                'longitude' => 2.288853,
-                               'hotspot' => false
+                               'hotspot' => false,
+                               'indoor' => false
                        ],
                        [
                                'title' => 'Colette place',
@@ -174,7 +202,8 @@ class AirFixtures extends \Doctrine\Bundle\FixturesBundle\Fixture implements \Sy
                                'city' => 'Paris',
                                'latitude' => 48.863219,
                                'longitude' => 2.335847,
-                               'hotspot' => false
+                               'hotspot' => false,
+                               'indoor' => false
                        ],
                        [
                                'title' => 'Swan island',
@@ -184,7 +213,8 @@ class AirFixtures extends \Doctrine\Bundle\FixturesBundle\Fixture implements \Sy
                                'city' => 'Paris',
                                'latitude' => 48.849976, #48.849976
                                'longitude' => 2.279603, #2.2796029,
-                               'hotspot' => false
+                               'hotspot' => false,
+                               'indoor' => false
                        ],
                        [
                                'title' => 'Jussieu esplanade',
@@ -194,7 +224,8 @@ class AirFixtures extends \Doctrine\Bundle\FixturesBundle\Fixture implements \Sy
                                'city' => 'Paris',
                                'latitude' => 48.847955, #48.8479548
                                'longitude' => 2.353291, #2.3532907,
-                               'hotspot' => false
+                               'hotspot' => false,
+                               'indoor' => false
                        ],
                        [
                                'title' => 'Orleans gallery',
@@ -204,7 +235,8 @@ class AirFixtures extends \Doctrine\Bundle\FixturesBundle\Fixture implements \Sy
                                'city' => 'Paris',
                                'latitude' => 48.863885,
                                'longitude' => 2.337387,
-                               'hotspot' => false
+                               'hotspot' => false,
+                               'indoor' => false
                        ],
                        [
                                'title' => 'Orsay museum',
@@ -214,7 +246,8 @@ class AirFixtures extends \Doctrine\Bundle\FixturesBundle\Fixture implements \Sy
                                'city' => 'Paris',
                                'latitude' => 48.860418,
                                'longitude' => 2.325815,
-                               'hotspot' => false
+                               'hotspot' => false,
+                               'indoor' => false
                        ],
                        [
                                'title' => 'Saint-Honore market',
@@ -224,7 +257,8 @@ class AirFixtures extends \Doctrine\Bundle\FixturesBundle\Fixture implements \Sy
                                'city' => 'Paris',
                                'latitude' => 48.866992,
                                'longitude' => 2.331752,
-                               'hotspot' => false
+                               'hotspot' => false,
+                               'indoor' => false
                        ],
                        [
                                'title' => 'Igor Stravinsky place',
@@ -234,7 +268,8 @@ class AirFixtures extends \Doctrine\Bundle\FixturesBundle\Fixture implements \Sy
                                'city' => 'Paris',
                                'latitude' => 48.859244,
                                'longitude' => 2.351289,
-                               'hotspot' => false
+                               'hotspot' => false,
+                               'indoor' => false
                        ],
                        [
                                'title' => 'Tokyo palace',
@@ -244,7 +279,8 @@ class AirFixtures extends \Doctrine\Bundle\FixturesBundle\Fixture implements \Sy
                                'city' => 'Paris',
                                'latitude' => 48.863827,
                                'longitude' => 2.297339,
-                               'hotspot' => false
+                               'hotspot' => false,
+                               'indoor' => false
                        ],
                        [
                                'title' => 'Drawings\' garden',
@@ -254,7 +290,8 @@ class AirFixtures extends \Doctrine\Bundle\FixturesBundle\Fixture implements \Sy
                                'city' => 'Paris',
                                'latitude' => 48.892503,
                                'longitude' => 2.389300,
-                               'hotspot' => false
+                               'hotspot' => false,
+                               'indoor' => false
                        ],
                        [
                                'title' => 'Louvre palace',
@@ -264,34 +301,25 @@ class AirFixtures extends \Doctrine\Bundle\FixturesBundle\Fixture implements \Sy
                                'city' => 'Paris',
                                'latitude' => 48.860386,
                                'longitude' => 2.332611,
-                               'hotspot' => false
+                               'hotspot' => false,
+                               'indoor' => false
                        ],
                        [
                                'title' => 'Monde garden',
-                               'short' => 'Monde',
                                'address' => '63 avenue Pierre Mendès-France',
                                'zipcode' => '75013',
                                'city' => 'Paris',
                                'latitude' => 48.840451,
                                'longitude' => 2.367638,
-                               'hotspot' => false
+                               'hotspot' => false,
+                               'indoor' => false
                        ]
                ];
 
                //Create locations
-               $locations = array();
+               $locations = [];
                foreach($locationTree as $locationData) {
-                       $location = new Location();
-                       $location->setTitle($locationData['title']);
-                       $location->setShort($locationData['short']);
-                       $location->setAddress($locationData['address']);
-                       $location->setZipcode($locationData['zipcode']);
-                       $location->setCity($locationData['city']);
-                       $location->setLatitude($locationData['latitude']);
-                       $location->setLongitude($locationData['longitude']);
-                       $location->setHotspot($locationData['hotspot']);
-                       $location->setCreated(new \DateTime('now'));
-                       $location->setUpdated(new \DateTime('now'));
+                       $location = new Location($locationData['title'], $locationData['address'], $locationData['zipcode'], $locationData['city'], $locationData['latitude'], $locationData['longitude'], $locationData['hotspot'], $locationData['indoor']);
                        $manager->persist($location);
                        $locations[$locationData['title']] = $location;
                        unset($location);
@@ -309,12 +337,9 @@ class AirFixtures extends \Doctrine\Bundle\FixturesBundle\Fixture implements \Sy
                ];
 
                //Create slots
-               $slots = array();
+               $slots = [];
                foreach($slotTree as $slotData) {
-                       $slot = new Slot();
-                       $slot->setTitle($slotData);
-                       $slot->setCreated(new \DateTime('now'));
-                       $slot->setUpdated(new \DateTime('now'));
+                       $slot = new Slot($slotData);
                        $manager->persist($slot);
                        $slots[$slot->getId()] = $slot;
                        unset($slot);
index caa585f25372fa9bf9051e14340d3f912d34ef4b..9500f466ee07beefcbf56ad216d04f3d290a4002 100644 (file)
@@ -1,10 +1,21 @@
-<?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\DependencyInjection;
 
 use Symfony\Component\Config\Definition\Builder\TreeBuilder;
 use Symfony\Component\Config\Definition\ConfigurationInterface;
 
+use Rapsys\AirBundle\RapsysAirBundle;
+
 /**
  * This is the class that validates and merges configuration from your app/config files.
  *
@@ -14,18 +25,33 @@ class Configuration implements ConfigurationInterface {
        /**
         * {@inheritdoc}
         */
-       public function getConfigTreeBuilder() {
-               $treeBuilder = new TreeBuilder('rapsys_air');
+       public function getConfigTreeBuilder(): TreeBuilder {
+               $treeBuilder = new TreeBuilder($alias = RapsysAirBundle::getAlias());
 
                // Here you should define the parameters that are allowed to
                // configure your bundle. See the documentation linked above for
                // more information on that topic.
                //Set defaults
                $defaults = [
-                       'site' => [
-                               'donate' => 'https://paypal.me/milongaraphael',
+                       'contact' => [
+                               'address' => 'contact@airlibre.eu',
+                               'name' => 'Libre Air'
+                       ],
+                       'copy' => [
+                               'by' => 'Rapsys',
+                               'link' => 'https://rapsys.eu',
+                               'long' => 'All rights reserved',
+                               'short' => 'Copyright 2019-2021',
+                               'title' => 'Rapsys'
+                       ],
+                       'donate' => 'https://paypal.me/milongaraphael',
+                       'facebook' => [
+                               'apps' => [3728770287223690],
+                               'height' => 630,
+                               'width' => 1200
+                       ],
+                       'icon' => [
                                'ico' => '@RapsysAir/ico/icon.ico',
-                               'logo' => '@RapsysAir/png/logo.png',
                                //The png icon array
                                //XXX: see https://www.emergeinteractive.com/insights/detail/the-essentials-of-favicons/
                                //XXX: see https://caniuse.com/#feat=link-icon-svg
@@ -63,47 +89,27 @@ class Configuration implements ConfigurationInterface {
                                        150 => '@RapsysAir/png/icon.150.png',
                                        70 => '@RapsysAir/png/icon.70.png'
                                ],
-                               'svg' => '@RapsysAir/svg/icon.svg',
-                               'title' => 'Libre Air',
-                               'url' => 'rapsys_air'
-                       ],
-                       'cache' => [
-                               'namespace' => 'airlibre',
-                               'lifetime' => 0
-                       ],
-                       'calendar' => [
-                               'calendar' => 'rmg68hd51sploubp5qffdthiak@group.calendar.google.com',
-                               'prefix' => 'airlibre',
-                               'project' => 'calendar-317315',
-                               'client' => '635317121880-usqucmne71jnmprl8br9khh2om4n8cmh.apps.googleusercontent.com',
-                               'secret' => 'HRsKd4FIc9gxQHM4IoBWnlbD'
-                       ],
-                       'copy' => [
-                               'by' => 'Rapsys',
-                               'link' => 'https://rapsys.eu',
-                               'long' => 'All rights reserved',
-                               'short' => 'Copyright 2019-2021',
-                               'title' => 'Rapsys'
-                       ],
-                       'contact' => [
-                               'title' => 'Libre Air',
-                               'mail' => 'contact@airlibre.eu'
+                               'svg' => '@RapsysAir/svg/icon.svg'
                        ],
-                       'facebook' => [
-                               'apps' => [3728770287223690],
-                               'height' => 630,
-                               'width' => 1200
-                       ],
-                       'locale' => '%kernel.default_locale%',
-                       'locales' => '%kernel.translator.fallbacks%',
                        //XXX: revert to underscore because of that shit:
                        //XXX: see https://symfony.com/doc/current/components/config/definition.html#normalization
                        //XXX: see https://github.com/symfony/symfony/issues/7405
-                       'languages' => '%rapsys_user.languages%',
-                       'path' => [
-                               'cache' => '%kernel.project_dir%/var/cache',
-                               'public' => dirname(__DIR__).'/Resources/public'
-                       ]
+                       //TODO: copy to '%rapsysuser.languages%',
+                       'languages' => [
+                               'en_gb' => 'English'
+                       ],
+                       //TODO: copy to '%kernel.default_locale%'
+                       'locale' => 'en_gb',
+                       //TODO: copy to '%kernel.translator.fallbacks%'
+                       'locales' => [ 'en_gb' ],
+                       'logo' => [
+                               'alt' => 'Libre Air\'s booking system logo',
+                               'png' => '@RapsysAir/png/logo.png',
+                               'svg' => '@RapsysAir/svg/logo.svg'
+                       ],
+                       'path' => is_link(($prefix = is_dir('public') ? './public/' : './').($link = 'bundles/'.str_replace('_', '', $alias))) && is_dir(realpath($prefix.$link)) || is_dir($prefix.$link) ? $link : dirname(__DIR__).'/Resources/public',
+                       'root' => 'rapsysair',
+            'title' => 'Libre Air\'s booking system'
                ];
 
                //Here we define the parameters that are allowed to configure the bundle.
@@ -117,37 +123,11 @@ class Configuration implements ConfigurationInterface {
                        ->getRootNode()
                                ->addDefaultsIfNotSet()
                                ->children()
-                                       ->arrayNode('site')
-                                               ->addDefaultsIfNotSet()
-                                               ->children()
-                                                       ->scalarNode('donate')->cannotBeEmpty()->defaultValue($defaults['site']['donate'])->end()
-                                                       ->scalarNode('ico')->cannotBeEmpty()->defaultValue($defaults['site']['ico'])->end()
-                                                       ->scalarNode('logo')->cannotBeEmpty()->defaultValue($defaults['site']['logo'])->end()
-                                                       ->arrayNode('png')
-                                                               ->treatNullLike([])
-                                                               ->defaultValue($defaults['site']['png'])
-                                                               ->scalarPrototype()->end()
-                                                       ->end()
-                                                       ->scalarNode('svg')->cannotBeEmpty()->defaultValue($defaults['site']['svg'])->end()
-                                                       ->scalarNode('title')->cannotBeEmpty()->defaultValue($defaults['site']['title'])->end()
-                                                       ->scalarNode('url')->cannotBeEmpty()->defaultValue($defaults['site']['url'])->end()
-                                               ->end()
-                                       ->end()
-                                       ->arrayNode('cache')
-                                               ->addDefaultsIfNotSet()
-                                               ->children()
-                                                       ->scalarNode('namespace')->defaultValue($defaults['cache']['namespace'])->end()
-                                                       ->integerNode('lifetime')->min(0)->defaultValue($defaults['cache']['lifetime'])->end()
-                                               ->end()
-                                       ->end()
-                                       ->arrayNode('calendar')
+                                       ->arrayNode('contact')
                                                ->addDefaultsIfNotSet()
                                                ->children()
-                                                       ->scalarNode('calendar')->defaultValue($defaults['calendar']['calendar'])->end()
-                                                       ->scalarNode('prefix')->defaultValue($defaults['calendar']['prefix'])->end()
-                                                       ->scalarNode('project')->defaultValue($defaults['calendar']['project'])->end()
-                                                       ->scalarNode('client')->defaultValue($defaults['calendar']['client'])->end()
-                                                       ->scalarNode('secret')->defaultValue($defaults['calendar']['secret'])->end()
+                                                       ->scalarNode('address')->cannotBeEmpty()->defaultValue($defaults['contact']['address'])->end()
+                                                       ->scalarNode('name')->cannotBeEmpty()->defaultValue($defaults['contact']['name'])->end()
                                                ->end()
                                        ->end()
                                        ->arrayNode('copy')
@@ -160,13 +140,7 @@ class Configuration implements ConfigurationInterface {
                                                        ->scalarNode('title')->defaultValue($defaults['copy']['title'])->end()
                                                ->end()
                                        ->end()
-                                       ->arrayNode('contact')
-                                               ->addDefaultsIfNotSet()
-                                               ->children()
-                                                       ->scalarNode('title')->cannotBeEmpty()->defaultValue($defaults['contact']['title'])->end()
-                                                       ->scalarNode('mail')->cannotBeEmpty()->defaultValue($defaults['contact']['mail'])->end()
-                                               ->end()
-                                       ->end()
+                                       ->scalarNode('donate')->cannotBeEmpty()->defaultValue($defaults['donate'])->end()
                                        ->arrayNode('facebook')
                                                ->addDefaultsIfNotSet()
                                                ->children()
@@ -179,16 +153,40 @@ class Configuration implements ConfigurationInterface {
                                                        ->integerNode('width')->min(0)->defaultValue($defaults['facebook']['width'])->end()
                                                ->end()
                                        ->end()
+                                       ->arrayNode('icon')
+                                               ->addDefaultsIfNotSet()
+                                               ->children()
+                                                       ->scalarNode('ico')->defaultValue($defaults['icon']['ico'])->end()
+                                                       ->scalarNode('png')->defaultValue($defaults['icon']['png'])->end()
+                                                       ->scalarNode('svg')->defaultValue($defaults['icon']['svg'])->end()
+                                               ->end()
+                                       ->end()
+                                       #TODO: see if we can't prevent key normalisation with ->normalizeKeys(false)
+                                       #->scalarNode('languages')->cannotBeEmpty()->defaultValue($defaults['languages'])->end()
+                                       ->variableNode('languages')
+                                               ->treatNullLike([])
+                                               ->defaultValue($defaults['languages'])
+                                               #->scalarPrototype()->end()
+                                       ->end()
                                        ->scalarNode('locale')->cannotBeEmpty()->defaultValue($defaults['locale'])->end()
-                                       ->scalarNode('locales')->cannotBeEmpty()->defaultValue($defaults['locales'])->end()
-                                       ->scalarNode('languages')->cannotBeEmpty()->defaultValue($defaults['languages'])->end()
-                                       ->arrayNode('path')
+                                       #TODO: see if we can't prevent key normalisation with ->normalizeKeys(false)
+                                       #->scalarNode('locales')->cannotBeEmpty()->defaultValue($defaults['locales'])->end()
+                                       ->variableNode('locales')
+                                               ->treatNullLike([])
+                                               ->defaultValue($defaults['locales'])
+                                               #->scalarPrototype()->end()
+                                       ->end()
+                                       ->arrayNode('logo')
                                                ->addDefaultsIfNotSet()
                                                ->children()
-                                                       ->scalarNode('cache')->defaultValue($defaults['path']['cache'])->end()
-                                                       ->scalarNode('public')->defaultValue($defaults['path']['public'])->end()
+                                                       ->scalarNode('alt')->defaultValue($defaults['logo']['alt'])->end()
+                                                       ->scalarNode('png')->defaultValue($defaults['logo']['png'])->end()
+                                                       ->scalarNode('svg')->defaultValue($defaults['logo']['svg'])->end()
                                                ->end()
                                        ->end()
+                                       ->scalarNode('path')->defaultValue($defaults['path'])->end()
+                                       ->scalarNode('root')->cannotBeEmpty()->defaultValue($defaults['root'])->end()
+                                       ->scalarNode('title')->cannotBeEmpty()->defaultValue($defaults['title'])->end()
                                ->end()
                        ->end();
 
index 3f7ea0971cfad29c5086a29de4cc0417584ee26b..b09b135eccd1749f2a9b2018d5721c0c3183429e 100644 (file)
@@ -1,4 +1,13 @@
-<?php
+<?php declare(strict_types=1);
+
+/*
+ * This file is part of the Rapsys PackBundle 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\DependencyInjection;
 
@@ -7,6 +16,10 @@ use Symfony\Component\DependencyInjection\Extension\Extension;
 use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
 use Symfony\Component\Translation\Loader\ArrayLoader;
 
+use Rapsys\AirBundle\RapsysAirBundle;
+
+use Rapsys\UserBundle\RapsysUserBundle;
+
 /**
  * This is the class that loads and manages your bundle configuration.
  *
@@ -14,49 +27,30 @@ use Symfony\Component\Translation\Loader\ArrayLoader;
  */
 class RapsysAirExtension extends Extension implements PrependExtensionInterface {
        /**
+        * {@inheritdoc}
+        *
         * Prepend the configuration
         *
-        * @desc Preload the configuration to allow sourcing as parameters
-        * {@inheritdoc}
+        * Preload the configuration to allow sourcing as parameters
         */
-       public function prepend(ContainerBuilder $container) {
-               //Load framework configurations
-               //XXX: required to extract default_locale and translation.fallbacks
-               $frameworks = $container->getExtensionConfig('framework');
-
-               //Recursively merge framework configurations
-               $framework = array_reduce(
-                       $frameworks,
-                       function ($res, $i) {
-                               return array_merge_recursive($res, $i);
-                       },
-                       []
-               );
+       public function prepend(ContainerBuilder $container): void {
+               /*Load rapsysuser configurations
+               $rapsysusers = $container->getExtensionConfig($alias = RapsysUserBundle::getAlias());
 
-               //Set translator fallbacks
-               $container->setParameter('kernel.translator.fallbacks', $framework['translator']['fallbacks']);
-
-               //Set default locale
-               $container->setParameter('kernel.default_locale', $framework['default_locale']);
-
-               //Load rapsys_user configurations
-               //XXX: required to extract default_locale and translation.fallbacks
-               $rapsys_users = $container->getExtensionConfig('rapsys_user');
-
-               //Recursively merge rapsys_user configurations
-               $rapsys_user = array_reduce(
-                       $rapsys_users,
+               //Recursively merge rapsysuser configurations
+               $rapsysuser = array_reduce(
+                       $rapsysusers,
                        function ($res, $i) {
                                return array_merge_recursive($res, $i);
                        },
                        []
                );
 
-               //Set rapsys_user.languages key
-               $container->setParameter('rapsys_user.languages', $rapsys_user['languages']);
+               //Set rapsysuser.languages key
+               $container->setParameter($alias, $rapsysuser);*/
 
                //Process the configuration
-               $configs = $container->getExtensionConfig($this->getAlias());
+               $configs = $container->getExtensionConfig($alias = RapsysAirBundle::getAlias());
 
                //Load configuration
                $configuration = $this->getConfiguration($configs, $container);
@@ -67,30 +61,29 @@ class RapsysAirExtension extends Extension implements PrependExtensionInterface
                //Detect when no user configuration is provided
                if ($configs === [[]]) {
                        //Prepend default config
-                       $container->prependExtensionConfig($this->getAlias(), $config);
+                       $container->prependExtensionConfig($alias, $config);
                }
 
                //Save configuration in parameters
-               $container->setParameter($this->getAlias(), $config);
+               $container->setParameter($alias, $config);
 
                //Store flattened array in parameters
-               //XXX: don't flatten rapsys_air.site.png key which is required to be an array
-               foreach($this->flatten($config, $this->getAlias(), 10, '.', ['rapsys_air.site.png', 'rapsys_air.facebook.apps', 'rapsys_air.locales', 'rapsys_air.languages']) as $k => $v) {
+               //XXX: don't flatten rapsysair.site.png key which is required to be an array
+               foreach($this->flatten($config, $alias, 10, '.', ['rapsysair.copy', 'rapsysair.icon', 'rapsysair.icon.png', 'rapsysair.logo', 'rapsysair.facebook.apps', 'rapsysair.locales', 'rapsysair.languages']) as $k => $v) {
                        $container->setParameter($k, $v);
                }
-       }
 
-       /**
-        * {@inheritdoc}
-        */
-       public function load(array $configs, ContainerBuilder $container) {
+               //Set rapsysair.alias key
+               $container->setParameter($alias.'.alias', $alias);
+
+               //Set rapsysair.version key
+               $container->setParameter($alias.'.version', RapsysAirBundle::getVersion());
        }
 
        /**
         * {@inheritdoc}
         */
-       public function getAlias() {
-               return 'rapsys_air';
+       public function load(array $configs, ContainerBuilder $container): void {
        }
 
        /**
@@ -124,4 +117,13 @@ class RapsysAirExtension extends Extension implements PrependExtensionInterface
                //Return result
                return $res;
        }
+
+       /**
+        * {@inheritdoc}
+        *
+        * @xxx Required by kernel to load renamed alias configuration
+        */
+       public function getAlias(): string {
+               return RapsysAirBundle::getAlias();
+       }
 }
index 5d3120360342c6761cf51c76bca43fe7e2df1a60..b2672a19aa898ff58a383fed4bb273c27fe7e957 100644 (file)
@@ -1,11 +1,11 @@
 <?php declare(strict_types=1);
 
 /*
- * this file is part of the rapsys packbundle package.
+ * This file is part of the Rapsys AirBundle package.
  *
- * (c) raphaël gertz <symfony@rapsys.eu>
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
  *
- * for the full copyright and license information, please view the license
+ * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
  */
 
@@ -18,56 +18,52 @@ use Doctrine\ORM\Event\PreUpdateEventArgs;
  */
 class Application {
        /**
-        * @var integer
+        * Primary key
         */
-       private $id;
+       private ?int $id = null;
 
        /**
-        * @var Dance
+        * Dance instance
         */
-       private $dance;
+       private Dance $dance;
 
        /**
-        * @var float
+        * Score
         */
-       private $score;
+       private ?float $score = null;
 
        /**
-        * @var \DateTime
+        * Cancel datetime
         */
-       private $canceled;
+       private ?\DateTime $canceled = null;
 
        /**
-        * @var \DateTime
+        * Create datetime
         */
-       private $created;
+       private \DateTime $created;
 
        /**
-        * @var \DateTime
+        * Update datetime
         */
-       private $updated;
+       private \DateTime $updated;
 
        /**
-        * @var \Rapsys\AirBundle\Entity\Session
+        * Session instance
         */
-       private $session;
+       private $session = null;
 
        /**
-        * @var \Rapsys\AirBundle\Entity\User
+        * User instance
         */
-       private $user;
+       private $user = null;
 
        /**
         * Constructor
         */
        public function __construct() {
                //Set defaults
-               $this->score = null;
-               $this->canceled = null;
                $this->created = new \DateTime('now');
                $this->updated = new \DateTime('now');
-               $this->session = null;
-               $this->user = null;
        }
 
        /**
@@ -75,7 +71,7 @@ class Application {
         *
         * @return integer
         */
-       public function getId(): int {
+       public function getId(): ?int {
                return $this->id;
        }
 
@@ -238,7 +234,7 @@ class Application {
         */
        public function preUpdate(PreUpdateEventArgs $eventArgs) {
                //Check that we have an application instance
-               if (($application = $eventArgs->getEntity()) instanceof Application) {
+               if (($application = $eventArgs->getObject()) instanceof Application) {
                        //Set updated value
                        $application->setUpdated(new \DateTime('now'));
                }
index d8461d7f5a021142075bea9ea0b164fb209451db..5e111c11ee1ca3c108c4c85edc4d540237f28429 100644 (file)
@@ -1,11 +1,11 @@
 <?php declare(strict_types=1);
 
 /*
- * this file is part of the rapsys packbundle package.
+ * This file is part of the Rapsys AirBundle package.
  *
- * (c) raphaël gertz <symfony@rapsys.eu>
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
  *
- * for the full copyright and license information, please view the license
+ * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
  */
 
@@ -13,5 +13,8 @@ namespace Rapsys\AirBundle\Entity;
 
 use Rapsys\UserBundle\Entity\Civility as BaseCivility;
 
+/**
+ * {@inheritdoc}
+ */
 class Civility extends BaseCivility {
 }
diff --git a/Entity/Country.php b/Entity/Country.php
new file mode 100644 (file)
index 0000000..48a3037
--- /dev/null
@@ -0,0 +1,227 @@
+<?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\Entity;
+
+use Doctrine\Common\Collections\Collection;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\ORM\Event\PreUpdateEventArgs;
+
+/**
+ * Country
+ */
+class Country {
+       /**
+        * Primary key
+        */
+       private ?int $id = null;
+
+       /**
+        * Create datetime
+        */
+       private \DateTime $created;
+
+       /**
+        * Update datetime
+        */
+       private \DateTime $updated;
+
+       /**
+        * Users collection
+        */
+       private Collection $users;
+
+       /**
+        * Constructor
+        *
+        * @param string $code The country code
+        * @param string $alpha The country alpha
+        * @param string $title The country title
+        */
+       public function __construct(private string $code, private string $alpha, private string $title) {
+               //Set defaults
+               $this->created = new \DateTime('now');
+               $this->updated = new \DateTime('now');
+
+               //Set collections
+               $this->users = new ArrayCollection();
+       }
+
+       /**
+        * Get id
+        *
+        * @return integer
+        */
+       public function getId(): ?int {
+               return $this->id;
+       }
+
+       /**
+        * Set code
+        *
+        * @param string $code
+        *
+        * @return Country
+        */
+       public function setCode(string $code): Country {
+               $this->code = $code;
+
+               return $this;
+       }
+
+       /**
+        * Get code
+        *
+        * @return string
+        */
+       public function getCode(): string {
+               return $this->code;
+       }
+
+       /**
+        * Set alpha
+        *
+        * @param string $alpha
+        *
+        * @return Country
+        */
+       public function setAlpha(string $alpha): Country {
+               $this->alpha = $alpha;
+
+               return $this;
+       }
+
+       /**
+        * Get alpha
+        *
+        * @return string
+        */
+       public function getAlpha(): string {
+               return $this->alpha;
+       }
+
+       /**
+        * Set title
+        *
+        * @param string $title
+        *
+        * @return Country
+        */
+       public function setTitle(string $title): Country {
+               $this->title = $title;
+
+               return $this;
+       }
+
+       /**
+        * Get title
+        *
+        * @return string
+        */
+       public function getTitle(): string {
+               return $this->title;
+       }
+
+       /**
+        * Set created
+        *
+        * @param \DateTime $created
+        *
+        * @return Country
+        */
+       public function setCreated(\DateTime $created): Country {
+               $this->created = $created;
+
+               return $this;
+       }
+
+       /**
+        * Get created
+        *
+        * @return \DateTime
+        */
+       public function getCreated(): \DateTime {
+               return $this->created;
+       }
+
+       /**
+        * Set updated
+        *
+        * @param \DateTime $updated
+        *
+        * @return Country
+        */
+       public function setUpdated(\DateTime $updated): Country {
+               $this->updated = $updated;
+
+               return $this;
+       }
+
+       /**
+        * Get updated
+        *
+        * @return \DateTime
+        */
+       public function getUpdated(): \DateTime {
+               return $this->updated;
+       }
+
+       /**
+        * Add user
+        *
+        * @param User $user
+        *
+        * @return Country
+        */
+       public function addUser(User $user): User {
+               $this->users[] = $user;
+
+               return $this;
+       }
+
+       /**
+        * Remove user
+        *
+        * @param User $user
+        */
+       public function removeUser(User $user): bool {
+               return $this->users->removeElement($user);
+       }
+
+       /**
+        * Get users
+        *
+        * @return ArrayCollection
+        */
+       public function getUsers(): ArrayCollection {
+               return $this->users;
+       }
+
+       /**
+        * {@inheritdoc}
+        */
+       public function preUpdate(PreUpdateEventArgs $eventArgs) {
+               //Check that we have an country instance
+               if (($country = $eventArgs->getObject()) instanceof Country) {
+                       //Set updated value
+                       $country->setUpdated(new \DateTime('now'));
+               }
+       }
+
+       /**
+        * Returns a string representation of the country
+        *
+        * @return string
+        */
+       public function __toString(): string {
+               return $this->title;
+       }
+}
index e3779da83fc03d8558dbaa0c4dead87b3aea33a8..6ae5f1bb14dcd72bd7e7e47b4a3f94ba44cc3887 100644 (file)
@@ -1,16 +1,17 @@
 <?php declare(strict_types=1);
 
 /*
- * this file is part of the rapsys packbundle package.
+ * This file is part of the Rapsys AirBundle package.
  *
- * (c) raphaël gertz <symfony@rapsys.eu>
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
  *
- * for the full copyright and license information, please view the license
+ * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
  */
 
 namespace Rapsys\AirBundle\Entity;
 
+use Doctrine\Common\Collections\Collection;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\ORM\Event\PreUpdateEventArgs;
 
@@ -19,42 +20,42 @@ use Doctrine\ORM\Event\PreUpdateEventArgs;
  */
 class Dance {
        /**
-        * @var integer
+        * Primary key
         */
-       private $id;
+       private ?int $id = null;
 
        /**
-        * @var string
+        * Create datetime
         */
-       protected $title;
+       private \DateTime $created;
 
        /**
-        * @var \DateTime
+        * Update datetime
         */
-       private $created;
+       private \DateTime $updated;
 
        /**
-        * @var \DateTime
+        * Applications collection
         */
-       private $updated;
+       private Collection $applications;
 
        /**
-        * @var ArrayCollection
+        * Users collection
         */
-       private $applications;
-
-       /**
-        * @var ArrayCollection
-        */
-       private $users;
+       private Collection $users;
 
        /**
         * Constructor
+        *
+        * @param string $name The dance name
+        * @param string $type The dance type
         */
-       public function __construct() {
+       public function __construct(private string $name, private string $type) {
                //Set defaults
                $this->created = new \DateTime('now');
                $this->updated = new \DateTime('now');
+
+               //Set collections
                $this->applications = new ArrayCollection();
                $this->users = new ArrayCollection();
        }
@@ -64,30 +65,52 @@ class Dance {
         *
         * @return integer
         */
-       public function getId(): int {
+       public function getId(): ?int {
                return $this->id;
        }
 
        /**
-        * Set title
+        * Set name
+        *
+        * @param string $name
+        *
+        * @return Dance
+        */
+       public function setName(string $name): Dance {
+               $this->name = $name;
+
+               return $this;
+       }
+
+       /**
+        * Get name
+        *
+        * @return string
+        */
+       public function getName(): string {
+               return $this->name;
+       }
+
+       /**
+        * Set type
         *
-        * @param string $title
+        * @param string $type
         *
         * @return Dance
         */
-       public function setTitle(string $title): Dance {
-               $this->title = $title;
+       public function setType(string $type): Dance {
+               $this->type = $type;
 
                return $this;
        }
 
        /**
-        * Get title
+        * Get type
         *
         * @return string
         */
-       public function getTitle(): string {
-               return $this->title;
+       public function getType(): string {
+               return $this->type;
        }
 
        /**
@@ -175,6 +198,9 @@ class Dance {
         * @return Dance
         */
        public function addUser(User $user): Dance {
+               //Add from owning side
+               $user->addDance($this);
+
                $this->users[] = $user;
 
                return $this;
@@ -188,6 +214,13 @@ class Dance {
         * @return bool
         */
        public function removeUser(User $user): bool {
+               if (!$this->dances->contains($user)) {
+                       return true;
+               }
+
+               //Remove from owning side
+               $user->removeDance($this);
+
                return $this->users->removeElement($user);
        }
 
@@ -204,8 +237,8 @@ class Dance {
         * {@inheritdoc}
         */
        public function preUpdate(PreUpdateEventArgs $eventArgs) {
-               //Check that we have an session instance
-               if (($dance = $eventArgs->getEntity()) instanceof Dance) {
+               //Check that we have a dance instance
+               if (($dance = $eventArgs->getObject()) instanceof Dance) {
                        //Set updated value
                        $dance->setUpdated(new \DateTime('now'));
                }
@@ -217,6 +250,6 @@ class Dance {
         * @return string
         */
        public function __toString(): string {
-               return $this->title;
+               return $this->name.' '.lcfirst($this->type);
        }
 }
diff --git a/Entity/GoogleCalendar.php b/Entity/GoogleCalendar.php
new file mode 100644 (file)
index 0000000..f4ef2ce
--- /dev/null
@@ -0,0 +1,198 @@
+<?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\Entity;
+
+use Doctrine\ORM\Event\PreUpdateEventArgs;
+
+/**
+ * GoogleCalendar
+ */
+class GoogleCalendar {
+       /**
+        * Primary key
+        */
+       private ?int $id = null;
+
+       /**
+        * Create datetime
+        */
+       private \DateTime $created;
+
+       /**
+        * Update datetime
+        */
+       private \DateTime $updated;
+
+       /**
+        * Constructor
+        *
+        * @param GoogleToken $googleToken The google token
+        * @param string $mail The google calendar id
+        * @param string $summary The google calendar summary
+        * @param \DateTime $synchronized The google calendar last synchronization
+        */
+       public function __construct(private GoogleToken $googleToken, private string $mail, private string $summary, private \DateTime $synchronized = new \DateTime('now')) {
+               //Set defaults
+               $this->created = new \DateTime('now');
+               $this->updated = new \DateTime('now');
+       }
+
+       /**
+        * Get id
+        *
+        * @return ?int
+        */
+       public function getId(): ?int {
+               return $this->id;
+       }
+
+       /**
+        * Set mail
+        *
+        * @param string $mail
+        * @return GoogleCalendar
+        */
+       public function setMail(string $mail): GoogleCalendar {
+               $this->mail = $mail;
+
+               return $this;
+       }
+
+       /**
+        * Get mail
+        *
+        * @return string
+        */
+       public function getMail(): string {
+               return $this->mail;
+       }
+
+       /**
+        * Set summary
+        *
+        * @param string $summary
+        * @return GoogleCalendar
+        */
+       public function setSummary(string $summary): GoogleCalendar {
+               $this->summary = $summary;
+
+               return $this;
+       }
+
+       /**
+        * Get summary
+        *
+        * @return string
+        */
+       public function getSummary(): string {
+               return $this->summary;
+       }
+
+       /**
+        * Set synchronized
+        *
+        * @param \DateTime $synchronized
+        *
+        * @return GoogleCalendar
+        */
+       public function setSynchronized(\DateTime $synchronized): GoogleCalendar {
+               $this->synchronized = $synchronized;
+
+               return $this;
+       }
+
+       /**
+        * Get synchronized
+        *
+        * @return \DateTime
+        */
+       public function getSynchronized(): \DateTime {
+               return $this->synchronized;
+       }
+
+       /**
+        * Set created
+        *
+        * @param \DateTime $created
+        *
+        * @return GoogleCalendar
+        */
+       public function setCreated(\DateTime $created): GoogleCalendar {
+               $this->created = $created;
+
+               return $this;
+       }
+
+       /**
+        * Get created
+        *
+        * @return \DateTime
+        */
+       public function getCreated(): \DateTime {
+               return $this->created;
+       }
+
+       /**
+        * Set updated
+        *
+        * @param \DateTime $updated
+        *
+        * @return GoogleCalendar
+        */
+       public function setUpdated(\DateTime $updated): GoogleCalendar {
+               $this->updated = $updated;
+
+               return $this;
+       }
+
+       /**
+        * Get updated
+        *
+        * @return \DateTime
+        */
+       public function getUpdated(): \DateTime {
+               return $this->updated;
+       }
+
+       /**
+        * Set google token
+        *
+        * @param \Rapsys\AirBundle\Entity\GoogleToken $googleToken
+        *
+        * @return GoogleCalendar
+        */
+       public function setGoogleToken(GoogleToken $googleToken): GoogleCalendar {
+               $this->googleToken = $googleToken;
+
+               return $this;
+       }
+
+       /**
+        * Get google token
+        *
+        * @return \Rapsys\AirBundle\Entity\GoogleToken
+        */
+       public function getGoogleToken(): GoogleToken {
+               return $this->googleToken;
+       }
+
+       /**
+        * {@inheritdoc}
+        */
+       public function preUpdate(PreUpdateEventArgs $eventArgs): ?GoogleCalendar {
+               //Check that we have an snippet instance
+               if (($entity = $eventArgs->getObject()) instanceof GoogleCalendar) {
+                       //Set updated value
+                       return $entity->setUpdated(new \DateTime('now'));
+               }
+       }
+}
diff --git a/Entity/GoogleToken.php b/Entity/GoogleToken.php
new file mode 100644 (file)
index 0000000..e44b176
--- /dev/null
@@ -0,0 +1,263 @@
+<?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\Entity;
+
+use Doctrine\Common\Collections\Collection;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\ORM\Event\PreUpdateEventArgs;
+
+/**
+ * GoogleToken
+ */
+class GoogleToken {
+       /**
+        * Primary key
+        */
+       private ?int $id = null;
+
+       /**
+        * Create datetime
+        */
+       private \DateTime $created;
+
+       /**
+        * Update datetime
+        */
+       private \DateTime $updated;
+
+       /**
+        * Google calendars collection
+        */
+       private Collection $googleCalendars;
+
+       /**
+        * Constructor
+        *
+        * @param User $user The user instance
+        * @param string The token user mail
+        * @param string The access token identifier
+        * @param \DateTime The access token expires
+        * @param ?string The refresh token identifier
+        */
+       public function __construct(private User $user, private string $mail, private string $access, private \DateTime $expired, private ?string $refresh = null) {
+               //Set defaults
+               $this->created = new \DateTime('now');
+               $this->updated = new \DateTime('now');
+
+               //Set collections
+               $this->googleCalendars = new ArrayCollection();
+       }
+
+       /**
+        * Get id
+        *
+        * @return ?int
+        */
+       public function getId(): ?int {
+               return $this->id;
+       }
+
+       /**
+        * Set mail
+        *
+        * @param string $mail
+        * @return GoogleToken
+        */
+       public function setMail(string $mail): GoogleToken {
+               $this->mail = $mail;
+
+               return $this;
+       }
+
+       /**
+        * Get mail
+        *
+        * @return string
+        */
+       public function getMail(): string {
+               return $this->mail;
+       }
+
+       /**
+        * Set access
+        *
+        * @param string $access
+        *
+        * @return GoogleToken
+        */
+       public function setAccess(string $access): GoogleToken {
+               $this->access = $access;
+
+               return $this;
+       }
+
+       /**
+        * Get access
+        *
+        * @return string
+        */
+       public function getAccess(): string {
+               return $this->access;
+       }
+
+       /**
+        * Set refresh
+        *
+        * @param string $refresh
+        *
+        * @return GoogleToken
+        */
+       public function setRefresh(?string $refresh): GoogleToken {
+               $this->refresh = $refresh;
+
+               return $this;
+       }
+
+       /**
+        * Get refresh
+        *
+        * @return string
+        */
+       public function getRefresh(): ?string {
+               return $this->refresh;
+       }
+
+       /**
+        * Set expired
+        *
+        * @param \DateTime $expired
+        *
+        * @return GoogleToken
+        */
+       public function setExpired(\DateTime $expired): GoogleToken {
+               $this->expired = $expired;
+
+               return $this;
+       }
+
+       /**
+        * Get expired
+        *
+        * @return \DateTime
+        */
+       public function getExpired(): \DateTime {
+               return $this->expired;
+       }
+
+       /**
+        * Set created
+        *
+        * @param \DateTime $created
+        *
+        * @return GoogleToken
+        */
+       public function setCreated(\DateTime $created): GoogleToken {
+               $this->created = $created;
+
+               return $this;
+       }
+
+       /**
+        * Get created
+        *
+        * @return \DateTime
+        */
+       public function getCreated(): \DateTime {
+               return $this->created;
+       }
+
+       /**
+        * Set updated
+        *
+        * @param \DateTime $updated
+        *
+        * @return GoogleToken
+        */
+       public function setUpdated(\DateTime $updated): GoogleToken {
+               $this->updated = $updated;
+
+               return $this;
+       }
+
+       /**
+        * Get updated
+        *
+        * @return \DateTime
+        */
+       public function getUpdated(): \DateTime {
+               return $this->updated;
+       }
+
+       /**
+        * Add google calendar
+        *
+        * @param GoogleCalendar $googleCalendar
+        *
+        * @return User
+        */
+       public function addGoogleCalendar(GoogleCalendar $googleCalendar): User {
+               $this->googleCalendars[] = $googleCalendar;
+
+               return $this;
+       }
+
+       /**
+        * Remove google calendar
+        *
+        * @param GoogleCalendar $googleCalendar
+        */
+       public function removeGoogleCalendar(GoogleCalendar $googleCalendar): bool {
+               return $this->googleCalendars->removeElement($googleCalendar);
+       }
+
+       /**
+        * Get google calendars
+        *
+        * @return \Doctrine\Common\Collections\Collection
+        */
+       public function getGoogleCalendars(): Collection {
+               return $this->googleCalendars;
+       }
+
+       /**
+        * Set user
+        *
+        * @param \Rapsys\AirBundle\Entity\User $user
+        *
+        * @return GoogleToken
+        */
+       public function setUser(User $user): GoogleToken {
+               $this->user = $user;
+
+               return $this;
+       }
+
+       /**
+        * Get user
+        *
+        * @return \Rapsys\AirBundle\Entity\User
+        */
+       public function getUser(): User {
+               return $this->user;
+       }
+
+       /**
+        * {@inheritdoc}
+        */
+       public function preUpdate(PreUpdateEventArgs $eventArgs): ?GoogleToken {
+               //Check that we have an snippet instance
+               if (($entity = $eventArgs->getObject()) instanceof GoogleToken) {
+                       //Set updated value
+                       return $entity->setUpdated(new \DateTime('now'));
+               }
+       }
+}
index b904a0df873e1ba2e7a96d9d9cd9aa92703d8a7e..6e340ad19fd9d68d0ee54e249a829ffceca0b656 100644 (file)
@@ -1,11 +1,11 @@
 <?php declare(strict_types=1);
 
 /*
- * this file is part of the rapsys packbundle package.
+ * This file is part of the Rapsys AirBundle package.
  *
- * (c) raphaël gertz <symfony@rapsys.eu>
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
  *
- * for the full copyright and license information, please view the license
+ * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
  */
 
@@ -13,5 +13,8 @@ namespace Rapsys\AirBundle\Entity;
 
 use Rapsys\UserBundle\Entity\Group as BaseGroup;
 
+/**
+ * {@inheritdoc}
+ */
 class Group extends BaseGroup {
 }
index 57af698c4ced30112a6c7fcd7d67939ffe248967..d70bddf5694320a19d487fbe55d465fd9756d7e7 100644 (file)
@@ -1,16 +1,17 @@
 <?php declare(strict_types=1);
 
 /*
- * this file is part of the rapsys packbundle package.
+ * This file is part of the Rapsys AirBundle package.
  *
- * (c) raphaël gertz <symfony@rapsys.eu>
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
  *
- * for the full copyright and license information, please view the license
+ * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
  */
 
 namespace Rapsys\AirBundle\Entity;
 
+use Doctrine\Common\Collections\Collection;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\ORM\Event\PreUpdateEventArgs;
 
@@ -19,77 +20,49 @@ use Doctrine\ORM\Event\PreUpdateEventArgs;
  */
 class Location {
        /**
-        * @var integer
+        * Primary key
         */
-       private $id;
+       private ?int $id = null;
 
        /**
         * @var string
         */
-       private $title;
+       private ?string $description = null;
 
        /**
-        * @var string
-        */
-       private $address;
-
-       /**
-        * @var string
+        * Create datetime
         */
-       private $zipcode;
+       private \DateTime $created;
 
        /**
-        * @var string
+        * Update datetime
         */
-       private $city;
+       private \DateTime $updated;
 
        /**
-        * @var string
+        * Sessions collection
         */
-       private $latitude;
+       private Collection $sessions;
 
        /**
-        * @var string
+        * Snippets collection
         */
-       private $longitude;
+       private Collection $snippets;
 
        /**
-        * @var boolean
+        * Users collection
         */
-       private $hotspot;
-
-       /**
-        * @var \DateTime
-        */
-       private $created;
-
-       /**
-        * @var \DateTime
-        */
-       private $updated;
-
-       /**
-        * @var ArrayCollection
-        */
-       private $sessions;
-
-       /**
-        * @var ArrayCollection
-        */
-       private $snippets;
-
-       /**
-        * @var ArrayCollection
-        */
-       private $users;
+       private Collection $users;
 
        /**
         * Constructor
         */
-       public function __construct() {
+       public function __construct(private string $title = '', private string $address = '', private string $zipcode = '0', private string $city = '', private string $latitude = '0', private string $longitude = '0', private bool $hotspot = false, private bool $indoor = false) {
                //Set defaults
                $this->created = new \DateTime('now');
                $this->updated = new \DateTime('now');
+
+               //Set collections
                $this->sessions = new ArrayCollection();
                $this->snippets = new ArrayCollection();
                $this->users = new ArrayCollection();
@@ -100,7 +73,7 @@ class Location {
         *
         * @return integer
         */
-       public function getId(): int {
+       public function getId(): ?int {
                return $this->id;
        }
 
@@ -126,6 +99,28 @@ class Location {
                return $this->title;
        }
 
+       /**
+        * Set description
+        *
+        * @param string $description
+        *
+        * @return Location
+        */
+       public function setDescription(?string $description): Location {
+               $this->description = $description;
+
+               return $this;
+       }
+
+       /**
+        * Get description
+        *
+        * @return string
+        */
+       public function getDescription(): ?string {
+               return $this->description;
+       }
+
        /**
         * Set address
         *
@@ -236,10 +231,32 @@ class Location {
                return $this->longitude;
        }
 
+       /**
+        * Set indoor
+        *
+        * @param bool $indoor
+        *
+        * @return Session
+        */
+       public function setIndoor(bool $indoor): Location {
+               $this->indoor = $indoor;
+
+               return $this;
+       }
+
+       /**
+        * Get indoor
+        *
+        * @return bool
+        */
+       public function getIndoor(): bool {
+               return $this->indoor;
+       }
+
        /**
         * Set hotspot
         *
-        * @param boolean $hotspot
+        * @param bool $hotspot
         *
         * @return Session
         */
@@ -252,7 +269,7 @@ class Location {
        /**
         * Get hotspot
         *
-        * @return boolean
+        * @return bool
         */
        public function getHotspot(): bool {
                return $this->hotspot;
@@ -319,7 +336,7 @@ class Location {
         * Remove session
         *
         * @param Session $session
-        * @return boolean
+        * @return bool
         */
        public function removeSession(Session $session): bool {
                return $this->sessions->removeElement($session);
@@ -351,7 +368,7 @@ class Location {
         * Remove snippet
         *
         * @param Snippet $snippet
-        * @return boolean
+        * @return bool
         */
        public function removeSnippet(Snippet $snippet): bool {
                return $this->snippets->removeElement($snippet);
@@ -374,6 +391,9 @@ class Location {
         * @return Location
         */
        public function addUser(User $user): Location {
+               //Add from owning side
+               $user->addLocation($this);
+
                $this->users[] = $user;
 
                return $this;
@@ -383,9 +403,16 @@ class Location {
         * Remove user
         *
         * @param User $user
-        * @return boolean
+        * @return bool
         */
        public function removeUser(User $user): bool {
+               if (!$this->locations->contains($user)) {
+                       return true;
+               }
+
+               //Remove from owning side
+               $user->removeLocation($this);
+
                return $this->users->removeElement($user);
        }
 
@@ -403,7 +430,7 @@ class Location {
         */
        public function preUpdate(PreUpdateEventArgs $eventArgs) {
                //Check that we have a location instance
-               if (($location = $eventArgs->getEntity()) instanceof Location) {
+               if (($location = $eventArgs->getObject()) instanceof Location) {
                        //Set updated value
                        $location->setUpdated(new \DateTime('now'));
                }
index 084580e6af8debfbcd0dd6a0f63f31911a315352..f74963cdb338dd2914d7c9fc189e2733abb12761 100644 (file)
 <?php declare(strict_types=1);
 
 /*
- * this file is part of the rapsys packbundle package.
+ * This file is part of the Rapsys AirBundle package.
  *
- * (c) raphaël gertz <symfony@rapsys.eu>
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
  *
- * for the full copyright and license information, please view the license
+ * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
  */
 
 namespace Rapsys\AirBundle\Entity;
 
+use Doctrine\Common\Collections\Collection;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\ORM\Event\PreUpdateEventArgs;
 
+use Rapsys\PackBundle\Util\IntlUtil;
+
 /**
  * Session
  */
 class Session {
        /**
-        * @var integer
-        */
-       private $id;
-
-       /**
-        * @var \DateTime
-        */
-       private $date;
-
-       /**
-        * @var \DateTime
+        * Primary key
         */
-       private $begin;
+       private ?int $id = null;
 
        /**
-        * @var \DateTime
+        * Begin time
         */
-       private $start;
+       private ?\DateTime $begin = null;
 
        /**
-        * @var \DateTime
+        * Computed start datetime
         */
-       private $length;
+       private ?\DateTime $start = null;
 
        /**
-        * @var \DateTime
+        * Length time
         */
-       private $stop;
+       private ?\DateTime $length = null;
 
        /**
-        * @var boolean
+        * Computed stop datetime
         */
-       private $premium;
+       private ?\DateTime $stop = null;
 
        /**
-        * @var float
+        * Premium
         */
-       private $rainfall;
+       private ?bool $premium = null;
 
        /**
-        * @var float
+        * Rain mm
         */
-       private $rainrisk;
+       private ?float $rainfall = null;
 
        /**
-        * @var float
+        * Rain chance
         */
-       private $realfeel;
+       private ?float $rainrisk = null;
 
        /**
-        * @var float
+        * Real feel temperature
         */
-       private $realfeelmin;
+       private ?float $realfeel = null;
 
        /**
-        * @var float
+        * Real feel minimum temperature
         */
-       private $realfeelmax;
+       private ?float $realfeelmin = null;
 
        /**
-        * @var float
+        * Real feel maximum temperature
         */
-       private $temperature;
+       private ?float $realfeelmax = null;
 
        /**
-        * @var float
+        * Temperature
         */
-       private $temperaturemin;
+       private ?float $temperature = null;
 
        /**
-        * @var float
+        * Minimum temperature
         */
-       private $temperaturemax;
+       private ?float $temperaturemin = null;
 
        /**
-        * @var \DateTime
+        * Maximum temperature
         */
-       private $locked;
+       private ?float $temperaturemax = null;
 
        /**
-        * @var \DateTime
+        * Lock datetime
         */
-       private $created;
+       private ?\DateTime $locked = null;
 
        /**
-        * @var \DateTime
+        * Create datetime
         */
-       private $updated;
+       private \DateTime $created;
 
        /**
-        * @var Application
+        * Update datetime
         */
-       private $application;
+       private \DateTime $updated;
 
        /**
-        * @var Location
+        * Application instance
         */
-       private $location;
+       private ?Application $application = null;
 
        /**
-        * @var Slot
+        * Applications collection
         */
-       private $slot;
-
-       /**
-        * @var ArrayCollection
-        */
-       private $applications;
+       private Collection $applications;
 
        /**
         * Constructor
         */
-       public function __construct() {
+       public function __construct(private \DateTime $date, private Location $location, private Slot $slot) {
                //Set defaults
-               $this->begin = null;
-               $this->start = null;
-               $this->length = null;
-               $this->stop = null;
-               $this->premium = null;
-               $this->rainfall = null;
-               $this->rainrisk = null;
-               $this->realfeel = null;
-               $this->realfeelmin = null;
-               $this->realfeelmax = null;
-               $this->temperature = null;
-               $this->temperaturemin = null;
-               $this->temperaturemax = null;
-               $this->locked = null;
                $this->created = new \DateTime('now');
                $this->updated = new \DateTime('now');
+
+               //Set collections
                $this->applications = new ArrayCollection();
        }
 
@@ -157,7 +133,7 @@ class Session {
         *
         * @return integer
         */
-       public function getId(): int {
+       public function getId(): ?int {
                return $this->id;
        }
 
@@ -285,11 +261,11 @@ class Session {
        /**
         * Set premium
         *
-        * @param boolean $premium
+        * @param bool $premium
         *
         * @return Session
         */
-       public function setPremium(bool $premium): Session {
+       public function setPremium(?bool $premium): Session {
                $this->premium = $premium;
 
                return $this;
@@ -300,7 +276,7 @@ class Session {
         *
         * @return bool
         */
-       public function getPremium(): bool {
+       public function getPremium(): ?bool {
                return $this->premium;
        }
 
@@ -628,7 +604,7 @@ class Session {
         *
         * @return Session
         */
-       public function setApplication(Application $application): Session {
+       public function setApplication(?Application $application): Session {
                $this->application = $application;
 
                return $this;
@@ -648,7 +624,7 @@ class Session {
         */
        public function preUpdate(PreUpdateEventArgs $eventArgs) {
                //Check that we have a session instance
-               if (($session = $eventArgs->getEntity()) instanceof Session) {
+               if (($session = $eventArgs->getObject()) instanceof Session) {
                        //Set updated value
                        $session->setUpdated(new \DateTime('now'));
                }
@@ -660,6 +636,8 @@ class Session {
         * Consider as premium a day off for afternoon, the eve for evening and after
         * Store computed result in premium member for afternoon and evening
         *
+        * @TODO improve by moving day off computation in IntlUtil or HolidayUtil class ?
+        *
         * @return bool Whether the date is day off or not
         */
        public function isPremium(): bool {
@@ -730,7 +708,7 @@ class Session {
                }
 
                //Get eastern
-               $eastern = $this->getEastern($date->format('Y'));
+               $eastern = (new IntlUtil())->getEastern($date->format('Y'));
 
                //Check dynamic holidays
                if (
@@ -757,48 +735,4 @@ class Session {
                //Date is not a holiday and week day
                return false;
        }
-
-       /**
-        * Compute eastern for selected year
-        *
-        * @param string $year The eastern year
-        *
-        * @return DateTime The eastern date
-        */
-       private function getEastern(string $year): \DateTime {
-               //Set static
-               static $data = null;
-
-               //Check if already computed
-               if (isset($data[$year])) {
-                       //Return computed eastern
-                       return $data[$year];
-               //Check if data is null
-               } elseif (is_null($data)) {
-                       //Init data array
-                       $data = [];
-               }
-
-               $d = (19 * ($year % 19) + 24) % 30;
-
-               $e = (2 * ($year % 4) + 4 * ($year % 7) + 6 * $d + 5) % 7;
-
-               $day = 22 + $d + $e;
-
-               $month = 3;
-
-               if ($day > 31) {
-                       $day = $d + $e - 9;
-                       $month = 4;
-               } elseif ($d == 29 && $e == 6) {
-                       $day = 10;
-                       $month = 4;
-               } elseif ($d == 28 && $e == 6) {
-                       $day = 18;
-                       $month = 4;
-               }
-
-               //Store eastern in data
-               return ($data[$year] = new \DateTime(sprintf('%04d-%02d-%02d', $year, $month, $day)));
-       }
 }
index 116c29fcd6576e081fb21d377a8e9d7c808aa783..25ef3e8427e642693c058eba7b2cd75d1575f7cf 100644 (file)
@@ -1,16 +1,17 @@
 <?php declare(strict_types=1);
 
 /*
- * this file is part of the rapsys packbundle package.
+ * This file is part of the Rapsys AirBundle package.
  *
- * (c) raphaël gertz <symfony@rapsys.eu>
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
  *
- * for the full copyright and license information, please view the license
+ * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
  */
 
 namespace Rapsys\AirBundle\Entity;
 
+use Doctrine\Common\Collections\Collection;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\ORM\Event\PreUpdateEventArgs;
 
@@ -19,37 +20,34 @@ use Doctrine\ORM\Event\PreUpdateEventArgs;
  */
 class Slot {
        /**
-        * @var integer
+        * Primary key
         */
-       private $id;
+       private ?int $id = null;
 
        /**
-        * @var string
+        * Create datetime
         */
-       protected $title;
+       private \DateTime $created;
 
        /**
-        * @var \DateTime
+        * Update datetime
         */
-       private $created;
+       private \DateTime $updated;
 
        /**
-        * @var \DateTime
+        * Sessions collection
         */
-       private $updated;
-
-       /**
-        * @var ArrayCollection
-        */
-       private $sessions;
+       private Collection $sessions;
 
        /**
         * Constructor
         */
-       public function __construct() {
+       public function __construct(private string $title) {
                //Set defaults
                $this->created = new \DateTime('now');
                $this->updated = new \DateTime('now');
+
+               //Set collections
                $this->sessions = new ArrayCollection();
        }
 
@@ -58,7 +56,7 @@ class Slot {
         *
         * @return integer
         */
-       public function getId(): int {
+       public function getId(): ?int {
                return $this->id;
        }
 
@@ -164,7 +162,7 @@ class Slot {
         */
        public function preUpdate(PreUpdateEventArgs $eventArgs) {
                //Check that we have a slot instance
-               if (($slot = $eventArgs->getEntity()) instanceof Slot) {
+               if (($slot = $eventArgs->getObject()) instanceof Slot) {
                        //Set updated value
                        $slot->setUpdated(new \DateTime('now'));
                }
index ee5b262e78f600007529b2d12b5782463324ec05..dea8897dd0fe3e0b68416b46378d0edec0fb0c38 100644 (file)
@@ -1,11 +1,11 @@
 <?php declare(strict_types=1);
 
 /*
- * this file is part of the rapsys packbundle package.
+ * This file is part of the Rapsys AirBundle package.
  *
- * (c) raphaël gertz <symfony@rapsys.eu>
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
  *
- * for the full copyright and license information, please view the license
+ * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
  */
 
@@ -18,97 +18,76 @@ use Doctrine\ORM\Event\PreUpdateEventArgs;
  */
 class Snippet {
        /**
-        * @var integer
-        */
-       private $id;
-
-       /**
-        * @var string
+        * Primary key
         */
-       protected $locale;
+       private ?int $id = null;
 
        /**
         * @var string
         */
-       protected $description;
+       private ?string $description = null;
 
        /**
         * @var string
         */
-       protected $class;
+       private ?string $class = null;
 
        /**
         * @var string
         */
-       protected $short;
+       private ?string $short = null;
 
        /**
         * @var integer
         */
-       protected $rate;
+       private ?int $rate = null;
 
        /**
         * @var bool
         */
-       protected $hat;
+       private ?bool $hat = null;
 
        /**
         * @var string
         */
-       protected $contact;
+       private ?string $contact = null;
 
        /**
         * @var string
         */
-       protected $donate;
+       private ?string $donate = null;
 
        /**
         * @var string
         */
-       protected $link;
+       private ?string $link = null;
 
        /**
         * @var string
         */
-       protected $profile;
-
-       /**
-        * @var \DateTime
-        */
-       protected $created;
-
-       /**
-        * @var \DateTime
-        */
-       protected $updated;
+       private ?string $profile = null;
 
        /**
-        * @var Location
+        * Create datetime
         */
-       protected $location;
+       private \DateTime $created;
 
        /**
-        * @var User
+        * Update datetime
         */
-       protected $user;
+       private \DateTime $updated;
 
        /**
         * Constructor
+        *
+        * @param string $locale The locale
+        * @param Location $location The location instance
+        * @param User $user The user instance
         */
-       public function __construct() {
+       public function __construct(private string $locale, private Location $location, private User $user) {
                //Set defaults
-               $this->description = null;
-               $this->class = null;
-               $this->short = null;
-               $this->rate = null;
-               $this->hat = null;
-               $this->contact = null;
-               $this->donate = null;
-               $this->link = null;
-               $this->profile = null;
                $this->created = new \DateTime('now');
                $this->updated = new \DateTime('now');
-               $this->location = null;
        }
 
        /**
@@ -116,7 +95,7 @@ class Snippet {
         *
         * @return integer
         */
-       public function getId(): int {
+       public function getId(): ?int {
                return $this->id;
        }
 
@@ -390,7 +369,7 @@ class Snippet {
         *
         * @return Snippet
         */
-       public function setLocation(Location $location) {
+       public function setLocation(Location $location): Snippet {
                $this->location = $location;
 
                return $this;
@@ -401,7 +380,7 @@ class Snippet {
         *
         * @return Location
         */
-       public function getLocation() {
+       public function getLocation(): Location {
                return $this->location;
        }
 
@@ -412,7 +391,7 @@ class Snippet {
         *
         * @return Snippet
         */
-       public function setUser(User $user) {
+       public function setUser(User $user): Snippet {
                $this->user = $user;
 
                return $this;
@@ -423,16 +402,16 @@ class Snippet {
         *
         * @return User
         */
-       public function getUser() {
+       public function getUser(): User {
                return $this->user;
        }
 
        /**
         * {@inheritdoc}
         */
-       public function preUpdate(\Doctrine\ORM\Event\PreUpdateEventArgs  $eventArgs) {
+       public function preUpdate(PreUpdateEventArgs $eventArgs) {
                //Check that we have an snippet instance
-               if (($snippet = $eventArgs->getEntity()) instanceof Snippet) {
+               if (($snippet = $eventArgs->getObject()) instanceof Snippet) {
                        //Set updated value
                        $snippet->setUpdated(new \DateTime('now'));
                }
index 1fb9f1ae33588692a5345a441d3b1b22d5bf69d3..178bcec8a9799c96a517b1974ccbfe7803dec95e 100644 (file)
 <?php declare(strict_types=1);
 
 /*
- * this file is part of the rapsys packbundle package.
+ * This file is part of the Rapsys AirBundle package.
  *
- * (c) raphaël gertz <symfony@rapsys.eu>
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
  *
- * for the full copyright and license information, please view the license
+ * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
  */
 
 namespace Rapsys\AirBundle\Entity;
 
+use Doctrine\Common\Collections\Collection;
 use Doctrine\Common\Collections\ArrayCollection;
 
-use Rapsys\AirBundle\Entity\Application;
-use Rapsys\AirBundle\Entity\Group;
-use Rapsys\AirBundle\Entity\Link;
-use Rapsys\AirBundle\Entity\Snippet;
+use Rapsys\UserBundle\Entity\Civility;
 use Rapsys\UserBundle\Entity\User as BaseUser;
 
+/**
+ * {@inheritdoc}
+ */
 class User extends BaseUser {
        /**
-        * @var string
+        * City
+        */
+       private ?string $city = null;
+
+       /**
+        * Country
+        */
+       private ?Country $country = null;
+
+       /**
+        * Phone
+        */
+       private ?string $phone = null;
+
+       /**
+        * Pseudonym
         */
-       protected $pseudonym;
+       private ?string $pseudonym = null;
 
        /**
-        * @var string
+        * Zipcode
         */
-       protected $phone;
+       private ?string $zipcode = null;
 
        /**
-        * @var string
+        * Applications collection
         */
-       protected $slug;
+       private Collection $applications;
 
        /**
-        * @var ArrayCollection
+        * Dances collection
         */
-       private $applications;
+       private Collection $dances;
 
        /**
-        * @var ArrayCollection
+        * Locations collection
         */
-       private $dances;
+       private Collection $locations;
 
        /**
-        * @var ArrayCollection
+        * Snippets collection
         */
-       private $locations;
+       private Collection $snippets;
 
        /**
-        * @var ArrayCollection
+        * Subscribers collection
         */
-       private $snippets;
+       private Collection $subscribers;
 
        /**
-        * @var ArrayCollection
+        * Subscriptions collection
         */
-       private $subscribers;
+       private Collection $subscriptions;
 
        /**
-        * @var ArrayCollection
+        * Google tokens collection
         */
-       private $subscriptions;
+       private Collection $googleTokens;
 
        /**
         * Constructor
         *
         * @param string $mail The user mail
-        */
-       public function __construct(string $mail) {
+        * @param string $password The user password
+        * @param ?Civility $civility The user civility
+        * @param ?string $forename The user forename
+        * @param ?string $surname The user surname
+        * @param bool $active The user active
+        * @param bool $enable The user enable
+        */
+       public function __construct(protected string $mail, protected string $password, protected ?Civility $civility = null, protected ?string $forename = null, protected ?string $surname = null, protected bool $active = false, protected bool $enable = true) {
                //Call parent constructor
-               parent::__construct($mail);
-
-               //Set defaults
-               $this->pseudonym = null;
-               $this->phone = null;
-               $this->slug = null;
+               parent::__construct($this->mail, $this->password, $this->civility, $this->forename, $this->surname, $this->active, $this->enable);
 
                //Set collections
                $this->applications = new ArrayCollection();
@@ -86,28 +103,51 @@ class User extends BaseUser {
                $this->snippets = new ArrayCollection();
                $this->subscribers = new ArrayCollection();
                $this->subscriptions = new ArrayCollection();
+               $this->googleTokens = new ArrayCollection();
        }
 
        /**
-        * Set pseudonym
+        * Set city
         *
-        * @param string $pseudonym
+        * @param string $city
         *
         * @return User
         */
-       public function setPseudonym(?string $pseudonym): User {
-               $this->pseudonym = $pseudonym;
+       public function setCity(?string $city): User {
+               $this->city = $city;
 
                return $this;
        }
 
        /**
-        * Get pseudonym
+        * Get city
         *
         * @return string
         */
-       public function getPseudonym(): ?string {
-               return $this->pseudonym;
+       public function getCity(): ?string {
+               return $this->city;
+       }
+
+       /**
+        * Set country
+        *
+        * @param Country $country
+        *
+        * @return User
+        */
+       public function setCountry(?Country $country): User {
+               $this->country = $country;
+
+               return $this;
+       }
+
+       /**
+        * Get country
+        *
+        * @return Country
+        */
+       public function getCountry(): ?Country {
+               return $this->country;
        }
 
        /**
@@ -133,87 +173,78 @@ class User extends BaseUser {
        }
 
        /**
-        * Set slug
+        * Set pseudonym
         *
-        * @param string $slug
+        * @param string $pseudonym
         *
         * @return User
         */
-       public function setSlug(?string $slug): User {
-               $this->slug = $slug;
+       public function setPseudonym(?string $pseudonym): User {
+               $this->pseudonym = $pseudonym;
 
                return $this;
        }
 
        /**
-        * Get slug
+        * Get pseudonym
         *
         * @return string
         */
-       public function getSlug(): ?string {
-               return $this->slug;
+       public function getPseudonym(): ?string {
+               return $this->pseudonym;
        }
 
        /**
-        * Add application
+        * Set zipcode
         *
-        * @param Application $application
+        * @param string $zipcode
         *
         * @return User
         */
-       public function addApplication(Application $application): User {
-               $this->applications[] = $application;
+       public function setZipcode(?string $zipcode): User {
+               $this->zipcode = $zipcode;
 
                return $this;
        }
 
        /**
-        * Remove application
-        *
-        * @param Application $application
-        */
-       public function removeApplication(Application $application): bool {
-               return $this->applications->removeElement($application);
-       }
-
-       /**
-        * Get applications
+        * Get zipcode
         *
-        * @return ArrayCollection
+        * @return string
         */
-       public function getApplications(): ArrayCollection {
-               return $this->applications;
+       public function getZipcode(): ?string {
+               return $this->zipcode;
        }
 
        /**
-        * Add snippet
+        * Add application
         *
-        * @param Snippet $snippet
+        * @param Application $application
         *
         * @return User
         */
-       public function addSnippet(Snippet $snippet): User {
-               $this->snippets[] = $snippet;
+       public function addApplication(Application $application): User {
+               $this->applications[] = $application;
 
                return $this;
        }
 
        /**
-        * Remove snippet
+        * Remove application
         *
-        * @param Snippet $snippet
+        * @param Application $application
         */
-       public function removeSnippet(Snippet $snippet): bool {
-               return $this->snippets->removeElement($snippet);
+       public function removeApplication(Application $application): bool {
+               return $this->applications->removeElement($application);
        }
 
        /**
-        * Get snippets
+        * Get applications
         *
-        * @return ArrayCollection
+        * @return \Doctrine\Common\Collections\Collection
         */
-       public function getSnippets(): ArrayCollection {
-               return $this->snippets;
+       public function getApplications(): Collection {
+               return $this->applications;
        }
 
        /**
@@ -243,9 +274,9 @@ class User extends BaseUser {
        /**
         * Get dances
         *
-        * @return ArrayCollection
+        * @return \Doctrine\Common\Collections\Collection
         */
-       public function getDances(): ArrayCollection {
+       public function getDances(): Collection {
                return $this->dances;
        }
 
@@ -274,12 +305,43 @@ class User extends BaseUser {
        /**
         * Get locations
         *
-        * @return ArrayCollection
+        * @return \Doctrine\Common\Collections\Collection
         */
-       public function getLocations(): ArrayCollection {
+       public function getLocations(): Collection {
                return $this->locations;
        }
 
+       /**
+        * Add snippet
+        *
+        * @param Snippet $snippet
+        *
+        * @return User
+        */
+       public function addSnippet(Snippet $snippet): User {
+               $this->snippets[] = $snippet;
+
+               return $this;
+       }
+
+       /**
+        * Remove snippet
+        *
+        * @param Snippet $snippet
+        */
+       public function removeSnippet(Snippet $snippet): bool {
+               return $this->snippets->removeElement($snippet);
+       }
+
+       /**
+        * Get snippets
+        *
+        * @return \Doctrine\Common\Collections\Collection
+        */
+       public function getSnippets(): Collection {
+               return $this->snippets;
+       }
+
        /**
         * Add subscriber
         *
@@ -288,6 +350,9 @@ class User extends BaseUser {
         * @return User
         */
        public function addSubscriber(User $subscriber): User {
+               //Add from owning side
+               $subscriber->addSubscription($this);
+
                $this->subscribers[] = $subscriber;
 
                return $this;
@@ -299,15 +364,22 @@ class User extends BaseUser {
         * @param User $subscriber
         */
        public function removeSubscriber(User $subscriber): bool {
+               if (!$this->subscriptions->contains($subscriber)) {
+                       return true;
+               }
+
+               //Remove from owning side
+               $subscriber->removeSubscription($this);
+
                return $this->subscribers->removeElement($subscriber);
        }
 
        /**
         * Get subscribers
         *
-        * @return ArrayCollection
+        * @return \Doctrine\Common\Collections\Collection
         */
-       public function getSubscribers(): ArrayCollection {
+       public function getSubscribers(): Collection {
                return $this->subscribers;
        }
 
@@ -336,9 +408,40 @@ class User extends BaseUser {
        /**
         * Get subscriptions
         *
-        * @return ArrayCollection
+        * @return \Doctrine\Common\Collections\Collection
         */
-       public function getSubscriptions(): ArrayCollection {
+       public function getSubscriptions(): Collection {
                return $this->subscriptions;
        }
+
+       /**
+        * Add google token
+        *
+        * @param GoogleToken $googleToken
+        *
+        * @return User
+        */
+       public function addGoogleToken(GoogleToken $googleToken): User {
+               $this->googleTokens[] = $googleToken;
+
+               return $this;
+       }
+
+       /**
+        * Remove google token
+        *
+        * @param GoogleToken $googleToken
+        */
+       public function removeGoogleToken(GoogleToken $googleToken): bool {
+               return $this->googleTokens->removeElement($googleToken);
+       }
+
+       /**
+        * Get googleTokens
+        *
+        * @return \Doctrine\Common\Collections\Collection
+        */
+       public function getGoogleTokens(): Collection {
+               return $this->googleTokens;
+       }
 }
diff --git a/EventSubscriber/FacebookSubscriber.php b/EventSubscriber/FacebookSubscriber.php
deleted file mode 100644 (file)
index d950a54..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-<?php
-
-namespace Rapsys\AirBundle\EventSubscriber;
-
-use Symfony\Component\EventDispatcher\EventSubscriberInterface;
-use Symfony\Component\HttpKernel\Event\ControllerEvent;
-use Symfony\Component\HttpKernel\Event\RequestEvent;
-use Symfony\Component\HttpKernel\Event\ResponseEvent;
-use Symfony\Component\HttpKernel\KernelEvents;
-use Symfony\Component\Routing\RouterInterface;
-
-class FacebookSubscriber implements EventSubscriberInterface {
-       ///Supported locales array
-       private $locales;
-
-       ///Router instance
-       private $router;
-
-       /*
-        * Inject router interface and locales
-        *
-        * @param RouterInterface $router The router instance
-        * @param array $locales The supported locales
-        */
-       public function __construct(RouterInterface $router, array $locales) {
-               //Set locales
-               $this->locales = $locales;
-
-               //Set router
-               $this->router = $router;
-       }
-
-       /**
-        * Change locale for request with ?fb_locale=xx
-        *
-        * @param RequestEvent The request event
-        */
-       public function onKernelRequest(RequestEvent $event) {
-               //Retrieve request
-               $request = $event->getRequest();
-
-               //Check for facebook locale
-               if (
-                       $request->query->has('fb_locale') &&
-                       in_array($preferred = $request->query->get('fb_locale'), $this->locales)
-               ) {
-                       //Set locale
-                       $request->setLocale($preferred);
-
-                       //Set default locale
-                       $request->setDefaultLocale($preferred);
-
-                       //Get router context
-                       $context = $this->router->getContext();
-
-                       //Set context locale
-                       $context->setParameter('_locale', $preferred);
-
-                       //Set back router context
-                       $this->router->setContext($context);
-               }
-       }
-
-       /**
-        * Get subscribed events
-        *
-        * @return array The subscribed events
-        */
-       public static function getSubscribedEvents() {
-               return [
-                       // must be registered before the default locale listener
-                       KernelEvents::REQUEST => [['onKernelRequest', 10]]
-               ];
-       }
-}
diff --git a/Factory.php b/Factory.php
new file mode 100644 (file)
index 0000000..a421165
--- /dev/null
@@ -0,0 +1,83 @@
+<?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;
+
+use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\Repository\RepositoryFactory;
+use Doctrine\Persistence\ObjectRepository;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\Routing\RouterInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
+
+use Rapsys\PackBundle\Util\SluggerUtil;
+
+/**
+ * This factory is used to create default repository objects for entities at runtime.
+ */
+final class Factory implements RepositoryFactory {
+       /**
+        * The list of EntityRepository instances
+        */
+       private array $repositoryList = [];
+
+       /**
+        * Initializes a new RepositoryFactory instance
+        *
+        * @param RequestStack $request The request stack
+        * @param RouterInterface $router The router instance
+        * @param SluggerUtil $slugger The SluggerUtil instance
+        * @param TranslatorInterface $translator The TranslatorInterface instance
+        * @param string $locale The current locale
+        * @param array $languages The languages list
+        */
+       public function __construct(private RequestStack $request, private RouterInterface $router, private SluggerUtil $slugger, private TranslatorInterface $translator, private string $locale, private array $languages) {
+       }
+
+       /**
+        * {@inheritdoc}
+        */
+       public function getRepository(EntityManagerInterface $entityManager, mixed $entityName): ObjectRepository {
+               //Set repository hash
+               $repositoryHash = $entityManager->getClassMetadata($entityName)->getName() . spl_object_hash($entityManager);
+
+               //With entity repository instance
+               if (isset($this->repositoryList[$repositoryHash])) {
+                       //Return existing entity repository instance
+                       return $this->repositoryList[$repositoryHash];
+               }
+
+               //Store and return created entity repository instance
+               return $this->repositoryList[$repositoryHash] = $this->createRepository($entityManager, $entityName);
+       }
+
+       /**
+        * Create a new repository instance for an entity class
+        *
+        * @param EntityManagerInterface $entityManager The EntityManager instance.
+        * @param string $entityName The name of the entity.
+        */
+       private function createRepository(EntityManagerInterface $entityManager, string $entityName): ObjectRepository {
+               //Get class metadata
+               $metadata = $entityManager->getClassMetadata($entityName);
+
+               //Get repository class
+               $repositoryClass = $metadata->customRepositoryClassName ?: $entityManager->getConfiguration()->getDefaultRepositoryClassName();
+
+               //Set to current locale
+               //XXX: current request is not yet populated in constructor
+               $this->locale = $this->request->getCurrentRequest()?->getLocale() ?? $this->locale;
+
+               //Return repository class instance
+               //XXX: router, slugger, translator, languages and locale arguments will be ignored by default
+               return new $repositoryClass($entityManager, $metadata, $this->router, $this->slugger, $this->translator, $this->locale, $this->languages);
+       }
+}
index 9d8dc83891a148cd1df1dce8c0acf80b3cd7c1c3..f52ad8c46709c1ed2b0addababe2089240ac3146 100644 (file)
@@ -18,7 +18,7 @@ use Symfony\Bridge\Doctrine\Form\Type\EntityType;
 use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
 use Symfony\Component\Form\Extension\Core\Type\DateType;
 use Symfony\Component\Form\Extension\Core\Type\SubmitType;
-use Symfony\Component\Validator\Constraints\Date;
+use Symfony\Component\Validator\Constraints\Type;
 use Symfony\Component\Validator\Constraints\NotBlank;
 
 use Rapsys\AirBundle\Entity\Dance;
@@ -32,39 +32,33 @@ class ApplicationType extends AbstractType {
        /**
         * {@inheritdoc}
         */
-       public function buildForm(FormBuilderInterface $builder, array $options) {
-               //Create form
-               $form = $builder;
-
+       public function buildForm(FormBuilderInterface $builder, array $options): void {
                //Add dance field
-               $form->add('dance', EntityType::class, ['class' => 'RapsysAirBundle:Dance', 'choices' => $options['dance_choices'], 'preferred_choices' => $options['dance_favorites'], 'attr' => ['placeholder' => 'Your dance'], 'choice_translation_domain' => true, 'constraints' => [new NotBlank(['message' => 'Please provide your dance'])], 'empty_data' => $options['dance_default']]);
+               $builder->add('dance', EntityType::class, ['class' => 'Rapsys\AirBundle\Entity\Dance', 'choices' => $options['dance_choices'], 'preferred_choices' => $options['dance_favorites'], 'attr' => ['placeholder' => 'Your dance'], 'choice_translation_domain' => true, 'constraints' => [new NotBlank(['message' => 'Please provide your dance'])], 'data' => $options['dance_default']]);
 
                //Add date field
-               $form->add('date', DateType::class, ['attr' => ['placeholder' => 'Your date', 'class' => 'date'], 'html5' => true, 'input' => 'datetime', 'widget' => 'single_text', 'format' => 'yyyy-MM-dd', 'data' => new \DateTime('+7 day'), 'constraints' => [new NotBlank(['message' => 'Please provide your date']), new Date(['message' => 'Your date doesn\'t seems to be valid'])]]);
+               $builder->add('date', DateType::class, ['attr' => ['placeholder' => 'Your date', 'class' => 'date'], 'html5' => true, 'input' => 'datetime', 'widget' => 'single_text', 'format' => 'yyyy-MM-dd', 'data' => new \DateTime('+7 day'), 'constraints' => [new NotBlank(['message' => 'Please provide your date']), new Type(['type' => \DateTime::class, 'message' => 'Your date doesn\'t seems to be valid'])]]);
 
                //Add location field
-               $form->add('location', EntityType::class, ['class' => 'RapsysAirBundle:Location', 'choices' => $options['location_choices'], 'preferred_choices' => $options['location_favorites'], 'attr' => ['placeholder' => 'Your location'], 'choice_translation_domain' => true, 'constraints' => [new NotBlank(['message' => 'Please provide your location'])], 'empty_data' => $options['location_default']]);
+               $builder->add('location', EntityType::class, ['class' => 'Rapsys\AirBundle\Entity\Location', 'choices' => $options['location_choices'], 'preferred_choices' => $options['location_favorites'], 'attr' => ['placeholder' => 'Your location'], 'choice_translation_domain' => true, 'constraints' => [new NotBlank(['message' => 'Please provide your location'])], 'data' => $options['location_default']]);
 
                //Add slot field
-               $form->add('slot', EntityType::class, ['class' => 'RapsysAirBundle:Slot', 'attr' => ['placeholder' => 'Your slot'], 'constraints' => [new NotBlank(['message' => 'Please provide your slot'])], 'choice_translation_domain' => true, 'data' => $options['slot_default']]);
+               $builder->add('slot', EntityType::class, ['class' => 'Rapsys\AirBundle\Entity\Slot', 'attr' => ['placeholder' => 'Your slot'], 'constraints' => [new NotBlank(['message' => 'Please provide your slot'])], 'choice_translation_domain' => true, 'data' => $options['slot_default']]);
 
                //Add extra user field
                if (!empty($options['user'])) {
                        //XXX: choicetype used here to use our own custom translated string
-                       $form->add('user', ChoiceType::class, ['attr' => ['placeholder' => 'Your user'], 'choice_translation_domain' => false, 'constraints' => [new NotBlank(['message' => 'Please provide your user'])], 'choices' => $options['user_choices'], 'empty_data' => $options['user_default']]);
+                       $builder->add('user', ChoiceType::class, ['attr' => ['placeholder' => 'Your user'], 'choice_translation_domain' => false, 'constraints' => [new NotBlank(['message' => 'Please provide your user'])], 'choices' => $options['user_choices'], 'data' => $options['user_default']]);
                }
 
                //Add submit
-               $form->add('submit', SubmitType::class, ['label' => 'Send', 'attr' => ['class' => 'submit']]);
-
-               //Return form
-               return $form;
+               $builder->add('submit', SubmitType::class, ['label' => 'Send', 'attr' => ['class' => 'submit']]);
        }
 
        /**
         * {@inheritdoc}
         */
-       public function configureOptions(OptionsResolver $resolver) {
+       public function configureOptions(OptionsResolver $resolver): void {
                //Set defaults
                $resolver->setDefaults(['error_bubbling' => true, 'dance_choices' => [], 'dance_default' => null, 'dance_favorites' => [], 'location_choices' => [], 'location_default' => null, 'location_favorites' => [], 'slot_default' => null, 'user' => true, 'user_choices' => [], 'user_default' => 1]);
 
@@ -102,7 +96,7 @@ class ApplicationType extends AbstractType {
        /**
         * {@inheritdoc}
         */
-       public function getName() {
-               return 'rapsys_air_application';
+       public function getName(): string {
+               return 'rapsysair_application';
        }
 }
index be5fcab53198b1bbcef6b04eef43090939c22d1f..8b15b010f21dbe015233cddc82a67acfc4bb6241 100644 (file)
@@ -1,41 +1,55 @@
-<?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\Form;
 
 use Symfony\Component\Form\AbstractType;
 use Symfony\Component\Form\FormBuilderInterface;
 use Symfony\Component\OptionsResolver\OptionsResolver;
-use Symfony\Component\Form\Extension\Core\Type\TextType;
+use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
 use Symfony\Component\Form\Extension\Core\Type\SubmitType;
-use Symfony\Component\Validator\Constraints\NotBlank;
 
 class CalendarType extends AbstractType {
        /**
         * {@inheritdoc}
         */
-       public function buildForm(FormBuilderInterface $builder, array $options) {
-               return $builder
-                       ->add('calendar', TextType::class, ['label' => 'Calendar id', 'attr' => ['placeholder' => 'Your calendar id'], 'constraints' => [new NotBlank(['message' => 'Please provide your calendar id'])]])
-                       //TODO: validate prefix against [a-v0-9]{5,}
-                       //XXX: see https://developers.google.com/calendar/api/v3/reference/events/insert#id
-                       ->add('prefix', TextType::class, ['label' => 'Prefix', 'attr' => ['placeholder' => 'Your prefix'], 'constraints' => [new NotBlank(['message' => 'Please provide your prefix'])]])
-                       ->add('project', TextType::class, ['label' => 'Project id', 'attr' => ['placeholder' => 'Your project id'], 'required' => false])
-                       ->add('client', TextType::class, ['label' => 'Client id', 'attr' => ['placeholder' => 'Your client id'], 'required' => false])
-                       ->add('secret', TextType::class, ['label' => 'Client secret', 'attr' => ['placeholder' => 'Your client secret'], 'required' => false])
-                       ->add('submit', SubmitType::class, ['label' => 'Send', 'attr' => ['class' => 'submit']]);
+       public function buildForm(FormBuilderInterface $builder, array $options): void {
+               //Build form
+               $builder
+                       ->add('calendar', ChoiceType::class, ['attr' => ['placeholder' => 'Your calendar'], 'choice_translation_domain' => false, 'expanded' => true, 'multiple' => true, 'choices' => $options['calendar_choices']/*, 'data' => $options['calendar_default']*/, 'choice_attr' => ['class' => 'row']])
+                       ->add('submit', SubmitType::class, ['label' => 'Send', 'attr' => ['class' => 'submit']])
+                       ->add('refresh', SubmitType::class, ['label' => 'Refresh', 'attr' => ['class' => 'submit']])
+                       ->add('add', SubmitType::class, ['label' => 'Add', 'attr' => ['class' => 'submit']])
+                       ->add('delete', SubmitType::class, ['label' => 'Delete', 'attr' => ['class' => 'submit']])
+                       ->add('unlink', SubmitType::class, ['label' => 'Unlink', 'attr' => ['class' => 'submit']]);
        }
 
        /**
         * {@inheritdoc}
         */
-       public function configureOptions(OptionsResolver $resolver) {
-               $resolver->setDefaults(['error_bubbling' => true]);
+       public function configureOptions(OptionsResolver $resolver): void {
+               //Set defaults
+               $resolver->setDefaults(['error_bubbling' => true, 'calendar_choices' => [], /*'calendar_default' => ['primary']*/]);
+
+               //Add calendar choices
+               $resolver->setAllowedTypes('calendar_choices', 'array');
+
+               //Add calendar default
+               #$resolver->setAllowedTypes('calendar_default', 'integer');
        }
 
        /**
         * {@inheritdoc}
         */
-       public function getName() {
+       public function getName(): string {
                return 'calendar_form';
        }
 }
index 24d4d3ec7822b13bd416cf9e799539a72fc470c3..4142c996d4c0bc3dcdcf8a3d13b8c7ffba8f8a7a 100644 (file)
@@ -1,8 +1,18 @@
-<?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\Form;
 
-use Symfony\Component\Form\AbstractType;
+use Rapsys\PackBundle\Form\CaptchaType;
+
 use Symfony\Component\Form\FormBuilderInterface;
 use Symfony\Component\OptionsResolver\OptionsResolver;
 use Symfony\Component\Form\Extension\Core\Type\TextType;
@@ -12,30 +22,41 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType;
 use Symfony\Component\Validator\Constraints\Email;
 use Symfony\Component\Validator\Constraints\NotBlank;
 
-class ContactType extends AbstractType {
+/**
+ * {@inheritdoc}
+ */
+class ContactType extends CaptchaType {
        /**
         * {@inheritdoc}
         */
-       public function buildForm(FormBuilderInterface $builder, array $options) {
-               return $builder
+       public function buildForm(FormBuilderInterface $builder, array $options): void {
+               //Add fields
+               $builder
                        ->add('name', TextType::class, ['attr' => ['placeholder' => 'Your name'], 'constraints' => [new NotBlank(['message' => 'Please provide your name'])]])
                        ->add('subject', TextType::class, ['attr' => ['placeholder' => 'Subject'], 'constraints' => [new NotBlank(['message' => 'Please provide your subject'])]])
                        ->add('mail', EmailType::class, ['attr' => ['placeholder' => 'Your mail'], 'constraints' => [new NotBlank(['message' => 'Please provide a valid mail']), new Email(['message' => 'Your mail doesn\'t seems to be valid'])]])
                        ->add('message', TextareaType::class, ['attr' => ['placeholder' => 'Your message', 'cols' => 50, 'rows' => 15], 'constraints' => [new NotBlank(['message' => 'Please provide your message'])]])
                        ->add('submit', SubmitType::class, ['label' => 'Send', 'attr' => ['class' => 'submit']]);
+
+               //Call parent
+               parent::buildForm($builder, $options);
        }
 
        /**
         * {@inheritdoc}
         */
-       public function configureOptions(OptionsResolver $resolver) {
+       public function configureOptions(OptionsResolver $resolver): void {
+               //Call parent configure options
+               parent::configureOptions($resolver);
+
+               //Set defaults
                $resolver->setDefaults(['error_bubbling' => true]);
        }
 
        /**
         * {@inheritdoc}
         */
-       public function getName() {
+       public function getName(): string {
                return 'contact_form';
        }
 }
index 7521633c25b4ddb993b13436097eab101372a8db..f9b27d1447bd06c0e0b6b4a74c33a8ae42b2e9f0 100644 (file)
@@ -40,7 +40,7 @@ class HiddenEntityType extends HiddenType implements DataTransformerInterface {
         *
         * {@inheritdoc}
         */
-       public function configureOptions(OptionsResolver $resolver) {
+       public function configureOptions(OptionsResolver $resolver): void {
                //Call parent
                parent::configureOptions($resolver);
 
@@ -102,24 +102,24 @@ class HiddenEntityType extends HiddenType implements DataTransformerInterface {
        /**
         * Reverse transformation from string to data object
         *
-        * @param string $data The object id
+        * @param mixed $value The object id
         * @return mixed The data object
         */
-       public function reverseTransform($data) {
-               if (!$data) {
+       public function reverseTransform(mixed $value): mixed {
+               if (!$value) {
                        return null;
                }
 
                $res = null;
                try {
                        $rep = $this->dm->getRepository($this->class);
-                       $res = $rep->findOneById($data);
+                       $res = $rep->findOneById($value);
                } catch (\Exception $e) {
                        throw new TransformationFailedException($e->getMessage());
                }
 
                if ($res === null) {
-                       throw new TransformationFailedException(sprintf('A %s with id %s does not exist!', $this->class, $data));
+                       throw new TransformationFailedException(sprintf('A %s with id %s does not exist!', $this->class, $value));
                }
 
                return $res;
diff --git a/Form/ImageType.php b/Form/ImageType.php
new file mode 100644 (file)
index 0000000..300ffe9
--- /dev/null
@@ -0,0 +1,86 @@
+<?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\Form;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\Extension\Core\Type\HiddenType;
+use Symfony\Component\Form\Extension\Core\Type\FileType;
+use Symfony\Component\Form\Extension\Core\Type\SubmitType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+use Symfony\Component\Validator\Constraints\File;
+
+/**
+ * {@inheritdoc}
+ */
+class ImageType extends AbstractType {
+       /**
+        * {@inheritdoc}
+        */
+       public function buildForm(FormBuilderInterface $form, array $options): FormBuilderInterface {
+               //With location
+               if ($options['location']) {
+                       $form->add('location', HiddenType::class, ['required' => true]);
+               }
+
+               //With user
+               if ($options['user']) {
+                       $form->add('user', HiddenType::class, ['required' => true]);
+               }
+
+               //With image
+               if ($options['image']) {
+                       //With image
+                       $form->add('image', FileType::class, ['attr' => ['placeholder' => 'Your image'], 'constraints' => [new File(['maxSize' => '5M', 'mimeTypes' => ['image/jpeg', 'image/png', 'image/tiff', 'image/webp'], 'mimeTypesMessage' => 'Please upload a valid Image document'])]/*, 'mapped' => false*/, 'required' => $options['delete'] ? false : true]);
+               }
+
+               //Add submit
+               $form->add('submit', SubmitType::class, ['label' => 'Send', 'attr' => ['class' => 'submit']]);
+
+               //With delete
+               if ($options['delete']) {
+                       //Add delete
+                       //TODO: add confirm on click ?
+                       $form->add('delete', SubmitType::class, ['label' => 'Delete', 'attr' => ['class' => 'submit']]);
+               }
+
+               //Return form builder
+               return $form;
+       }
+
+       /**
+        * {@inheritdoc}
+        */
+       public function configureOptions(OptionsResolver $resolver): void {
+               //Set defaults
+               $resolver->setDefaults(['delete' => true, 'error_bubbling' => true, 'image' => true, 'location' => true, 'user' => true]);
+
+               //Add extra delete option
+               $resolver->setAllowedTypes('delete', 'boolean');
+
+               //Add extra image option
+               $resolver->setAllowedTypes('image', 'boolean');
+
+               //Add extra location option
+               $resolver->setAllowedTypes('location', 'boolean');
+
+               //Add extra user option
+               $resolver->setAllowedTypes('user', 'boolean');
+       }
+
+       /**
+        * {@inheritdoc}
+        */
+       public function getName(): string {
+               return 'rapsysair_image';
+       }
+}
index 53784a996abbc65834f1109b6c729ca9f014ce66..0072d8bc6a254ffb53cebf3a4fd5570e300f748b 100644 (file)
@@ -1,55 +1,54 @@
-<?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\Form;
 
 use Symfony\Component\Form\AbstractType;
-use Symfony\Component\Form\FormView;
-use Symfony\Component\Form\FormInterface;
-use Symfony\Component\Form\FormBuilderInterface;
-use Symfony\Component\OptionsResolver\OptionsResolver;
-use Symfony\Component\Form\Extension\Core\Type\TextType;
-use Symfony\Component\Form\Extension\Core\Type\NumberType;
 use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
+use Symfony\Component\Form\Extension\Core\Type\NumberType;
 use Symfony\Component\Form\Extension\Core\Type\SubmitType;
+use Symfony\Component\Form\Extension\Core\Type\TextareaType;
+use Symfony\Component\Form\Extension\Core\Type\TextType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
 use Symfony\Component\Validator\Constraints\NotBlank;
+
 use Rapsys\AirBundle\Entity\Location;
 
+/**
+ * {@inheritdoc}
+ */
 class LocationType extends AbstractType {
        /**
         * {@inheritdoc}
         */
-       public function buildForm(FormBuilderInterface $builder, array $options) {
-               return $builder
-                       ->setAttribute('label_prefix', $options['label_prefix'])
-                       ->add('title', TextType::class, ['attr' => ['placeholder' => 'Your title']])
-                       ->add('short', TextType::class, ['attr' => ['placeholder' => 'Your short']])
-                       ->add('address', TextType::class, ['attr' => ['placeholder' => 'Your address']])
-                       ->add('zipcode', NumberType::class, ['attr' => ['placeholder' => 'Your zipcode'], 'html5' => true])
-                       ->add('city', TextType::class, ['attr' => ['placeholder' => 'Your city']])
-                       ->add('latitude', NumberType::class, ['attr' => ['placeholder' => 'Your latitude', 'step' => 0.000001], 'html5' => true, 'scale' => 6])
-                       ->add('longitude', NumberType::class, ['attr' => ['placeholder' => 'Your longitude', 'step' => 0.000001], 'html5' => true, 'scale' => 6])
+       public function buildForm(FormBuilderInterface $builder, array $options): void {
+               //Build form
+               $builder
+                       ->add('title', TextType::class, ['attr' => ['placeholder' => 'Your title'], 'constraints' => [new NotBlank(['message' => 'Please provide your title'])]])
+                       ->add('description', TextareaType::class, ['attr' => ['placeholder' => 'Your description', 'cols' => 50, 'rows' => 15], 'required' => false])
+                       ->add('address', TextType::class, ['attr' => ['placeholder' => 'Your address'], 'constraints' => [new NotBlank(['message' => 'Please provide your address'])]])
+                       ->add('zipcode', NumberType::class, ['attr' => ['placeholder' => 'Your zipcode'], 'html5' => true, 'constraints' => [new NotBlank(['message' => 'Please provide your zipcode'])]])
+                       ->add('city', TextType::class, ['attr' => ['placeholder' => 'Your city'], 'constraints' => [new NotBlank(['message' => 'Please provide your city'])]])
+                       ->add('latitude', NumberType::class, ['attr' => ['placeholder' => 'Your latitude', 'step' => 0.000001], 'html5' => true, 'scale' => 6, 'constraints' => [new NotBlank(['message' => 'Please provide your latitude'])]])
+                       ->add('longitude', NumberType::class, ['attr' => ['placeholder' => 'Your longitude', 'step' => 0.000001], 'html5' => true, 'scale' => 6, 'constraints' => [new NotBlank(['message' => 'Please provide your longitude'])]])
+                       ->add('indoor', CheckboxType::class, ['attr' => ['placeholder' => 'Your indoor'], 'required' => false])
                        ->add('hotspot', CheckboxType::class, ['attr' => ['placeholder' => 'Your hotspot'], 'required' => false])
                        ->add('submit', SubmitType::class, ['label' => 'Send', 'attr' => ['class' => 'submit']]);
        }
 
-       /**
-        * {@inheritdoc}
-        *
-       public function buildView(FormView $view, FormInterface $form, array $options) {
-               #$labelPrefix = $form->getRoot()->hasAttribute('label_prefix') ? $form->getRoot()->getAttribute('label_prefix') : '';
-               #$labelPrefix = $form->getConfig()->hasAttribute('label_prefix');
-               #$labelPrefix = $form->getConfig()->getAttribute('label_prefix');
-               #var_dump($view);
-               var_dump($view['label']);
-               exit;
-               //Prefix label to prevent collision
-               $view['label'] = $form->getConfig()->getAttribute('label_prefix').$view->getVar('label');
-       }*/
-
        /**
         * {@inheritdoc}
         */
-       public function configureOptions(OptionsResolver $resolver) {
-               $resolver->setDefaults(['data_class' => Location::class, 'error_bubbling' => true, 'label_prefix' => '']);
+       public function configureOptions(OptionsResolver $resolver): void {
+               $resolver->setDefaults(['data_class' => Location::class, 'error_bubbling' => true]);
        }
 }
index 0e0395f2d8e3a94855c9aaecd953529113b7c0ea..8082fc5fc5c1d44254131b8f0d28a11802b06a0e 100644 (file)
 
 namespace Rapsys\AirBundle\Form;
 
-use Symfony\Component\Form\Extension\Core\Type\TextType;
+use Doctrine\ORM\EntityManagerInterface;
+
+use Rapsys\AirBundle\Entity\Country;
+use Rapsys\AirBundle\Entity\Dance;
+use Rapsys\AirBundle\Entity\User;
+use Rapsys\AirBundle\Transformer\DanceTransformer;
+use Rapsys\AirBundle\Transformer\SubscriptionTransformer;
+
+use Rapsys\PackBundle\Util\ImageUtil;
+use Rapsys\PackBundle\Util\SluggerUtil;
+
+use Rapsys\UserBundle\Form\RegisterType as BaseRegisterType;
+
+use Symfony\Bridge\Doctrine\Form\Type\EntityType;
+use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
 use Symfony\Component\Form\Extension\Core\Type\TelType;
+use Symfony\Component\Form\Extension\Core\Type\TextType;
 use Symfony\Component\Form\FormBuilderInterface;
 use Symfony\Component\OptionsResolver\OptionsResolver;
+use Symfony\Contracts\Translation\TranslatorInterface;
+
+/**
+ * {@inheritdoc}
+ */
+class RegisterType extends BaseRegisterType {
+       /**
+        * Constructor
+        *
+        * @param EntityManagerInterface $manager The entity manager
+        * @param ?ImageUtil $image The image instance
+        * @param ?SluggerUtil $slugger The slugger instance
+        * @param ?TranslatorInterface $translator The translator instance
+        */
+       public function __construct(protected EntityManagerInterface $manager, protected ?ImageUtil $image = null, protected ?SluggerUtil $slugger = null, protected ?TranslatorInterface $translator = null) {
+               //Call parent constructor
+               parent::__construct($this->image, $this->slugger, $this->translator);
+       }
 
-class RegisterType extends \Rapsys\UserBundle\Form\RegisterType {
        /**
         * {@inheritdoc}
         */
-       public function buildForm(FormBuilderInterface $builder, array $options): FormBuilderInterface {
-               //Call parent build form
-               $form = parent::buildForm($builder, $options);
+       public function buildForm(FormBuilderInterface $builder, array $options): void {
+               //Add extra city field
+               if (!empty($options['city'])) {
+                       $builder->add('city', TextType::class, ['attr' => ['placeholder' => 'Your city'], 'required' => false]);
+               }
+
+               //Add extra country field
+               if (!empty($options['country'])) {
+                       //Add country field
+                       $builder->add('country', EntityType::class, ['class' => $options['country_class'], 'choice_label' => 'title'/*, 'choices' => $options['location_choices']*/, 'preferred_choices' => $options['country_favorites'], 'attr' => ['placeholder' => 'Your country'], 'choice_translation_domain' => false, 'required' => true, 'data' => $options['country_default']]);
+               }
+
+               //Add extra dance field
+               if (!empty($options['dance'])) {
+                       //Add dance field
+                       #$builder->add('dances', EntityType::class, ['class' => $options['dance_class'], 'choice_label' => null, 'preferred_choices' => $options['dance_favorites'], 'attr' => ['placeholder' => 'Your dance'], 'choice_translation_domain' => false, 'required' => false, 'data' => $options['dance_default']]);
+                       $builder->add(
+                               $builder
+                                       ->create('dances', ChoiceType::class, ['attr' => ['placeholder' => 'Your dance']/*, 'by_reference' => false*/, 'choice_attr' => ['class' => 'row'], 'choice_translation_domain' => false, 'choices' => $options['dance_choices'], 'multiple' => true, 'preferred_choices' => $options['dance_favorites'], 'required' => false])
+                                       ->addModelTransformer(new DanceTransformer($this->manager))
+                                       #->addModelTransformer(new CollectionToArrayTransformer)
+                       );
+                       /*, 'expanded' => true*/ /*, 'data' => $options['dance_default']*/
+               }
 
                //Add extra phone field
                if (!empty($options['phone'])) {
-                       $form->add('phone', TelType::class, ['attr' => ['placeholder' => 'Your phone'], 'required' => false]);
+                       $builder->add('phone', TelType::class, ['attr' => ['placeholder' => 'Your phone'], 'required' => false]);
                }
 
                //Add extra pseudonym field
                if (!empty($options['pseudonym'])) {
-                       $form->add('pseudonym', TextType::class, ['attr' => ['placeholder' => 'Your pseudonym'], 'required' => false]);
+                       $builder->add('pseudonym', TextType::class, ['attr' => ['placeholder' => 'Your pseudonym'], 'required' => false]);
                }
 
-               //Add extra slug field
-               if (!empty($options['slug'])) {
-                       $form->add('slug', TextType::class, ['attr' => ['placeholder' => 'Your slug'], 'required' => false]);
+               //Add extra subscription field
+               if (!empty($options['subscription'])) {
+                       //Add subscription field
+                       #$builder->add('subscriptions', EntityType::class, ['class' => $options['subscription_class'], 'choice_label' => 'pseudonym', 'preferred_choices' => $options['subscription_favorites'], 'attr' => ['placeholder' => 'Your subscription'], 'choice_translation_domain' => false, 'required' => false, 'data' => $options['subscription_default']]);
+                       #$builder->add('subscriptions', ChoiceType::class, ['attr' => ['placeholder' => 'Your subscription'], 'choice_attr' => ['class' => 'row'], 'choice_translation_domain' => false, 'choices' => $options['subscription_choices'], 'multiple' => true, 'preferred_choices' => $options['subscription_favorites'], 'required' => false]);
+                       $builder->add(
+                               $builder
+                                       //XXX: by_reference need to be false to allow persisting of data from the read only inverse side
+                                       ->create('subscriptions', ChoiceType::class, ['attr' => ['placeholder' => 'Your subscription']/*, 'by_reference' => false*/, 'choice_attr' => ['class' => 'row'], 'choice_translation_domain' => false, 'choices' => $options['subscription_choices'], 'multiple' => true, 'preferred_choices' => $options['subscription_favorites'], 'required' => false])
+                                       ->addModelTransformer(new SubscriptionTransformer($this->manager))
+                                       #->addModelTransformer(new CollectionToArrayTransformer)
+                       );
+                       /*, 'expanded' => true*/ /*, 'data' => $options['subscription_default']*/
                }
 
-               //Return form
-               return $form;
+               //Add extra zipcode field
+               if (!empty($options['zipcode'])) {
+                       $builder->add('zipcode', TextType::class, ['attr' => ['placeholder' => 'Your zipcode'], 'required' => false]);
+               }
+
+               //Call parent
+               parent::buildForm($builder, $options);
        }
 
        /**
@@ -51,7 +119,53 @@ class RegisterType extends \Rapsys\UserBundle\Form\RegisterType {
                parent::configureOptions($resolver);
 
                //Set defaults
-               $resolver->setDefaults(['phone' => true, 'pseudonym' => true, 'slug' => true]);
+               $resolver->setDefaults(
+                       [
+                               'city' => true,
+                               'country' => true,
+                               'country_class' => 'Rapsys\AirBundle\Entity\Country',
+                               'country_default' => null,
+                               'country_favorites' => [],
+                               'dance' => false,
+                               'dance_choices' => [],
+                               #'dance_default' => null,
+                               'dance_favorites' => [],
+                               'phone' => true,
+                               'pseudonym' => true,
+                               'subscription' => false,
+                               'subscription_choices' => [],
+                               #'subscription_default' => null,
+                               'subscription_favorites' => [],
+                               'zipcode' => true
+                       ]
+               );
+
+               //Add extra city option
+               $resolver->setAllowedTypes('city', 'boolean');
+
+               //Add extra country option
+               $resolver->setAllowedTypes('country', 'boolean');
+
+               //Add country class
+               $resolver->setAllowedTypes('country_class', 'string');
+
+               //Add country default
+               $resolver->setAllowedTypes('country_default', [Country::class, 'null']);
+
+               //Add country favorites
+               $resolver->setAllowedTypes('country_favorites', 'array');
+
+               //Add extra dance option
+               $resolver->setAllowedTypes('dance', 'boolean');
+
+               //Add dance choices
+               $resolver->setAllowedTypes('dance_choices', 'array');
+
+               //Add dance default
+               #$resolver->setAllowedTypes('dance_default', 'integer');
+
+               //Add dance favorites
+               $resolver->setAllowedTypes('dance_favorites', 'array');
 
                //Add extra phone option
                $resolver->setAllowedTypes('phone', 'boolean');
@@ -59,15 +173,26 @@ class RegisterType extends \Rapsys\UserBundle\Form\RegisterType {
                //Add extra pseudonym option
                $resolver->setAllowedTypes('pseudonym', 'boolean');
 
-               //Add extra slug option
-               $resolver->setAllowedTypes('slug', 'boolean');
-       }
+               //Add extra subscription option
+               $resolver->setAllowedTypes('subscription', 'boolean');
 
+               //Add subscription choices
+               $resolver->setAllowedTypes('subscription_choices', 'array');
+
+               //Add subscription default
+               #$resolver->setAllowedTypes('subscription_default', 'integer');
+
+               //Add subscription favorites
+               $resolver->setAllowedTypes('subscription_favorites', 'array');
+
+               //Add extra zipcode option
+               $resolver->setAllowedTypes('zipcode', 'boolean');
+       }
 
        /**
         * {@inheritdoc}
         */
        public function getName(): string {
-               return 'rapsys_air_register';
+               return 'rapsysair_register';
        }
 }
index e7c238bd362eb5106f9db0b4dd8963ea019b00bd..d6f1ba3095d841abea876e9927a2526c23964936 100644 (file)
@@ -2,7 +2,8 @@
 
 namespace Rapsys\AirBundle\Form;
 
-use Symfony\Bridge\Doctrine\RegistryInterface;
+use Doctrine\Persistence\ManagerRegistry;
+use Symfony\Bridge\Doctrine\Form\Type\EntityType;
 use Symfony\Component\Form\AbstractType;
 use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
 use Symfony\Component\Form\Extension\Core\Type\DateType;
@@ -11,11 +12,12 @@ use Symfony\Component\Form\Extension\Core\Type\TimeType;
 use Symfony\Component\Form\FormBuilderInterface;
 use Symfony\Component\OptionsResolver\OptionsResolver;
 use Symfony\Component\Translation\TranslatorInterface;
-use Symfony\Component\Validator\Constraints\Date;
+use Symfony\Component\Validator\Constraints\Type;
 use Symfony\Component\Validator\Constraints\NotBlank;
-use Symfony\Component\Validator\Constraints\Time;
 
+use Rapsys\AirBundle\Entity\Dance;
 use Rapsys\AirBundle\Entity\Location;
+use Rapsys\AirBundle\Entity\Slot;
 use Rapsys\AirBundle\Entity\User;
 
 class SessionType extends AbstractType {
@@ -25,14 +27,19 @@ class SessionType extends AbstractType {
        /**
         * {@inheritdoc}
         */
-       public function __construct(RegistryInterface $doctrine) {
+       public function __construct(ManagerRegistry $doctrine) {
                $this->doctrine = $doctrine;
        }
 
        /**
+        * @todo: clean that shit
+        * @todo: mapped => false for each button not related with session !!!!
+        * @todo: set stuff in the SessionController, no loggic here please !!!
+        * @todo: add the dance link stuff
+        *
         * {@inheritdoc}
         */
-       public function buildForm(FormBuilderInterface $builder, array $options) {
+       public function buildForm(FormBuilderInterface $builder, array $options): void {
                //Is admin or user with rainfall >= 2
                if (!empty($options['raincancel'])) {
                        //Add raincancel item
@@ -46,14 +53,21 @@ class SessionType extends AbstractType {
                //Is admin or owner
                if (!empty($options['modify'])) {
                        if (!empty($options['admin'])) {
-                               $builder->add('date', DateType::class, ['attr' => ['placeholder' => 'Your date', 'class' => 'date'], 'html5' => true, 'input' => 'datetime', 'widget' => 'single_text', 'format' => 'yyyy-MM-dd', 'data' => $options['date'], 'constraints' => [new NotBlank(['message' => 'Please provide your date']), new Date(['message' => 'Your date doesn\'t seems to be valid'])]]);
+                               $builder
+                                       //Add dance field
+                                       ->add('dance', EntityType::class, ['class' => 'Rapsys\AirBundle\Entity\Dance', 'choices' => $options['dance_choices'], 'preferred_choices' => $options['dance_favorites'], 'attr' => ['placeholder' => 'Your dance'], 'choice_translation_domain' => true, 'constraints' => [new NotBlank(['message' => 'Please provide your dance'])], 'data' => $options['dance_default']])
+
+                                       //Add slot field
+                                       ->add('slot', EntityType::class, ['class' => 'Rapsys\AirBundle\Entity\Slot', 'attr' => ['placeholder' => 'Your slot'], 'constraints' => [new NotBlank(['message' => 'Please provide your slot'])], 'choice_translation_domain' => true, 'data' => $options['slot_default']])
+                                       //Add date field
+                                       ->add('date', DateType::class, ['attr' => ['placeholder' => 'Your date', 'class' => 'date'], 'html5' => true, 'input' => 'datetime', 'widget' => 'single_text', 'format' => 'yyyy-MM-dd', 'data' => $options['date'], 'constraints' => [new NotBlank(['message' => 'Please provide your date']), new Type(['type' => \DateTime::class, 'message' => 'Your date doesn\'t seems to be valid'])]]);
                        }
 
                        $builder
                                //TODO: avertissement + minimum et maximum ???
-                               ->add('begin', TimeType::class, ['attr' => ['placeholder' => 'Your begin', 'class' => 'time'], 'html5' => true, 'input' => 'datetime', 'widget' => 'single_text', 'data' => $options['begin'], 'constraints' => [new NotBlank(['message' => 'Please provide your begin']), new Time(['message' => 'Your begin doesn\'t seems to be valid'])]])
+                               ->add('begin', TimeType::class, ['attr' => ['placeholder' => 'Your begin', 'class' => 'time'], 'html5' => true, 'input' => 'datetime', 'widget' => 'single_text', 'data' => $options['begin'], 'constraints' => [new NotBlank(['message' => 'Please provide your begin']), new Type(['type' => \DateTime::class, 'message' => 'Your begin doesn\'t seems to be valid'])]])
                                //TODO: avertissement + minimum et maximum ???
-                               ->add('length', TimeType::class, ['attr' => ['placeholder' => 'Your length', 'class' => 'time'], 'html5' => true, 'input' => 'datetime', 'widget' => 'single_text', 'data' => $options['length'], 'constraints' => [new NotBlank(['message' => 'Please provide your length']), new Time(['message' => 'Your length doesn\'t seems to be valid'])]])
+                               ->add('length', TimeType::class, ['attr' => ['placeholder' => 'Your length', 'class' => 'time'], 'html5' => true, 'input' => 'datetime', 'widget' => 'single_text', 'data' => $options['length'], 'constraints' => [new NotBlank(['message' => 'Please provide your length']), new Type(['type' => \DateTime::class, 'message' => 'Your length doesn\'t seems to be valid'])]])
                                ->add('modify', SubmitType::class, ['label' => 'Modify', 'attr' => ['class' => 'submit']]);
                }
 
@@ -75,7 +89,7 @@ class SessionType extends AbstractType {
                //Add extra user field
                if (!empty($options['admin'])) {
                        //Load users
-                       $users = $this->doctrine->getRepository(User::class)->findAllApplicantBySession($options['session']);
+                       $users = $this->doctrine->getRepository(User::class)->findBySessionId($options['session']);
                        //Add admin fields
                        $builder
                                //TODO: class admin en rouge ???
@@ -90,34 +104,61 @@ class SessionType extends AbstractType {
                                        ->add('autoattribute', SubmitType::class, ['label' => 'Auto attribute', 'attr' => ['class' => 'submit']]);
                        }
                }
-
-               //Return form
-               return $builder;
        }
 
        /**
         * {@inheritdoc}
         */
-       public function configureOptions(OptionsResolver $resolver) {
-               $resolver->setDefaults(['error_bubbling' => true, 'admin' => false, 'date' => null, 'begin' => null, 'length' => null, 'cancel' => false, 'raincancel' => false, 'modify' => false, 'move' => false, 'attribute' => false, 'user' => null, 'session' => null]);
+       public function configureOptions(OptionsResolver $resolver): void {
+               $resolver->setDefaults(['error_bubbling' => true, 'admin' => false, 'dance_choices' => [], 'dance_default' => null, 'dance_favorites' => [], 'date' => null, 'begin' => null, 'length' => null, 'cancel' => false, 'raincancel' => false, 'modify' => false, 'move' => false, 'attribute' => false, 'user' => null, 'session' => null, 'slot_default' => null]);
+
+               //Add admin
                $resolver->setAllowedTypes('admin', 'boolean');
-               #TODO: voir si c'est le bon type
+
+               //Add dance choices
+               $resolver->setAllowedTypes('dance_choices', 'array');
+
+               //Add dance default
+               $resolver->setAllowedTypes('dance_default', [Dance::class, 'null']);
+
+               //Add dance favorites
+               $resolver->setAllowedTypes('dance_favorites', 'array');
+
+               //Add date
                $resolver->setAllowedTypes('date', 'datetime');
+
+               //Add begin
                $resolver->setAllowedTypes('begin', 'datetime');
+
+               //Add length
                $resolver->setAllowedTypes('length', 'datetime');
+
+               //Add cancel
                $resolver->setAllowedTypes('cancel', 'boolean');
+
+               //Add raincancel
                $resolver->setAllowedTypes('raincancel', 'boolean');
+
+               //Add modify
                $resolver->setAllowedTypes('modify', 'boolean');
+
+               //Add move
                $resolver->setAllowedTypes('move', 'boolean');
+
+               //Add attribute
                $resolver->setAllowedTypes('attribute', 'boolean');
+
+               //Add user
                $resolver->setAllowedTypes('user', 'integer');
+
+               //Add session
                $resolver->setAllowedTypes('session', 'integer');
        }
 
        /**
         * {@inheritdoc}
         */
-       public function getName() {
-               return 'rapsys_air_session_edit';
+       public function getName(): string {
+               return 'rapsysair_session_edit';
        }
 }
index 05f581833d77d07c7d127fd9ab285e2c326c8a80..8f81b408dc80f3c3ceebefad820b9dbadcbfff89 100644 (file)
@@ -13,51 +13,113 @@ use Symfony\Component\Form\Extension\Core\Type\UrlType;
 use Symfony\Component\Form\FormBuilderInterface;
 use Symfony\Component\OptionsResolver\OptionsResolver;
 use Symfony\Component\Validator\Constraints\File;
-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
+       public function buildForm(FormBuilderInterface $form, array $options): FormBuilderInterface {
+               //Start build form
+               $form
+                       //Add locale, location and user hidden fields
                        ->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], 'required' => false])
-                       ->add('class', TextareaType::class, ['attr' => ['placeholder' => 'Your class', 'cols' => 50, 'rows' => 10], 'required' => false])
-                       ->add('short', TextareaType::class, ['attr' => ['placeholder' => 'Your short', 'cols' => 50, 'rows' => 10], 'required' => false])
-                       ->add('rate', NumberType::class, ['attr' => ['placeholder' => 'Your rate'], 'required' => false])
-                       ->add('hat', CheckboxType::class, ['attr' => ['placeholder' => 'Your hat'], 'required' => false])
-                       ->add('contact', UrlType::class, ['attr' => ['placeholder' => 'Your contact'], 'required' => false])
-                       ->add('donate', UrlType::class, ['attr' => ['placeholder' => 'Your donate'], 'required' => false])
-                       ->add('link', UrlType::class, ['attr' => ['placeholder' => 'Your link'], 'required' => false])
-                       ->add('profile', UrlType::class, ['attr' => ['placeholder' => 'Your profile'], 'required' => false])
-                       ->add('image', FileType::class, ['attr' => ['placeholder' => 'Your image'], 'constraints' => [new File(['maxSize' => '5M', 'mimeTypes' => ['image/jpeg', 'image/png', 'image/tiff', 'image/webp'], 'mimeTypesMessage' => 'Please upload a valid Image document'])], 'mapped' => false, 'required' => false])
-                       ->add('submit', SubmitType::class, ['label' => 'Send', 'attr' => ['class' => 'submit']]);
+                       ->add('user', HiddenEntityType::class, ['required' => true]);
+
+               //With description
+               if ($options['description']) {
+                       $form->add('description', TextareaType::class, ['attr' => ['placeholder' => 'Your description', 'cols' => 50, 'rows' => 15], 'required' => false]);
+               }
+
+               //With class
+               if ($options['class']) {
+                       $form->add('class', TextareaType::class, ['attr' => ['placeholder' => 'Your class', 'cols' => 50, 'rows' => 10], 'required' => false]);
+               }
+
+               //With short
+               if ($options['short']) {
+                       $form->add('short', TextareaType::class, ['attr' => ['placeholder' => 'Your short', 'cols' => 50, 'rows' => 10], 'required' => false]);
+               }
+
+               //With rate
+               if ($options['rate']) {
+                       $form->add('rate', NumberType::class, ['attr' => ['placeholder' => 'Your rate'], 'required' => false]);
+               }
+
+               //With hat
+               if ($options['hat']) {
+                       $form->add('hat', CheckboxType::class, ['attr' => ['placeholder' => 'Your hat'], 'required' => false]);
+               }
+
+               //With contact
+               if ($options['contact']) {
+                       $form->add('contact', UrlType::class, ['attr' => ['placeholder' => 'Your contact'], 'required' => false]);
+               }
+
+               //With donate
+               if ($options['donate']) {
+                       $form->add('donate', UrlType::class, ['attr' => ['placeholder' => 'Your donate'], 'required' => false]);
+               }
+
+               //With link
+               if ($options['link']) {
+                       $form->add('link', UrlType::class, ['attr' => ['placeholder' => 'Your link'], 'required' => false]);
+               }
+
+               //With profile
+               if ($options['profile']) {
+                       $form->add('profile', UrlType::class, ['attr' => ['placeholder' => 'Your profile'], 'required' => false]);
+               }
+
+               //Add submit
+               $form->add('submit', SubmitType::class, ['label' => 'Send', 'attr' => ['class' => 'submit']]);
+
+               //Return form builder
+               return $form;
        }
 
        /**
         * {@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']);
+       public function configureOptions(OptionsResolver $resolver): void {
+               //Set defaults
+               $resolver->setDefaults(['class' => true, 'contact' => true, 'data_class' => Snippet::class, 'description' => true, 'donate' => true, 'error_bubbling' => true, 'hat' => true, 'link' => true, 'profile' => true, 'rate' => true, 'short' => true]);
+
+               //Add extra class option
+               $resolver->setAllowedTypes('class', 'boolean');
+
+               //Add extra contact option
+               $resolver->setAllowedTypes('contact', 'boolean');
+
+               //Add extra description option
+               $resolver->setAllowedTypes('description', 'boolean');
+
+               //Add extra donate option
+               $resolver->setAllowedTypes('donate', 'boolean');
+
+               //Add extra hat option
+               $resolver->setAllowedTypes('hat', 'boolean');
+
+               //Add extra link option
+               $resolver->setAllowedTypes('link', 'boolean');
+
+               //Add extra profile option
+               $resolver->setAllowedTypes('profile', 'boolean');
+
+               //Add extra rate option
+               $resolver->setAllowedTypes('rate', 'boolean');
+
+               //Add extra short option
+               $resolver->setAllowedTypes('short', 'boolean');
        }
 
        /**
         * {@inheritdoc}
-        * XXX: this doesn't work, because it's impossible to generate this same id on other side
-        * TODO: we would need to be able to generate this id at form creation
-        *
-       public function getBlockPrefix() {
-               //Prevent collision between instances with an unique block prefix
-               return 'snippet_'.uniqid();
-       }*/
+        */
+       public function getName(): string {
+               return 'rapsysair_snippet';
+       }
 }
index b742db003f74bc75014d724ab0192c8e51fedbde..961b5260e7d1e856868b03bf9abc285a10a24edd 100644 (file)
@@ -1,8 +1,18 @@
-<?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\Handler;
 
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Security\Core\Exception\AccessDeniedException;
 use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface;
 
@@ -16,7 +26,7 @@ class AccessDeniedHandler extends AbstractController implements AccessDeniedHand
        /**
         * {@inheritdoc}
         */
-       public function handle(Request $request, AccessDeniedException $exception) {
+       public function handle(Request $request, AccessDeniedException $exception): Response {
                //Set title
                $this->context['title'] = $this->translator->trans('Access denied');
 
@@ -25,7 +35,7 @@ class AccessDeniedHandler extends AbstractController implements AccessDeniedHand
                $this->context['message'] = $exception->getMessage();
 
                //With admin
-               if ($this->isGranted('ROLE_ADMIN')) {
+               if ($this->checker->isGranted('ROLE_ADMIN')) {
                        //Add trace for admin
                        $this->context['trace'] = $exception->getTraceAsString();
                }
diff --git a/Pdf/DisputePdf.php b/Pdf/DisputePdf.php
deleted file mode 100644 (file)
index f7ee8da..0000000
+++ /dev/null
@@ -1,787 +0,0 @@
-<?php
-
-namespace Rapsys\AirBundle\Pdf;
-
-class DisputePdf extends \Fpdf\Fpdf {
-       //Generate gathering dispute
-       public static function genGathering($court, $notice, $agent, $service, $abstract, $civility, $forename, $surname) {
-               //Create new pdf
-               $p = new DisputePdf('P', 'mm', 'A4');
-
-               //Set author
-               $p->SetAuthor('Réaction 19', true);
-
-               //Set creator
-               $p->SetCreator(basename(__FILE__), true);
-
-               //Set subject
-               $p->SetSubject('Contestation de contravention pour rassemblement interdit sur la voie publique', true);
-
-               //Set title
-               $p->SetTitle('Modèle de contestation', true);
-
-               //Disable auto page break
-               $p->SetAutoPageBreak(true, 10);
-
-               //Add page
-               $p->AddPage('P', 'A4', 0);
-
-               //Add text
-               $p->Multi('À '.ucfirst(trim($court)).' le '.date('d/m/Y'), 0, '', 10, 4, 'R');
-
-               //Save x
-               $x = $p->GetX();
-
-               //Set margins
-               $p->SetXY(105, 60);
-
-               //Add formal
-               $p->Multi('L\'OFFICIER DU MINISTÈRE PUBLIC
-PRÈS LE TRIBUNAL DE POLICE DE
-'.strtoupper(trim($court)), 0, '', 12);
-
-               //Jump line
-               $p->Ln(16);
-
-               //Add notice number
-               $p->Multi('Numéro avis : '.trim($notice), 6, 'B', 10, 2);
-
-               //Add object
-               $p->Multi('Objet : CONTESTATION DE CONTRAVENTION', 6, 'B', 10, 6);
-
-               //Add text
-               $p->Multi('Madame, Monsieur l\'Officier du Ministère Public,
-
-Par la présente, j\'entends former opposition à l\'encontre de l\'avis de contravention référencé ci-avant dressé à mon encontre.
-
-A cette fin, je vous prie de bien vouloir trouver sous ce pli le formulaire de requête en exonération dûment rempli, ainsi que l\'original de l\'avis de contravention.
-
-Après un rappel des faits et de la procédure qui ont conduit à dresser cet avis de contravention (I), il sera démontré que ledit avis est entaché d\'irrégularité manifeste (II).');
-
-               //Add title
-               $p->Title1('I/ RAPPEL DES FAITS OBJET DE LA PRESENTE CONTRAVENTION');
-
-               //Add text
-               $p->Body('L\'avis de contravention contesté m\'a été adressé en raison d\'une prétendue participation à un rassemblement interdit sur la voie publique, dans les termes suivants :');
-
-               //Add text
-               $p->Quoted('« Rassemblement interdit sur la voie publique dans une circonscription territoriale en état d\'urgence sanitaire et devant faire face à l\'épidémie de Covid-19 ».', 'BI');
-
-               //Add text
-               $p->Quoted('étant précisé qu\'il est visé à l\'avis de contravention les articles « L.3131-15 §1 6°, L.3131-13, L 3131-16 al.2, L 3131-17 §1 du Code de la santé publique, Art 3, art 3-1 al. 2, art. 1 du décret du 2020-1310 du 29-10-2020 » et en répression l\'article L.3136-1 al.3 du Code de la santé publique.');
-
-               //Add text
-               $p->Body('Cette infraction a été constatée et validée par un agent verbalisateur'.(!empty($agent)?' numéro '.trim($agent):'').(!empty($service)?' doté du code service '.trim($service):'').', sans plus de précision quant à sa qualité exacte.');
-
-               //With abstract
-               if (!empty($abstract)) {
-                       //Add text
-                       $p->Body(trim($abstract));
-               }
-
-               //Add title
-               $p->Title1('II/ UN AVIS DE CONTRAVENTION ENTACHÉ D\'IRRÉGULARITÉ MANIFESTE');
-
-               //Add title
-               $p->Title2('II.1 - L\'article 3136-1 du code la santé publique visé à l\'avis de contravention ne réprime pas l\'infraction de rassemblement interdit');
-
-               //Add ittle
-               $p->Title3('(i) En droit - le principe de l\'application stricte de la loi pénale');
-
-               //Add text
-               $p->Body('L\'article 111-4 du code pénal dispose :');
-
-               //Add text
-               $p->Quoted('« La loi pénale est d\'interprétation stricte. »');
-
-               //Add text
-               $p->Body('La Cour Européenne des Droits de l\'Homme a reconnu que le principe de l\'interprétation stricte de la loi pénale constituait un corollaire du principe de légalité (cf. CEDH, 25 mai 1993, Kokkinakis c. Grèce).
-
-Il est ainsi admis que le principe de l\'interprétation stricte de la loi pénale a une valeur normative équivalente aux principes affirmés à l\'article 7 § 1 de la Convention et qu\'il contribue, à l\'instar de ces derniers, à protéger les individus contre toute forme de répression arbitraire.
-
-La jurisprudence constante de la Chambre criminelle de la Cour de Cassation interdit au demeurant toute interprétation par « extension, analogie ou induction » (Cass. Crim 9 août 1913- Cass. Crim 1er juin 1977 n°76.91-999).
-
-Seule une loi pénale obscure peut faire l\'objet d\'une interprétation.
-
-En conséquence de l\'application de ce principe, dès lors qu\'une loi pénale est dépourvue de toute ambiguïté, celle-ci doit être interprétée strictement.');
-
-               //Add title
-               $p->Title3('(ii) En fait');
-
-               //Add text
-               $p->Body('L\'avis de contravention vise l\'article 3136-1 alinéa 3 du code de la santé publique, lequel dispose :');
-
-               //Add text
-               $p->Quoted('« La violation des autres interdictions ou obligations édictées en application des articles L. 3131-1 et L. 3131-15 à L. 3131-17 est punie de l\'amende prévue pour les contraventions de la quatrième classe. Cette contravention peut faire l\'objet de la procédure de l\'amende forfaitaire prévue à l\'article 529 du code de procédure pénale. Si cette violation est constatée à nouveau dans un délai de quinze jours, l\'amende est celle prévue pour les contraventions de la cinquième classe. ».');
-
-               //Add text
-               $p->Body('Force est de constater que ce texte de répression renvoie à des textes de prévention dont il édicte prétendument la sanction.
-
-Ce texte répressif vise les articles L. 3131-1 et L. 3131-15 à L. 3131-17 du code de la santé publique.
-
-Or, ces quatre articles ne définissent pas l\'infraction de rassemblement interdit :');
-
-               //Add text
-               $p->Quoted('- Les violations des interdictions ou obligations édictées par l\'article 3131-1 du CSP à savoir les mesures prises sur arrêté du 1er ministre et/ou des préfets pour des mesures individuelles ou collectives ne mentionnent pas une quelconque interdiction de rassemblement;
-- Les violations des interdictions ou obligations édictées par l\'article 3131-15 du CSP c\'est-à-dire des mesures prises par le 1er ministre « dans les circonscriptions territoriales où l\'état d\'urgence sanitaire est déclaré ». Cet article n\'incrimine pas les rassemblements sur la voie publique. 
-- Les violations des interdictions ou obligations édictées par l\'article 3131-16 CSP c\'est-à-dire des mesures prises par le ministre de la santé « dans les circonscriptions territoriales où l\'état d\'urgence sanitaire est déclaré ». Cet article ne vise pas les rassemblements interdits,
-- Les violations des interdictions ou obligations édictées par l\'article 3131-17 CSP c\'est-à-dire des mesures prises par le représentant de l\'Etat territorialement compétent, dûment habilité par le 1 er ministre ou le ministre de la santé ne mentionnent pas une quelconque interdiction de rassemblement.');
-
-               //Add text
-               $p->Body('En d\'autres termes, l\'article L. 3136-1 alinéa 3 du code de la santé publique renvoie à des textes de prévention qui ne définissent pas l\'infraction de rassemblement interdit sur la voie publique dans une circonscription territoriale en état d\'urgence sanitaire ou devant faire face à l\'épidémie de COVID-19.');
-
-               //Add text
-               $p->Body('Par conséquent, l\'article L. 3136-1 alinéa 3 du code de la santé publique ne réprime pas les rassemblements interdits sur la voie publique.
-Dès lors, l\'avis de contravention ne mentionne pas le texte de répression de l\'infraction qui m\'est reprochée.', 'BU');
-
-               //Add text
-               $p->Body('L\'absence de cette mention entache l\'avis de contravention d\'irrégularité manifeste.', 'B');
-
-               //Add title
-               $p->Title2('II.2 En tout état de cause, le non-respect du principe de légalité');
-
-               //Add title
-               $p->Title3('(i) En droit');
-
-               //Add text
-               $p->Body('Le droit pénal français est fondé sur le principe fondamental de la légalité des délits et des peines selon lequel quiconque ne peut être condamné en l\'absence d\'un texte clair et précis.
-
-Ce principe est au demeurant consacré par l\'article 8 de la Déclaration des droits de l\'Homme et du Citoyen de 1789 et a donc une valeur constitutionnelle.
-
-Plus encore, l\'article 111-3 du code pénal dispose : ');
-
-               //Add text
-               $p->Quoted('« Nul ne peut être puni pour un crime ou pour un délit dont les éléments ne sont pas définis par la loi, ou pour une contravention dont les éléments ne sont pas définis par le règlement.
-
-Nul ne peut être puni d\'une peine qui n\'est pas prévue par la loi, si l\'infraction est un crime ou un délit, ou par le règlement, si l\'infraction est une contravention. »');
-
-               //Add text
-               $p->Body('Il en découle que chaque justiciable doit être en mesure de connaître non seulement les textes prévoyant l\'incrimination d\'un comportement déterminé mais également les textes fondant les peines applicables à l\'infraction visée. 
-
-En matière de contraventions, l\'article A37-4 du Code de procédure pénale prévoit :');
-
-               //Add text
-               $p->Quoted('« Les caractéristiques de l\'avis de contravention mentionné à l\'article A.37-1 sont les suivantes:
-I. Sur la partie gauche sont portées les mentions relatives au service verbalisateur, à la nature, au lieu et à la date de la contravention ainsi que les références des textes réprimant ladite contravention et, le cas échéant, sont précisés les éléments d\'identification du véhicule et l\'obligation de procéder à l\'échange du permis de conduire.
-
-Ainsi, le Code de procédure pénale exige, comme condition de recevabilité et conformément au principe de légalité, que les textes répressifs soient mentionnés à l\'acte de contravention.');
-
-               //Add title
-               $p->Title3('(ii) En fait');
-
-               //Add text
-               $p->Body('En prévention, l\'avis de contravention précité mentionne un certain nombre d\'articles :
-
-- L\'article L.3131-15 §I 6° du Code de la santé publique :');
-
-               //Add text
-               $p->Quoted('« I. Dans les circonscriptions territoriales où l\'état d\'urgence sanitaire est déclaré, le Premier ministre peut, par décret réglementaire pris sur le rapport du ministre chargé de la santé, aux seules fins de garantir la santé publique :
-
-(...)
-
-6° Limiter ou interdire les rassemblements sur la voie publique ainsi que les réunions de toute nature ;»');
-
-               //Add text
-               $p->Body('Cet alinéa mentionne la faculté par le Premier Ministre de limiter ou interdire des rassemblements sur la voie publique ainsi que les réunions de toute nature, par voie de décret spécifique.
-Ce texte ne prévoit donc pas l\'infraction qui m\'est reprochée.
-
--L\'article L.3131-13 du même code, est également mentionné sans référence à un alinéa en particulier :');
-
-               //Add text
-               $p->Quoted('« L\'état d\'urgence sanitaire est déclaré par décret en conseil des ministres pris sur le rapport du ministre chargé de la santé. Ce décret motivé détermine la ou les circonscriptions territoriales à l\'intérieur desquelles il entre en vigueur et reçoit application. Les données scientifiques disponibles sur la situation sanitaire qui ont motivé la décision sont rendues publiques.
-
-               L\'Assemblée nationale et le Sénat sont informés sans délai des mesures prises par le Gouvernement au titre de l\'état d\'urgence sanitaire. L\'Assemblée nationale et le Sénat peuvent requérir toute information complémentaire dans le cadre du contrôle et de l\'évaluation de ces mesures.
-
-               La prorogation de l\'état d\'urgence sanitaire au-delà d\'un mois ne peut être autorisée que par la loi, après avis du comité de scientifiques prévu à l\'article L. 3131-19. »');
-
-               //Add text
-               $p->Body('Cette disposition est donc relative à l\'état d\'urgence sanitaire.
-
-- L\'article 3131-17 §1 du code de la santé publique, également cité à la prévention, dispose quant à lui :');
-
-               //Add text
-               $p->Quoted('« I. - Lorsque le Premier ministre ou le ministre chargé de la santé prennent des mesures mentionnées aux articles L. 3131-15 et L. 3131-16, ils peuvent habiliter le représentant de l\'Etat territorialement compétent à prendre toutes les mesures générales ou individuelles d\'application de ces dispositions.
-
-Lorsque les mesures prévues aux 1°, 2° et 5° à 9° du I de l\'article L. 3131-15 et à l\'article L. 3131-16 doivent s\'appliquer dans un champ géographique qui n\'excède pas le territoire d\'un département, les autorités mentionnées aux mêmes articles L. 3131-15 et L. 3131-16 peuvent habiliter le représentant de l\'Etat dans le département à les décider lui-même. Les décisions sont prises par ce dernier après avis du directeur général de l\'agence régionale de santé. ».');
-
-               //Add text
-               $p->Body('L\'article 3131-16 al. 2 dispose que :');
-
-               //Add text
-               $p->Quoted('« Dans les mêmes conditions, le ministre chargé de la santé peut prescrire toute mesure individuelle nécessaire à l\'application des mesures prescrites par le Premier ministre en application des 1° à 9° du I de l\'article L. 3131-15. ».');
-
-               //Add text
-               $p->Body('- Outre ces dispositions, sont également visés les articles 1 et 3 du décret 2020-1310 du 29 octobre 2020 à savoir :');
-
-               //Add text
-               $p->Quoted('«I. - Afin de ralentir la propagation du virus, les mesures d\'hygiène définies en annexe 1 au présent décret et de distanciation sociale, incluant la distanciation physique d\'au moins un mètre entre deux personnes, dites barrières, définies au niveau national, doivent être observées en tout lieu et en toute circonstance.
-
-II. - Les rassemblements, réunions, activités, accueils et déplacements ainsi que l\'usage des moyens de transports qui ne sont pas interdits en vertu du présent décret sont organisés en veillant au strict respect de ces mesures. Dans les cas où le port du masque n\'est pas prescrit par le présent décret, le préfet de département est habilité à le rendre obligatoire, sauf dans les locaux d\'habitation, lorsque les circonstances locales l\'exigent.».');
-
-               //Add text
-               $p->Quoted('«I. - Tout rassemblement, réunion ou activité sur la voie publique ou dans un lieu ouvert au public, qui n\'est pas interdit par le présent décret, est organisé dans des conditions de nature à permettre le respect des dispositions de l\'article 1er.
-II. - Les organisateurs des manifestations sur la voie publique mentionnées à l\'article L. 211-1 du code de la sécurité intérieure adressent au préfet de département sur le territoire duquel la manifestation doit avoir lieu, sans préjudice des autres formalités applicables, une déclaration contenant les mentions prévues à l\'article L. 211-2 du même code, en y précisant, en outre, les mesures qu\'ils mettent en œuvre afin de garantir le respect des dispositions de l\'article 1er du présent décret.
-Sans préjudice des dispositions de l\'article L. 211-4 du code de la sécurité intérieure, le préfet peut en prononcer l\'interdiction si ces mesures ne sont pas de nature à permettre le respect des dispositions de l\'article 1er.
-III. - Les rassemblements, réunions ou activités sur la voie publique ou dans un lieu ouvert au public autres que ceux mentionnés au II mettant en présence de manière simultanée plus de six personnes sont interdits.
-Ne sont pas soumis à cette interdiction :
-1° Les rassemblements, réunions ou activités à caractère professionnel ;
-2° Les services de transport de voyageurs ;
-3° Les établissements recevant du public dans lesquels l\'accueil du public n\'est pas interdit en application du présent décret ;
-4° Les cérémonies funéraires organisées hors des établissements mentionnés au 3°, dans la limite de 30 personnes ;
-5° Les cérémonies publiques mentionnées par le décret du 13 septembre 1989 susvisé.
-La dérogation mentionnée au 3° n\'est pas applicable pour la célébration de mariages.
-IV. - Le préfet de département est habilité à interdire ou à restreindre, par des mesures réglementaires ou individuelles, tout rassemblement, réunion ou activité mettant en présence de manière simultanée plus de six personnes sur la voie publique ou dans des lieux ouverts au public relevant du III, lorsque les circonstances locales l\'exigent. Toutefois, dans les collectivités de l\'article 74 de la Constitution et en Nouvelle-Calédonie, sous réserve que le présent décret leur soit applicable en vertu des dispositions de l\'article 55, le représentant de l\'Etat est habilité à prendre des mesures d\'interdiction proportionnées à l\'importance du risque de contamination en fonction des circonstances locales, après avis de l\'autorité compétente en matière sanitaire.»');
-
-               //Add text
-               $p->Body('- En répression, il est renvoyé à l\'article L.3136-1 du Code de la santé publique en son 3ème alinéa, dont le contenue est le suivant :');
-
-               //Add text
-               $p->Quoted('« La violation des autres interdictions ou obligations édictées en application des articles L. 3131-1 et L. 3131-15 à L. 3131-17 est punie de l\'amende prévue pour les contraventions de la quatrième classe. Cette contravention peut faire l\'objet de la procédure de l\'amende forfaitaire prévue à l\'article 529 du code de procédure pénale. Si cette violation est constatée à nouveau dans un délai de quinze jours, l\'amende est celle prévue pour les contraventions de la cinquième classe. ».');
-
-               //Add text
-               $p->Body('Cet alinéa 3 mentionne successivement l\'amende prévue pour les contraventions de la quatrième classe, ainsi que celle prévue pour la cinquième classe en cas de récidive dans un délai de 15 jours.');
-
-               //Add text
-               $p->Body('En aucun cas, il n\'est précisé la catégorie de contravention applicable à ma situation spécifique.
-
-Plus encore, outre la confusion générée par la référence à deux classes de contravention, en aucun cas l\'article précité, ou l\'article 529 du Code de procédure pénale ne fixent le montant de l\'amende forfaitaire à laquelle je suis condamné.', 'BU');
-
-               //Add text
-               $p->Body('En effet, cette information ressort de l\'article R. 49 du Code de procédure pénale, qui dispose :');
-
-               //Add text
-               $p->Quoted('« Le montant de l\'amende forfaitaire prévue par l\'article 529 est fixé ainsi qu\'il suit : [...]
-
-5° 135 euros pour les contraventions de la 4e classe 
-
-6° 200 euros pour les contraventions de la 5e classe ».', 'BI');
-
-               //Add text
-               $p->Body('Or, cet article n\'est nullement mentionné à l\'avis de contravention reçu !
-
-Par conséquent, l\'avis de contravention dressé à mon encontre est entaché d\'irrégularité.', 'B');
-
-               //Add text
-               $p->Body('A tout point de vue, l\'avis de contravention reçu souffre de plusieurs manquements graves de base légale à savoir :', 'B');
-
-               //Add text
-               $p->Quoted('- L\'avis de contravention est dépourvu de base légale puisque l\'infraction prétendument commise n\'est pas visée par un texte de répression ;
-
-- A considérer qu\'il soit besoin d\'examiner le contenu de l\'avis de contravention, il devra être considéré qu\'au regard du principe de légalité, lequel a pour corollaire le principe de légalité des peines, les textes de prévention ne sont pas correctement visés.', 'BU');
-
-               //Add text
-               $p->Body('Or, en application des principes fondamentaux et constitutionnels, un fait ne peut être réprimé pénalement qu\'en vertu d\'une disposition pénale suffisamment précise et claire, et ce afin notamment d\'exclure tout arbitraire dans le prononcé des peines.');
-
-               //Add text
-               $p->Body('Par conséquent, cette condamnation pénale constitue une violation des principes essentiels rappelés.', 'BU');
-
-               //Add text
-               $p->Body('Pour l\'ensemble de ces raisons, je vous remercie, Madame ou Monsieur l\'Officier du Ministère Public, de faire droit à cette requête en me confirmant que vous renoncez à toute poursuite du chef de la contravention contestée et, le cas échéant, vous invite à me convoquer à une prochaine audience.
-
-Vous remerciant de l\'accueil et l\'attention que vous réserverez à la présente,
-
-Je vous prie d\'agréer, Madame, Monsieur l\'Officier du Ministère Public, l\'expression de mes sentiments distingués.');
-
-               //Add text
-               $p->Body('Signé, '.$civility.' '.$forename.' '.$surname);
-
-               //Close pdf
-               $p->Close();
-
-               //Return pdf
-               return $p->Output('S');
-
-       }
-
-       //Generate traffic dispute
-       public static function genTraffic($court, $notice, $agent, $service, $abstract, $civility, $forename, $surname) {
-               //Create new pdf
-               $p = new DisputePdf('P', 'mm', 'A4');
-
-               //Set author
-               $p->SetAuthor('Réaction 19', true);
-
-               //Set creator
-               $p->SetCreator(basename(__FILE__), true);
-
-               //Set subject
-               $p->SetSubject('Contestation de contravention pour circulation à une heure interdite', true);
-
-               //Set title
-               $p->SetTitle('Modèle de contestation', true);
-
-               //Disable auto page break
-               $p->SetAutoPageBreak(true, 10);
-
-               //Add page
-               $p->AddPage('P', 'A4', 0);
-
-               //Add text
-               $p->Multi('À '.ucfirst(trim($court)).' le '.date('d/m/Y'), 0, '', 10, 4, 'R');
-
-               //Save x
-               $x = $p->GetX();
-
-               //Set margins
-               $p->SetXY(105, 60);
-
-               //Add formal
-               $p->Multi('L\'OFFICIER DU MINISTÈRE PUBLIC
-PRÈS LE TRIBUNAL DE POLICE DE
-'.strtoupper(trim($court)), 0, '', 12);
-
-               //Jump line
-               $p->Ln(16);
-
-               //Add notice number
-               $p->Multi('Numéro avis : '.trim($notice), 6, 'B', 10, 2);
-
-               //Add object
-               $p->Multi('Objet : CONTESTATION DE CONTRAVENTION', 6, 'B', 10, 6);
-
-               //Add text
-               $p->Multi('Madame, Monsieur l\'Officier du Ministère Public,
-
-Par la présente, j\'entends former opposition à l\'encontre de l\'avis de contravention référencé ci-avant dressé à mon encontre.
-
-A cette fin, je vous prie de bien vouloir trouver sous ce pli le formulaire de requête en exonération dûment rempli, ainsi que l\'original de l\'avis de contravention.
-
-Après un rappel des faits et de la procédure qui ont conduit à dresser cet avis de contravention (I), il sera démontré que ledit avis est entaché d\'irrégularité manifeste (II).');
-
-               //Add title
-               $p->Title1('I/ RAPPEL DES FAITS OBJET DE LA PRESENTE CONTRAVENTION');
-
-               //Add text
-               $p->Body('L\'avis de contravention contesté m\'a été adressé en raison d\'une prétendue violation d\'une interdiction de déplacement à une heure interdite, dans les termes suivants :');
-
-               //Add text
-               $p->Quoted('« Circulation à une heure interdite dans une circonscription territoriale en état d\'urgence sanitaire et devant faire face à l\'épidémie de COVID-19 ».', 'BI');
-
-               //Add text
-               $p->Quoted('étant précisé qu\'il est visé à l\'avis de contravention les articles L.3131-15 §I 1°, L.3131-13 du Code de la santé publique, 3131-16 alinéa 2, 3131-17 §1, les articles 4 et 4-1 du décret 2020-1310 du 29-10-2020, et en répression l\'article L.3136-1 al. 3 du Code de la santé publique.');
-
-               //Add text
-               $p->Body('Cette infraction a été constatée et validée par un agent verbalisateur'.(!empty($agent)?' numéro '.trim($agent):'').(!empty($service)?' doté du code service '.trim($service):'').', sans plus de précision quant à sa qualité exacte.');
-
-               //With abstract
-               if (!empty($abstract)) {
-                       //Add text
-                       $p->Body(trim($abstract));
-               }
-
-               //Add title
-               $p->Title1('II/ UN AVIS DE CONTRAVENTION ENTACHÉ D\'IRRÉGULARITÉ MANIFESTE');
-
-               //Add title
-               $p->Title2('II.1 - Une infraction non réprimée par l\'article visé, à savoir l\'article 3136-1 al. 3 du code de la santé publique');
-
-               //Add title
-               $p->Title3('(i) En droit - le principe de l\'application stricte de la loi pénale');
-
-               //Add text
-               $p->Body('L\'article 111-4 du code pénal dispose :');
-
-               //Add text
-               $p->Quoted('« La loi pénale est d\'interprétation stricte. »', 'I');
-
-               //Add text
-               $p->Body('La Cour Européenne des Droits de l\'Homme a reconnu que le principe de l\'interprétation stricte de la loi pénale constituait un corollaire du principe de légalité (cf. CEDH, 25 mai 1993, Kokkinakis c. Grèce).
-
-Il est ainsi admis que le principe de l\'interprétation stricte de la loi pénale a une valeur normative équivalente aux principes affirmés à l\'article 7 § 1 de la Convention et qu\'il contribue, à l\'instar de ces derniers, à protéger les individus contre toute forme de répression arbitraire.');
-
-               //Add text
-               $p->Body('La jurisprudence constante de la Chambre criminelle de la Cour de Cassation interdit au demeurant toute interprétation par « extension, analogie ou induction » (Cass. Crim 9 août 1913- Cass. Crim 1er juin 1977 n°76.91-999).
-
-Seule une loi pénale obscure peut faire l\'objet d\'une interprétation.
-
-En conséquence de l\'application de ce principe, dès lors qu\'une loi pénale est dépourvue de toute ambiguïté, celle-ci doit être interprétée strictement.');
-
-               //Add title
-               $p->Title3('(ii) En fait');
-
-               //Add text
-               $p->Body('Sur la répression, l\'avis de contravention vise l\'article 3136-1 alinéa 3 du code de la santé publique, lequel dispose :');
-
-               //Add text
-               $p->Quoted('« La violation des autres interdictions ou obligations édictées en application des articles L. 3131-1 et L. 3131-15 à L. 3131-17 est punie de l\'amende prévue pour les contraventions de la quatrième classe. Cette contravention peut faire l\'objet de la procédure de l\'amende forfaitaire prévue à l\'article 529 du code de procédure pénale. Si cette violation est constatée à nouveau dans un délai de quinze jours, l\'amende est celle prévue pour les contraventions de la cinquième classe. ».');
-
-               //Add text
-               $p->Body('Force est de constater que ce texte de répression renvoie à des textes de prévention dont il édicte la sanction.', 'B');
-
-               //Add text
-               $p->Body('Ce texte répressif vise les articles L. 3131-1 et L. 3131-15 à L. 3131-17 du code de la santé publique.
-
-Or, ces quatre articles ne définissent pas l\'infraction de circulation à une heure interdite :');
-
-               //Add text
-               $p->Quoted('- Les violations des interdictions ou obligations édictées par l\'article 3131-1 du CSP à savoir les mesures prises sur arrêté du 1er ministre et/ou des préfets pour des mesures individuelles ou collectives ne mentionnent pas une quelconque circulation à une heure interdite;
-
-- Les violations des interdictions ou obligations édictées par l\'article 3131-15 du CSP c\'est-à-dire des mesures prises par le 1er ministre « dans les circonscriptions où l\'état d\'urgence sanitaire est déclaré ». Cet article n\'incrimine pas la circulation à une heure interdite;
-
-- Les violations des interdictions ou obligations édictées par l\'article 3131-16 CSP c\'est-à-dire des mesures prises par le ministre de la santé « dans les circonscriptions où l\'état d\'urgence sanitaire est déclaré ». Cet article ne vise la circulation à une heure interdite;
-
-- Les violations des interdictions ou obligations édictées par l\'article 3131-17 CSP c\'est-à-dire des mesures prises par le représentant de l\'État territorialement compétent, dûment habilité par le 1er ministre ou le ministre de la santé ne mentionnent pas une quelconque interdiction de circulation à une heure interdite.', '');
-
-               //Add text
-               $p->Body('En d\'autres termes, l\'article L. 3136-1 alinéa 3 du code de la santé publique renvoie à des textes de prévention qui ne définissent pas l\'infraction de circulation à une heure interdite.');
-
-               //Add text
-               $p->Body('Par conséquent, l\'article L. 3136-1 alinéa 3 du code de la santé publique ne réprime pas la circulation à une heure interdite.
-
-Dès lors, force est de constater que l\'avis de contravention ne mentionne pas le texte de répression de l\'infraction qui m\'est reprochée.', 'BU');
-
-               //Add text
-               $p->Body('L\'absence de cette mention entache l\'avis de contravention d\'irrégularité manifeste.', 'B');
-
-               //Add title
-               $p->Title2('II.2 Sur le non-respect du principe de légalité');
-
-               //Add title
-               $p->Title3('(i) En droit');
-
-               //Add text
-               $p->Body('Le droit pénal français est fondé sur le principe fondamental de la légalité des délits et des peines selon lequel quiconque ne peut être condamné en l\'absence d\'un texte clair et précis.
-
-Ce principe est au demeurant consacré par l\'article 8 de la Déclaration des droits de l\'Homme et du Citoyen de 1789 et a donc une valeur constitutionnelle.
-
-Plus encore, l\'article 111-3 du code pénal dispose :');
-
-               //Add text
-               $p->Quoted('« Nul ne peut être puni pour un crime ou pour un délit dont les éléments ne sont pas définis par la loi, ou pour une contravention dont les éléments ne sont pas définis par le règlement.
-
-Nul ne peut être puni d\'une peine qui n\'est pas prévue par la loi, si l\'infraction est un crime ou un délit, ou par le règlement, si l\'infraction est une contravention. »');
-
-               //Add text
-               $p->Body('Il en découle que chaque justiciable doit être en mesure de connaître non seulement les textes prévoyant l\'incrimination d\'un comportement déterminé mais également les textes fondant les peines applicables à l\'infraction visée.', 'U');
-
-               //Add text
-               $p->Body('En matière de contraventions, l\'article A37-4 du Code de procédure pénale prévoit :');
-
-               //Add text
-               $p->Quoted('« Les caractéristiques de l\'avis de contravention mentionné à l\'article A.37-1 sont les suivantes :
-I. Sur la partie gauche sont portées les mentions relatives au service verbalisateur, à la nature, au lieu et à la date de la contravention ainsi que les références des textes réprimant ladite contravention et, le cas échéant, sont précisés les éléments d\'identification du véhicule et l\'obligation de procéder à l\'échange du permis de conduire. »');
-
-               //Add text
-               $p->Body('Ainsi, le Code de procédure pénale exige, comme condition de recevabilité et conformément au principe de légalité, que les textes répressifs soient mentionnés à l\'acte de contravention.', 'B');
-
-               //Add title
-               $p->Title3('(ii) En fait');
-
-               //Add text
-               $p->Body('En prévention, l\'avis de contravention précité mentionne en prévention l\'article L.3131-15 §I 1° du Code de la santé publique :');
-
-               //Add text
-               $p->Quoted('« I.- Dans les circonscriptions territoriales où l\'état d\'urgence sanitaire est déclaré, le Premier ministre peut, par décret réglementaire pris sur le rapport du ministre chargé de la santé, aux seules fins de garantir la santé publique :
-
-1° Réglementer ou interdire la circulation des personnes et des véhicules et réglementer l\'accès aux moyens de transport et les conditions de leur usage ;
-[...] »');
-
-               //Add text
-               $p->Body('Ainsi que l\'article L.3131-13 du Code de la santé publique, sans référence à un alinéa en particulier :');
-
-               //Add text
-               $p->Quoted('« L\'état d\'urgence sanitaire est déclaré par décret en conseil des ministres pris sur le rapport du ministre chargé de la santé. Ce décret motivé détermine la ou les circonscriptions territoriales à l\'intérieur desquelles il entre en vigueur et reçoit application. Les données scientifiques disponibles sur la situation sanitaire qui ont motivé la décision sont rendues publiques.
-
-L\'Assemblée nationale et le Sénat sont informés sans délai des mesures prises par le Gouvernement au titre de l\'état d\'urgence sanitaire. L\'Assemblée nationale et le Sénat peuvent requérir toute information complémentaire dans le cadre du contrôle et de l\'évaluation de ces mesures.
-
-La prorogation de l\'état d\'urgence sanitaire au-delà d\'un mois ne peut être autorisée que par la loi, après avis du comité de scientifiques prévu à l\'article L. 3131-19. »');
-
-               //Add text
-               $p->Body('Sont également visées, les dispositions de l\'article 3131-16 alinéa 2, desquelles il ressort :');
-
-               //Add text
-               $p->Quoted('« Dans les mêmes conditions, le ministre chargé de la santé peut prescrire toute mesure individuelle nécessaire à l\'application des mesures prescrites par le Premier ministre en application des 1° à 9° du I de l\'article L. 3131-15. ».');
-
-               //Add text
-               $p->Body('Les dispositions de l\'article 3131-17 §1 également visées par l\'avis de contravention prévoient quant à elles que :');
-
-               //Add text
-               $p->Quoted('« I. - Lorsque le Premier ministre ou le ministre chargé de la santé prennent des mesures mentionnées aux articles L. 3131-15 et L. 3131-16, ils peuvent habiliter le représentant de l\'Etat territorialement compétent à prendre toutes les mesures générales ou individuelles d\'application de ces dispositions.
-
-Lorsque les mesures prévues aux 1°, 2° et 5° à 9° du I de l\'article L. 3131-15 et à l\'article L. 3131-16 doivent s\'appliquer dans un champ géographique qui n\'excède pas le territoire d\'un département, les autorités mentionnées aux mêmes articles L. 3131-15 et L. 3131-16 peuvent habiliter le représentant de l\'Etat dans le département à les décider lui-même. Les décisions sont prises par ce dernier après avis du directeur général de l\'agence régionale de santé. ».');
-
-               //Add text
-               $p->Body('Outre ces dispositions, est également visé l\'article 4 du décret 2020-1310 du 29 octobre 2020, en vigueur à la date des faits :');
-
-               //Add text
-               //XXX: https://www.legifrance.gouv.fr/loda/id/LEGIARTI000043330006/2021-04-04/
-               $p->Quoted('I.-Tout déplacement de personne hors de son lieu de résidence est interdit entre 19 heures et 6 heures du matin à l\'exception des déplacements pour les motifs suivants, en évitant tout regroupement de personnes :
-
-1° Déplacements à destination ou en provenance :
-
-a) Du lieu d\'exercice ou de recherche d\'une activité professionnelle et déplacements professionnels ne pouvant être différés ;
-
-b) Des établissements ou services d\'accueil de mineurs, d\'enseignement ou de formation pour adultes mentionnés aux articles 32 à 35 du présent décret ;
-
-c) Du lieu d\'organisation d\'un examen ou d\'un concours ;
-
-2° Déplacements pour des consultations, examens, actes de prévention et soins ne pouvant être assurés à distance ou pour l\'achat de produits de santé ;
-
-3° Déplacements pour motif familial impérieux, pour l\'assistance aux personnes vulnérables ou précaires ou pour la garde d\'enfants ;
-
-4° Déplacements des personnes en situation de handicap et, le cas échéant, de leur accompagnant ;
-
-5° Déplacements pour répondre à une convocation judiciaire ou administrative ou pour se rendre chez un professionnel du droit pour un acte ou une démarche qui ne peuvent être réalisés à distance ;
-
-6° Déplacements pour participer à des missions d\'intérêt général sur demande de l\'autorité administrative ;
-
-7° Déplacements liés à des transferts ou transits vers ou depuis des gares ou aéroports dans le cadre de déplacements de longue distance relevant de l\'un des motifs mentionnés au présent article ;
-
-8° Déplacements brefs, dans un rayon maximal d\'un kilomètre autour du domicile pour les besoins des animaux de compagnie.
-
-II.-Tout déplacement de personne hors de son lieu de résidence est interdit entre 6 heures et 19 heures à l\'exception des déplacements pour les motifs mentionnés au I et les motifs suivants, en évitant tout regroupement de personnes :
-
-1° Déplacements pour effectuer des achats de fournitures nécessaires à l\'activité professionnelle ou pour des livraisons à domicile ;
-
-2° Déplacements pour effectuer des achats de première nécessité, des retraits de commandes ou pour les besoins de prestations de services qui ne sont pas interdites en application des chapitres 1er et 3 du titre IV du présent décret ;
-
-3° Déplacements liés à un déménagement résultant d\'un changement de domicile et déplacements indispensables à l\'acquisition ou à la location d\'une résidence principale, insusceptibles d\'être différés ;
-
-4° Déplacements, dans un rayon maximal de dix kilomètres autour du domicile, liés soit à la promenade, soit à l\'activité physique individuelle des personnes, à l\'exclusion de toute pratique sportive collective ;
-
-5° Déplacements pour se rendre dans un service public, pour un acte ou une démarche qui ne peuvent être réalisés à distance ;
-
-6° Déplacements à destination ou en provenance d\'un lieu de culte ;
-
-7° Participation à des rassemblements, réunions ou activités sur la voie publique ou dans un lieu ouvert au public qui ne sont pas interdits en application de l\'article 3.
-
-II bis.-Les déplacements mentionnés aux 2°, 5°, 6° du II, ainsi que ceux mentionnés à son 7° lorsqu\'ils ne relèvent pas du II de l\'article 3, s\'effectuent dans les limites du département de résidence de la personne ou, en dehors de celui-ci, dans un périmètre de 30 kilomètres autour de son domicile.
-
-III.-Les personnes souhaitant bénéficier de l\'une des exceptions mentionnées aux I et II se munissent, lors de leurs déplacements hors de leur domicile, d\'un document leur permettant de justifier que le déplacement considéré entre dans le champ de l\'une de ces exceptions.
-
-Les interdictions de déplacement mentionnées aux I et II ne peuvent faire obstacle à l\'exercice d\'une activité professionnelle sur la voie publique dont il est justifié dans les conditions prévues à l\'alinéa précédent.
-
-IV.-Le représentant de l\'Etat dans le département est habilité à adopter des mesures plus restrictives en matière de trajets et déplacements des personnes lorsque les circonstances locales l\'exigent. Toutefois, dans les collectivités mentionnées à l\'article 72-3 de la Constitution, sous réserve que le présent décret leur soit applicable en vertu des dispositions de l\'article 55, le représentant de l\'Etat est habilité à prendre des mesures d\'interdiction proportionnées à l\'importance du risque de contamination en fonction des circonstances locales, après avis de l\'autorité compétente en matière sanitaire, notamment en les limitant à certaines parties du territoire.');
-
-               //Add text
-               $p->Body('Enfin, l\'avis de contravention mentionne les dispositions de l\'article 4-1 du décret du 29 octobre 2020, à savoir : ');
-
-               //Add text
-               $p->Quoted('Dans les cas où le lieu d\'exercice de l\'activité professionnelle est le domicile du client, les déplacements mentionnés au a du 1° du I de l\'article 4 ne sont, sauf intervention urgente, livraison ou lorsqu\'ils ont pour objet l\'assistance à des personnes vulnérables ou précaires ou la garde d\'enfants, autorisés qu\'entre 6 heures et 19 heures.');
-
-               //Add text
-               $p->Body('En répression, il est renvoyé à l\'article L.3136-1 du Code de la santé publique en son 3ème alinéa, dont le contenu est le suivant :');
-
-               //Add text
-               $p->Quoted('« La violation des autres interdictions ou obligations édictées en application des articles L. 3131-1 et L. 3131-15 à L. 3131-17 est punie de l\'amende prévue pour les contraventions de la quatrième classe. Cette contravention peut faire l\'objet de la procédure de l\'amende forfaitaire prévue à l\'article 529 du code de procédure pénale. Si cette violation est constatée à nouveau dans un délai de quinze jours, l\'amende est celle prévue pour les contraventions de la cinquième classe. ».');
-
-               //Add text
-               $p->Body('Cet alinéa 3 mentionne successivement l\'amende prévue pour les contraventions de la quatrième classe, ainsi que celle prévue pour la cinquième classe en cas de récidive dans un délai de 15 jours.');
-
-               //Add text
-               $p->Body('En aucun cas, il n\'est précisé la catégorie de contravention applicable à ma situation spécifique.
-
-Plus encore, outre la confusion générée par la référence à deux classes de contravention, en aucun cas l\'article précité, ou l\'article 529 du Code de procédure pénale ne fixent le montant de l\'amende forfaitaire à laquelle je suis condamné.', 'BU');
-
-               //Add text
-               $p->Body('En effet, cette information ressort de l\'article R. 49 du Code de procédure pénale, qui dispose :');
-
-               //Add text
-               $p->Quoted('« Le montant de l\'amende forfaitaire prévue par l\'article 529 est fixé ainsi qu\'il suit : [...]
-
-5° 135 euros pour les contraventions de la 4e classe
-
-6° 200 euros pour les contraventions de la 5e classe ».');
-
-               //Add text
-               $p->Body('Or, cet article n\'est nullement mentionné à l\'avis de contravention reçu !
-
-Par conséquent, l\'avis de contravention dressé à mon encontre est entaché d\'irrégularité.', 'B');
-
-               //Add title
-               $p->Title1('III/ EN TOUT ETAT DE CAUSE, SUR L\'ABSENCE DE CARACTERISATION DE L\'INFRACTION QUI M\'EST REPROCHEE');
-
-               //Add text
-               $p->Body('L\'avis de contravention ne porte pas mention des circonstances exactes de commission de l\'infraction.
-
-(i) Pourtant, l\'article 537 du code de procédure pénale dispose que :');
-
-               //Add text
-               $p->Quoted('« Les contraventions sont prouvées soit par procès-verbaux ou rapports, soit par témoins à défaut de rapports et procès-verbaux, ou à leur appui.
-Sauf dans les cas où la loi en dispose autrement, les procès-verbaux ou rapports établis par les officiers et agents de police judiciaire et les agents de police judiciaire adjoints, ou les fonctionnaires ou agents chargés de certaines fonctions de police judiciaire auxquels la loi a attribué le pouvoir de constater les contraventions, font foi jusqu\'à preuve contraire.
-La preuve contraire ne peut être rapportée que par écrit ou par témoins. »');
-
-               //Add text
-               $p->Body('Par ailleurs, l\'article 429 du code de procédure pénale, en son alinéa 1er, prévoit que :');
-
-               //Add text
-               $p->Quoted('« Tout procès-verbal ou rapport n\'a de valeur probante que s\'il est régulier en la forme, si son auteur a agi dans l\'exercice de ses fonctions et a rapporté sur une matière de sa compétence ce qu\'il a vu, entendu ou constaté personnellement.
-[...] ».');
-
-               //Add text
-               $p->Body('La matérialité doit ainsi être constatée dans le procès-verbal de constatation.
-
-(ii) Concernant le couvre-feu, l\'article 4 du décret du 29 octobre 2020, qui pose l\'interdiction générale de déplacement entre 18h et 6h prévoit également expressément des exceptions. 
-
-Ces exceptions sont limitativement listées par le décret. 
-
-En outre, l\'article 4 du décret du 29 octobre 2020 dispose au titre du III desdites dispositions :');
-
-               //Add text
-               $p->Quoted('« Les personnes souhaitant bénéficier de l\'une des exceptions mentionnées aux I et II se munissent, lors de leurs déplacements hors de leur domicile, d\'un document leur permettant de justifier que le déplacement considéré entre dans le champ de l\'une de ces exceptions. ».');
-
-               //Add text
-               $p->Body('Il ressort de la combinaison de ces dispositions qu\'il est possible de se déplacer entre 19 heures et 6 heures, à condition d\'être muni d\'un document permettant de justifier que le déplacement entre dans le champ de l\'une des exceptions.', 'B');
-
-               //Add title
-               $p->Title3('(iii) En l\'espèce, l\'avis de contravention indique que l\'infraction qui m\'est reprochée, est :');
-
-               //Add text
-               $p->Quoted('« CIRCULATION A UNE HEURE INTERDITE (...) ».', 'BI');
-
-               //Add text
-               $p->Body('Dès lors, il est patent qu\'il m\'est reproché de m\'être déplacé entre 19 heures et 6 heures du matin.');
-
-               //Add text
-               $p->Body('Ainsi, c\'est sur le fondement du principe d\'interdiction générale de déplacement que j\'ai été verbalisé.', 'BU');
-
-               //Add text
-               $p->Body('Or, force est de constater que l\'avis de contravention ne fait aucunement mention de l\'existence même des exceptions et encore moins de l\'absence d\'un document justificatif.', 'B');
-
-               //Add text
-               $p->Body('Il est donc impossible de déterminer si mon déplacement entrait dans le cadre d\'une des exceptions prévues par le décret du 29 octobre 2020, et si j\'étais en possession du justificatif y afférent. 
-
-Pour caractériser l\'infraction, l\'agent verbalisateur aurait dû préciser les raisons pour lesquelles il a jugé mon déplacement interdit malgré les dispositions relatives aux exceptions. ');
-
-               //Add text
-               $p->Body('En s\'abstenant de mentionner que mon déplacement n\'était pas justifié, l\'infraction qui m\'est reprochée ne saurait être caractérisée.', 'B');
-
-               //Add text
-               $p->Body('A tout point de vue, l\'avis de contravention reçu souffre de plusieurs manquements graves de base légale à savoir :', 'B');
-
-               //Add text
-               $p->Quoted('- L\'avis de contravention est dépourvu de base légale puisque l\'infraction prétendument commise n\'est pas visée par un texte de répression ;
-
-- A considérer qu\'il soit besoin d\'examiner le contenu de l\'avis de contravention, il devra être considéré qu\'au regard du principe de légalité, lequel a pour corollaire le principe de légalité des peines, les textes de prévention ne sont pas correctement visés.
-
-- Enfin, force est de constater que l\'infraction qui m\'est reprochée n\'est aucunement caractérisée.', 'BU');
-
-               //Add text
-               $p->Body('Or, en application des principes fondamentaux et constitutionnels, un fait ne peut être réprimé pénalement qu\'en vertu d\'une disposition pénale suffisamment précise et claire, et ce afin notamment d\'exclure tout arbitraire dans le prononcé des peines.');
-
-               //Add text
-               $p->Body('Par conséquent, cette condamnation pénale constitue une violation des principes essentiels rappelés.', 'BU');
-
-               //Add text
-               $p->Body('Pour l\'ensemble de ces raisons, je vous remercie, Madame ou Monsieur l\'Officier du Ministère Public, de faire droit à cette requête en me confirmant que vous renoncez à toute poursuite du chef de la contravention contestée et, le cas échéant, vous invite, à me convoquer à une prochaine audience.
-
-Vous remerciant de l\'accueil et l\'attention que vous réserverez à la présente,
-
-Je vous prie d\'agréer, Madame, Monsieur l\'Officier du Ministère Public, l\'expression de mes sentiments distingués.');
-
-               //Add text
-               $p->Body('Signé, '.$civility.' '.$forename.' '.$surname);
-
-               //Close pdf
-               $p->Close();
-
-               //Return pdf
-               return $p->Output('S');
-       }
-
-       //Set multi member
-       public function Multi($text, $x = 0, $style = '', $size = 9, $line = 4, $align = 'J') {
-               //Set font
-               $this->SetFont('Times', $style, $size);
-
-               //Set X
-               $this->SetX($this->GetX() + $x);
-
-               //Add text
-               $this->MultiCell(0, 4, utf8_decode($text), 0, $align);
-
-               //Jump line
-               $this->Ln($line);
-       }
-
-       //Set Title1 member
-       public function Title1($text) {
-               //Jump line
-               $this->Ln(4);
-
-               //Set font
-               $this->SetFont('Times', 'B', 12);
-
-               //Add text
-               $this->MultiCell(0, 4, utf8_decode($text), 0, 'L');
-
-               //Jump line
-               $this->Ln(4);
-       }
-
-       //Set Title2 member
-       public function Title2($text) {
-               //Set font
-               $this->SetFont('Times', 'BU', 11);
-
-               //Set X
-               $this->SetX($this->GetX() + 2);
-
-               //Add text
-               $this->MultiCell(0, 4, utf8_decode($text), 0, 'L');
-
-               //Jump line
-               $this->Ln(4);
-       }
-
-       //Set Title3 member
-       public function Title3($text) {
-               //Set font
-               $this->SetFont('Times', 'U', 10);
-
-               //Set X
-               $this->SetX($this->GetX() + 4);
-
-               //Add text
-               $this->MultiCell(0, 4, utf8_decode($text), 0, 'L');
-
-               //Jump line
-               $this->Ln(4);
-       }
-
-       //Set Body member
-       public function Body($text, $style = '') {
-               //Set font
-               $this->SetFont('Times', $style, 9);
-
-               //Add text
-               $this->MultiCell(0, 3, utf8_decode($text), 0, 'J');
-
-               //Jump line
-               $this->Ln(2);
-       }
-
-       //Set Quoted member
-       public function Quoted($text, $style = 'I') {
-               //Set font
-               $this->SetFont('Times', $style, 9);
-
-               //Set X
-               $this->SetX($this->GetX() + 6);
-
-               //Add text
-               $this->MultiCell(0, 3, utf8_decode($text), 0, 'J');
-
-               //Jump line
-               $this->Ln(2);
-       }
-
-       //Set footer hook
-       public function Footer() {
-               //Set at bottom
-               $this->SetY(-15);
-               //Set font
-               $this->SetFont('Times', 'I', 8);
-               //Set page number
-               $this->Cell(0, 10, 'Page '.$this->PageNo().'/{nb}', 0, 0, 'R');
-       }
-
-       //Replace close member
-       public function Close() {
-               //Replace alias nb pages
-               $this->AliasNbPages();
-
-               //Call parent close
-               parent::Close();
-       }
-}
index b105d39bef4817a6404c21334495c1c89dd2804e..e1bb77b65664c6049e6a47d79cf7239e581cd0bd 100644 (file)
 
 namespace Rapsys\AirBundle;
 
-use Symfony\Component\DependencyInjection\Container;
+use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
 use Symfony\Component\HttpKernel\Bundle\Bundle;
 
-class RapsysAirBundle extends Bundle{
+/**
+ * {@inheritdoc}
+ */
+class RapsysAirBundle extends Bundle {
+       /**
+        * {@inheritdoc}
+        */
+       public function getContainerExtension(): ?ExtensionInterface {
+               //Return created container extension
+               return $this->createContainerExtension();
+       }
+
        /**
         * Return bundle alias
         *
         * @return string The bundle alias
         */
-    public static function getAlias(): string {
+       public static function getAlias(): string {
                //With namespace
                if ($npos = strrpos(static::class, '\\')) {
                        //Set name pos
@@ -40,7 +51,17 @@ class RapsysAirBundle extends Bundle{
                        $bpos = strlen(static::class) - $npos;
                }
 
-               //Return underscored lowercase bundle alias
-               return Container::underscore(substr(static::class, $npos, $bpos));
-    }
+               //Return lowercase bundle alias
+               return strtolower(substr(static::class, $npos, $bpos));
+       }
+
+       /**
+        * Return bundle version
+        *
+        * @return string The bundle version
+        */
+       public static function getVersion(): string {
+               //Return version
+               return '0.5.0';
+       }
 }
diff --git a/Repository.php b/Repository.php
new file mode 100644 (file)
index 0000000..ef35965
--- /dev/null
@@ -0,0 +1,127 @@
+<?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;
+
+use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\EntityRepository;
+use Doctrine\ORM\Mapping\ClassMetadata;
+use Symfony\Component\Routing\RouterInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
+
+use Rapsys\PackBundle\Util\SluggerUtil;
+
+/**
+ * Repository
+ *
+ * {@inheritdoc}
+ */
+class Repository extends EntityRepository {
+       /**
+        * The table keys array
+        *
+        * @var array
+        */
+       protected array $tableKeys;
+
+       /**
+        * The table values array
+        *
+        * @var array
+        */
+       protected array $tableValues;
+
+       /**
+        * Initializes a new LocationRepository instance
+        *
+        * @param EntityManagerInterface $manager The EntityManagerInterface instance
+        * @param ClassMetadata $class The ClassMetadata instance
+        * @param RouterInterface $router The router instance
+        * @param SluggerUtil $slugger The SluggerUtil instance
+        * @param TranslatorInterface $translator The TranslatorInterface instance
+        * @param string $locale The current locale
+        * @param array $languages The languages list
+        */
+       public function __construct(protected EntityManagerInterface $manager, protected ClassMetadata $class, protected RouterInterface $router, protected SluggerUtil $slugger, protected TranslatorInterface $translator, protected string $locale, protected array $languages) {
+               //Call parent constructor
+               parent::__construct($manager, $class);
+
+               //Get quote strategy
+               $qs = $this->manager->getConfiguration()->getQuoteStrategy();
+               $dp = $this->manager->getConnection()->getDatabasePlatform();
+
+               //Set quoted table names
+               //XXX: this allow to make this code table name independent
+               //XXX: remember to place longer prefix before shorter to avoid strange replacings
+               //XXX: entity short syntax removed in doctrine/persistence 3.x: https://github.com/doctrine/orm/issues/8818
+               $tables = [
+                       'Rapsys\AirBundle\Entity\UserDance' => $qs->getJoinTableName($manager->getClassMetadata('Rapsys\AirBundle\Entity\User')->getAssociationMapping('dances'), $manager->getClassMetadata('Rapsys\AirBundle\Entity\User'), $dp),
+                       'Rapsys\AirBundle\Entity\UserGroup' => $qs->getJoinTableName($manager->getClassMetadata('Rapsys\AirBundle\Entity\User')->getAssociationMapping('groups'), $manager->getClassMetadata('Rapsys\AirBundle\Entity\User'), $dp),
+                       'Rapsys\AirBundle\Entity\UserLocation' => $qs->getJoinTableName($manager->getClassMetadata('Rapsys\AirBundle\Entity\User')->getAssociationMapping('locations'), $manager->getClassMetadata('Rapsys\AirBundle\Entity\User'), $dp),
+                       'Rapsys\AirBundle\Entity\UserSubscription' => $qs->getJoinTableName($manager->getClassMetadata('Rapsys\AirBundle\Entity\User')->getAssociationMapping('subscriptions'), $manager->getClassMetadata('Rapsys\AirBundle\Entity\User'), $dp),
+                       'Rapsys\AirBundle\Entity\Application' => $qs->getTableName($manager->getClassMetadata('Rapsys\AirBundle\Entity\Application'), $dp),
+                       'Rapsys\AirBundle\Entity\Civility' => $qs->getTableName($manager->getClassMetadata('Rapsys\AirBundle\Entity\Civility'), $dp),
+                       'Rapsys\AirBundle\Entity\Country' => $qs->getTableName($manager->getClassMetadata('Rapsys\AirBundle\Entity\Country'), $dp),
+                       'Rapsys\AirBundle\Entity\Dance' => $qs->getTableName($manager->getClassMetadata('Rapsys\AirBundle\Entity\Dance'), $dp),
+                       'Rapsys\AirBundle\Entity\GoogleCalendar' => $qs->getTableName($manager->getClassMetadata('Rapsys\AirBundle\Entity\GoogleCalendar'), $dp),
+                       'Rapsys\AirBundle\Entity\GoogleToken' => $qs->getTableName($manager->getClassMetadata('Rapsys\AirBundle\Entity\GoogleToken'), $dp),
+                       'Rapsys\AirBundle\Entity\Group' => $qs->getTableName($manager->getClassMetadata('Rapsys\AirBundle\Entity\Group'), $dp),
+                       'Rapsys\AirBundle\Entity\Location' => $qs->getTableName($manager->getClassMetadata('Rapsys\AirBundle\Entity\Location'), $dp),
+                       'Rapsys\AirBundle\Entity\Session' => $qs->getTableName($manager->getClassMetadata('Rapsys\AirBundle\Entity\Session'), $dp),
+                       'Rapsys\AirBundle\Entity\Slot' => $qs->getTableName($manager->getClassMetadata('Rapsys\AirBundle\Entity\Slot'), $dp),
+                       'Rapsys\AirBundle\Entity\Snippet' => $qs->getTableName($manager->getClassMetadata('Rapsys\AirBundle\Entity\Snippet'), $dp),
+                       'Rapsys\AirBundle\Entity\User' => $qs->getTableName($manager->getClassMetadata('Rapsys\AirBundle\Entity\User'), $dp),
+                       //Set accuweather max number of daily pages
+                       ':accudaily' => 12,
+                       //Set accuweather max number of hourly pages
+                       ':accuhourly' => 3,
+                       //Set guest delay
+                       ':guestdelay' => 2 * 24 * 3600,
+                       //Set regular delay
+                       ':regulardelay' => 3 * 24 * 3600,
+                       //Set senior delay
+                       ':seniordelay' => 4 * 24 * 3600,
+                       //Set guest group id
+                       ':guestid' => 2,
+                       //Set regular group id
+                       ':regularid' => 3,
+                       //Set senior group id
+                       ':seniorid' => 4,
+                       //Set afternoon slot id
+                       ':afternoonid' => 2,
+                       //Set evening slot id
+                       ':eveningid' => 3,
+                       //Set after slot id
+                       ':afterid' => 4,
+                       //XXX: days since last session after which guest regain normal priority
+                       ':guestwait' => 30,
+                       //XXX: session count until considered at regular delay
+                       ':scount' => 5,
+                       //XXX: pn_ratio over which considered at regular delay
+                       ':pnratio' => 1,
+                       //XXX: tr_ratio diff over which considered at regular delay
+                       ':trdiff' => 5,
+                       //Set locale
+                       //XXX: or $manager->getConnection()->quote($this->locale) ???
+                       ':locale' => $dp->quoteStringLiteral($this->locale),
+                       //XXX: Set limit used to workaround mariadb subselect optimization
+                       ':limit' => PHP_INT_MAX,
+                       "\t" => '',
+                       "\n" => ' '
+               ];
+
+               //Set quoted table name keys
+               $this->tableKeys = array_keys($tables);
+
+               //Set quoted table name values
+               $this->tableValues = array_values($tables);
+       }
+}
index 3112d1479792348fc24237bbabdb57df6f32b7ae..57acc37547f9e7ca832670fa4570a597274b8b0c 100644 (file)
@@ -15,7 +15,7 @@ class ApplicationRepository extends \Doctrine\ORM\EntityRepository {
        public function findOneBySessionUser($session, $user) {
                //Fetch article
                $ret = $this->getEntityManager()
-                       ->createQuery('SELECT a FROM RapsysAirBundle:Application a WHERE (a.session = :session AND a.user = :user)')
+                       ->createQuery('SELECT a FROM Rapsys\AirBundle\Entity\Application a WHERE (a.session = :session AND a.user = :user)')
                        ->setParameter('session', $session)
                        ->setParameter('user', $user)
                        ->getSingleResult();
index 8296ccd14e9e3cdfabb7389dadb69b2ab8891e80..251a1baa21913829941c783703a620248b93ab5b 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\Repository;
 
-use Symfony\Component\Translation\TranslatorInterface;
+use Doctrine\ORM\AbstractQuery;
 use Doctrine\ORM\Query\ResultSetMapping;
 
+use Rapsys\AirBundle\Repository;
+
 /**
  * DanceRepository
  */
-class DanceRepository extends \Doctrine\ORM\EntityRepository {
+class DanceRepository extends Repository {
        /**
-        * Find dances by user id
+        * Find dances indexed by id
         *
-        * @param $id The user id
-        * @return array The user dances
+        * @return array The dances
+        */
+       public function findAllIndexed(): array {
+               //Set the request
+               $req = <<<SQL
+SELECT
+       d.id,
+       d.name,
+       d.type
+FROM Rapsys\AirBundle\Entity\Dance AS d
+SQL;
+
+               //Replace bundle entity name by table name
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
+
+               //Get result set mapping instance
+               //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
+               $rsm = new ResultSetMapping();
+
+               //Declare all fields
+               //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
+               //addScalarResult($sqlColName, $resColName, $type = 'string');
+               $rsm->addEntityResult('Rapsys\AirBundle\Entity\Dance', 'd')
+                       ->addFieldResult('d', 'id', 'id')
+                       ->addFieldResult('d', 'name', 'name')
+                       ->addFieldResult('d', 'type', 'type')
+                       ->addIndexByColumn('d', 'id');
+
+               //Return return
+               return $this->_em
+                       ->createNativeQuery($req, $rsm)
+                       ->getResult();
+       }
+
+       /**
+        * Find dance choices as array
+        *
+        * @return array The dance choices
+        */
+       public function findChoicesAsArray(): array {
+               //Set the request
+               $req = <<<SQL
+SELECT
+       d.name,
+       GROUP_CONCAT(d.id ORDER BY d.id SEPARATOR "\\n") AS ids,
+       GROUP_CONCAT(d.type ORDER BY d.id SEPARATOR "\\n") AS types
+FROM Rapsys\AirBundle\Entity\Dance AS d
+GROUP BY d.name
+ORDER BY d.name
+SQL;
+
+               //Replace bundle entity name by table name
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
+
+               //Get result set mapping instance
+               //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
+               $rsm = new ResultSetMapping();
+
+               //Declare all fields
+               //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
+               //addScalarResult($sqlColName, $resColName, $type = 'string');
+               $rsm->addScalarResult('name', 'name', 'string')
+                       ->addScalarResult('ids', 'ids', 'string')
+                       ->addScalarResult('types', 'types', 'string')
+                       ->addIndexByScalar('name');
+
+               //Get result
+               $result = $this->_em
+                       ->createNativeQuery($req, $rsm)
+                       ->getArrayResult();
+
+               //Set return
+               $return = [];
+
+               //Iterate on each name
+               foreach($result as $name) {
+                       //Set types
+                       $types = [];
+
+                       //Explode ids
+                       $name['ids'] = explode("\n", $name['ids']);
+
+                       //Explode types
+                       $name['types'] = explode("\n", $name['types']);
+
+                       //Iterate on each type
+                       foreach($name['ids'] as $k => $id) {
+                               //Add to types
+                               $types[$this->translator->trans($name['types'][$k]).' ('.$id.')'] = intval($id);
+                       }
+
+                       //Add to return
+                       $return[$this->translator->trans($name['name'])] = $types;
+               }
+
+               //Return return
+               return $return;
+       }
+
+       /**
+        * Find dances ids by nametype
+        *
+        * @param array $nametype The nametype filter
+        * @return array The dance ids
         */
-       public function findByUserId($userId) {
-               //Get entity manager
-               $em = $this->getEntityManager();
+       public function findIdByNameTypeAsArray(array $nametype): array {
+               //Set the request
+               $req = <<<SQL
+SELECT
+       d.id
+FROM Rapsys\AirBundle\Entity\Dance AS d
+WHERE CONCAT_WS(' ', d.name, d.type) IN (:nametype)
+ORDER BY d.name, d.type
+SQL;
 
-               //Get quote strategy
-               $qs = $em->getConfiguration()->getQuoteStrategy();
-               $dp = $em->getConnection()->getDatabasePlatform();
+               //Replace bundle entity name by table name
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
 
-               //Get quoted table names
-               //XXX: this allow to make this code table name independent
-               $tables = [
-                       'RapsysAirBundle:UserDance' => $qs->getJoinTableName($em->getClassMetadata('RapsysAirBundle:User')->getAssociationMapping('dances'), $em->getClassMetadata('RapsysAirBundle:User'), $dp),
-                       'RapsysAirBundle:Dance' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Dance'), $dp)
-               ];
+               //Get result set mapping instance
+               //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
+               $rsm = new ResultSetMapping();
+
+               //Declare all fields
+               //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
+               //XXX: we don't use a result set as we want to translate group and civility
+               $rsm->addScalarResult('id', 'id', 'integer');
 
+               //Return result
+               return $this->_em
+                       ->createNativeQuery($req, $rsm)
+                       ->setParameter('nametype', $nametype)
+                       //XXX: instead of array_column on the result
+                       ->getResult(AbstractQuery::HYDRATE_SCALAR_COLUMN);
+       }
+
+       /**
+        * Find dance names as array
+        *
+        * @return array The dance names
+        */
+       public function findNamesAsArray(): array {
+               //Set the request
+               $req = <<<SQL
+SELECT
+       d.name,
+       GROUP_CONCAT(d.id ORDER BY d.id SEPARATOR "\\n") AS ids,
+       GROUP_CONCAT(d.type ORDER BY d.id SEPARATOR "\\n") AS types,
+       MAX(d.updated) AS modified
+FROM Rapsys\AirBundle\Entity\Dance AS d
+GROUP BY d.name
+ORDER BY d.name
+SQL;
+
+               //Replace bundle entity name by table name
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
+
+               //Get result set mapping instance
+               //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
+               $rsm = new ResultSetMapping();
+
+               //Declare all fields
+               //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
+               //addScalarResult($sqlColName, $resColName, $type = 'string');
+               $rsm->addScalarResult('name', 'name', 'string')
+                       ->addScalarResult('ids', 'ids', 'string')
+                       ->addScalarResult('types', 'types', 'string')
+                       ->addScalarResult('modified', 'modified', 'datetime')
+                       ->addIndexByScalar('name');
+
+               //Get result
+               $result = $this->_em
+                       ->createNativeQuery($req, $rsm)
+                       ->getArrayResult();
+
+               //Set return
+               $return = [];
+
+               //Iterate on each name
+               foreach($result as $name) {
+                       //Set name slug
+                       $slug = $this->slugger->slug($tname = $this->translator->trans($name['name']));
+
+                       //Set types
+                       $types = [];
+
+                       //Explode ids
+                       $name['ids'] = explode("\n", $name['ids']);
+
+                       //Explode types
+                       $name['types'] = explode("\n", $name['types']);
+
+                       //Iterate on each type
+                       foreach($name['ids'] as $k => $id) {
+                               //Add to types
+                               $types[$this->slugger->short($name['types'][$k])] = [
+                                       'id' => $id,
+                                       'type' => $type = $this->translator->trans($name['types'][$k]),
+                                       'slug' => $stype = $this->slugger->slug($type),
+                                       'link' => $this->router->generate('rapsysair_dance_view', ['id' => $id, 'name' => $slug, 'type' => $stype])
+                               ];
+                       }
+
+                       //Add to return
+                       $return[$sname = $this->slugger->short($name['name'])] = [
+                               'name' => $tname,
+                               'slug' => $slug,
+                               'link' => $this->router->generate('rapsysair_dance_name', ['name' => $sname, 'dance' => $slug]),
+                               'types' => $types,
+                               'modified' => $name['modified']
+                       ];
+               }
+
+               //Return return
+               return $return;
+       }
+
+       /**
+        * Find dances by user id
+        *
+        * @param $id The user id
+        * @return array The user dances
+        */
+       public function findByUserId($userId): array {
                //Set the request
-               $req = 'SELECT d.id, d.title
-FROM RapsysAirBundle:UserDance AS ud
-JOIN RapsysAirBundle:Dance AS d ON (d.id = ud.dance_id)
+               $req = 'SELECT d.id, d.name, d.type
+FROM Rapsys\AirBundle\Entity\UserDance AS ud
+JOIN Rapsys\AirBundle\Entity\Dance AS d ON (d.id = ud.dance_id)
 WHERE ud.user_id = :uid';
 
                //Replace bundle entity name by table name
-               $req = str_replace(array_keys($tables), array_values($tables), $req);
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
 
                //Get result set mapping instance
                //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
                $rsm = new ResultSetMapping();
 
                //Declare result set for our request
-               $rsm->addEntityResult('RapsysAirBundle:Dance', 'd');
+               $rsm->addEntityResult('Rapsys\AirBundle\Entity\Dance', 'd');
                $rsm->addFieldResult('d', 'id', 'id');
-               $rsm->addFieldResult('d', 'title', 'title');
+               $rsm->addFieldResult('d', 'name', 'name');
+               $rsm->addFieldResult('d', 'type', 'type');
 
                //Send result
-               return $em
+               return $this->_em
                        ->createNativeQuery($req, $rsm)
                        ->setParameter('uid', $userId)
                        ->getResult();
diff --git a/Repository/GoogleTokenRepository.php b/Repository/GoogleTokenRepository.php
new file mode 100644 (file)
index 0000000..29b96d2
--- /dev/null
@@ -0,0 +1,178 @@
+<?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\Repository;
+
+use Doctrine\ORM\AbstractQuery;
+use Doctrine\ORM\Query\ResultSetMapping;
+
+use Rapsys\AirBundle\Repository;
+
+/**
+ * GoogleTokenRepository
+ */
+class GoogleTokenRepository extends Repository {
+       /**
+        * Find google tokens indexed by id
+        *
+        * @return array The google tokens array
+        */
+       public function findAllIndexed(): array {
+               //Set the request
+               $req = <<<SQL
+SELECT
+       b.tid,
+       b.gmail,
+       b.uid,
+       b.access,
+       b.refresh,
+       b.created,
+       b.expired,
+       b.cids,
+       b.cmails,
+       b.csummaries,
+       b.csynchronizeds,
+       b.dids,
+       GROUP_CONCAT(us.subscribed_id ORDER BY us.subscribed_id SEPARATOR "\\n") AS sids
+FROM (
+       SELECT
+               a.tid,
+               a.gmail,
+               a.uid,
+               a.access,
+               a.refresh,
+               a.created,
+               a.expired,
+               a.cids,
+               a.cmails,
+               a.csummaries,
+               a.csynchronizeds,
+               GROUP_CONCAT(ud.dance_id ORDER BY ud.dance_id SEPARATOR "\\n") AS dids
+       FROM (
+               SELECT
+                       t.id AS tid,
+                       t.mail AS gmail,
+                       t.user_id AS uid,
+                       t.access,
+                       t.refresh,
+                       t.created,
+                       t.expired,
+                       GROUP_CONCAT(c.id ORDER BY c.id SEPARATOR "\\n") AS cids,
+                       GROUP_CONCAT(c.mail ORDER BY c.id SEPARATOR "\\n") AS cmails,
+                       GROUP_CONCAT(c.summary ORDER BY c.id SEPARATOR "\\n") AS csummaries,
+                       GROUP_CONCAT(IFNULL(c.synchronized, 'NULL') ORDER BY c.id SEPARATOR "\\n") AS csynchronizeds
+               FROM Rapsys\AirBundle\Entity\GoogleToken AS t
+               JOIN Rapsys\AirBundle\Entity\GoogleCalendar AS c ON (c.google_token_id = t.id)
+               GROUP BY t.id
+               ORDER BY NULL
+       ) AS a
+       LEFT JOIN Rapsys\AirBundle\Entity\UserDance AS ud ON (ud.user_id = a.uid)
+) AS b
+LEFT JOIN Rapsys\AirBundle\Entity\UserSubscription AS us ON (us.user_id = b.uid)
+SQL;
+
+               //Replace bundle entity name by table name
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
+
+               //Get result set mapping instance
+               //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
+               $rsm = new ResultSetMapping();
+
+               //Declare all fields
+               //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
+               //addScalarResult($sqlColName, $resColName, $type = 'string');
+               $rsm
+                       ->addScalarResult('tid', 'tid', 'integer')
+                       ->addScalarResult('gmail', 'gmail', 'string')
+                       ->addScalarResult('uid', 'uid', 'integer')
+                       ->addScalarResult('access', 'access', 'string')
+                       ->addScalarResult('refresh', 'refresh', 'string')
+                       ->addScalarResult('created', 'created', 'datetime')
+                       ->addScalarResult('expired', 'expired', 'datetime')
+                       ->addScalarResult('cids', 'cids', 'string')
+                       ->addScalarResult('cmails', 'cmails', 'string')
+                       ->addScalarResult('csummaries', 'csummaries', 'string')
+                       ->addScalarResult('csynchronizeds', 'csynchronizeds', 'string')
+                       ->addScalarResult('dids', 'dids', 'string')
+                       ->addScalarResult('sids', 'sids', 'string')
+                       ->addIndexByScalar('tid');
+
+               //Set result array
+               $result = [];
+
+               //Get tokens
+               $tokens = $this->_em
+                       ->createNativeQuery($req, $rsm)
+                       ->getArrayResult();
+
+               //Iterate on tokens
+               foreach($tokens as $tid => $token) {
+                       //Set cids
+                       $cids = explode("\n", $token['cids']);
+
+                       //Set cmails
+                       $cmails = explode("\n", $token['cmails']);
+
+                       //Set csummaries
+                       $csummaries = explode("\n", $token['csummaries']);
+
+                       //Set csynchronizeds
+                       $csynchronizeds = array_map(function($v){return new \DateTime($v);}, explode("\n", $token['csynchronizeds']));
+
+                       //Set result
+                       $result[$tid] = [
+                               'id' => $tid,
+                               'mail' => $token['gmail'],
+                               'uid' => $token['uid'],
+                               'access' => $token['access'],
+                               'refresh' => $token['refresh'],
+                               'created' => $token['created'],
+                               'expired' => $token['expired'],
+                               'calendars' => [],
+                               'dances' => [],
+                               'subscriptions' => []
+                       ];
+
+                       //Iterate on calendars
+                       foreach($cids as $k => $cid) {
+                               $result[$tid]['calendars'][$cid] = [
+                                       'id' => $cid,
+                                       'mail' => $cmails[$k],
+                                       'summary' => $csummaries[$k],
+                                       'synchronized' => $csynchronizeds[$k]
+                               ];
+                       }
+
+                       //Set dids
+                       $dids = explode("\n", $token['dids']);
+
+                       //Iterate on dances
+                       foreach($dids as $k => $did) {
+                               $result[$tid]['dances'][$did] = [
+                                       'id' => $did
+                               ];
+                       }
+
+                       //Set sids
+                       $sids = explode("\n", $token['sids']);
+
+                       //Iterate on subscriptions
+                       foreach($sids as $k => $sid) {
+                               $result[$tid]['subscriptions'][$sid] = [
+                                       'id' => $sid
+                               ];
+                       }
+               }
+
+               //Return result
+               return $result;
+       }
+}
index 129a1763ee497dafc6b64bc339fbe20ab878fcc0..9b621ef3cafe2b073c511ee42867c2bd665bf8e4 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\Repository;
 
-use Symfony\Component\Translation\TranslatorInterface;
 use Doctrine\ORM\Query\ResultSetMapping;
 
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+use Symfony\Component\Routing\RouterInterface;
+
+use Rapsys\AirBundle\Repository;
+
 /**
  * LocationRepository
+ *
+ * @TODO: use new window function syntax https://mariadb.com/kb/en/window-functions-overview/ MAX(updated) OVER (PARTITION updated) AS modified ???
  */
-class LocationRepository extends \Doctrine\ORM\EntityRepository {
+class LocationRepository extends Repository {
        /**
-        * Find complementary locations by session id
+        * Find locations
         *
-        * @param $id The session id
-        * @return array The other locations
+        * @return array
         */
-       public function findComplementBySessionId($id) {
-               //Fetch complement locations
-               $ret = $this->getEntityManager()
-                         ->createQuery('SELECT l.id, l.title FROM RapsysAirBundle:Session s LEFT JOIN RapsysAirBundle:Session s2 WITH s2.id != s.id AND s2.slot = s.slot AND s2.date = s.date LEFT JOIN RapsysAirBundle:Location l WITH l.id != s.location AND (l.id != s2.location OR s2.location IS NULL) WHERE s.id = :sid GROUP BY l.id ORDER BY l.id')
-                       ->setParameter('sid', $id)
+       public function findAll(): array {
+               //Get all locations index by id
+               return $this->createQueryBuilder('location', 'location.id')->getQuery()->getResult();
+       }
+
+       /**
+        * Find locations as array
+        *
+        * @param DatePeriod $period The period
+        * @return array The locations array
+        */
+       public function findAllAsArray(\DatePeriod $period): array {
+               //Set the request
+               //TODO: ajouter pays ???
+               $req = <<<SQL
+SELECT
+       l.id,
+       l.title,
+       l.latitude,
+       l.longitude,
+       l.indoor,
+       l.updated
+FROM Rapsys\AirBundle\Entity\Location AS l
+LEFT JOIN Rapsys\AirBundle\Entity\Session AS s ON (l.id = s.location_id)
+GROUP BY l.id
+ORDER BY COUNT(IF(s.date BETWEEN :begin AND :end, s.id, NULL)) DESC, COUNT(s.id) DESC, l.id
+SQL;
+
+               //Replace bundle entity name by table name
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
+
+               //Get result set mapping instance
+               //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
+               $rsm = new ResultSetMapping();
+
+               //Declare all fields
+               //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
+               //addScalarResult($sqlColName, $resColName, $type = 'string');
+               $rsm->addScalarResult('id', 'id', 'integer')
+                       ->addScalarResult('title', 'title', 'string')
+                       ->addScalarResult('latitude', 'latitude', 'float')
+                       ->addScalarResult('longitude', 'longitude', 'float')
+                       ->addScalarResult('indoor', 'indoor', 'boolean')
+                       ->addScalarResult('count', 'count', 'integer')
+                       ->addScalarResult('updated', 'updated', 'datetime');
+
+               //Get result
+               $result = $this->_em
+                       ->createNativeQuery($req, $rsm)
+                       ->setParameter('begin', $period->getStartDate())
+                       ->setParameter('end', $period->getEndDate())
                        ->getArrayResult();
 
-               //Rekey array
-               $ret = array_column($ret, 'id', 'title');
+               //Set return
+               $return = [];
+
+               //Iterate on each city
+               foreach($result as $data) {
+                       //Add to return
+                       $return[] = [
+                               'id' => $data['id'],
+                               'title' => $title = $this->translator->trans($data['title']),
+                               'latitude' => $data['latitude'],
+                               'longitude' => $data['longitude'],
+                               'updated' => $data['updated'],
+                               //XXX: Useless ???
+                               'slug' => $location = $this->slugger->slug($title),
+                               'link' => $this->router->generate('rapsysair_location_view', ['id' => $data['id'], 'location' => $this->slugger->slug($location)])
+                       ];
+               }
 
-               return $ret;
+               //Return return
+               return $return;
        }
 
        /**
-        * Find translated location title sorted by date period
+        * Find cities as array
         *
-        * @param $translator The TranslatorInterface instance
-        * @param $period The date period
-        * @param $granted The session is granted
+        * @param DatePeriod $period The period
+        * @param int $count The session count
+        * @return array The cities array
         */
-       public function findTranslatedSortedByPeriod(TranslatorInterface $translator, $period, $userId = null) {
-               //Fetch sessions
-               $ret = $this->getEntityManager()
-                       ->createQuery(
-'SELECT l.id, l.title
-FROM RapsysAirBundle:Location l
-LEFT JOIN RapsysAirBundle:Session s WITH s.location = l.id AND s.date BETWEEN :begin AND :end
-LEFT JOIN RapsysAirBundle:Application a WITH a.id = s.application'.(!empty($userId)?' AND a.user = :uid':'').'
-GROUP BY l.id
-ORDER BY '.(!empty($userId)?'COUNT(a.id) DESC, ':'').'COUNT(s.id) DESC, l.id'
-                       )
+       public function findCitiesAsArray(\DatePeriod $period, int $count = 1): array {
+               //Set the request
+               $req = <<<SQL
+SELECT
+       SUBSTRING(a.zipcode, 1, 2) AS id,
+       a.city AS city,
+       ROUND(AVG(a.latitude), 6) AS latitude,
+       ROUND(AVG(a.longitude), 6) AS longitude,
+       GROUP_CONCAT(a.id ORDER BY a.pcount DESC, a.count DESC, a.id SEPARATOR "\\n") AS ids,
+       GROUP_CONCAT(a.title ORDER BY a.pcount DESC, a.count DESC, a.id SEPARATOR "\\n") AS titles,
+       GROUP_CONCAT(a.latitude ORDER BY a.pcount DESC, a.count DESC, a.id SEPARATOR "\\n") AS latitudes,
+       GROUP_CONCAT(a.longitude ORDER BY a.pcount DESC, a.count DESC, a.id SEPARATOR "\\n") AS longitudes,
+       GROUP_CONCAT(a.indoor ORDER BY a.pcount DESC, a.count DESC, a.id SEPARATOR "\\n") AS indoors,
+       GROUP_CONCAT(a.count ORDER BY a.pcount DESC, a.count DESC, a.id SEPARATOR "\\n") AS counts,
+       MAX(a.modified) AS modified
+FROM (
+       SELECT
+               l.id,
+               l.city,
+               l.title,
+               l.latitude,
+               l.longitude,
+               l.indoor,
+               GREATEST(l.created, l.updated, COALESCE(s.created, '1970-01-01'), COALESCE(s.updated, '1970-01-01')) AS modified,
+               l.zipcode,
+               COUNT(s.id) AS count,
+               COUNT(IF(s.date BETWEEN :begin AND :end, s.id, NULL)) AS pcount
+       FROM Rapsys\AirBundle\Entity\Location AS l
+       LEFT JOIN Rapsys\AirBundle\Entity\Session AS s ON (l.id = s.location_id)
+       GROUP BY l.id
+       ORDER BY NULL
+       LIMIT 0, :limit
+) AS a
+GROUP BY a.city, SUBSTRING(a.zipcode, 1, 2)
+ORDER BY a.city, a.zipcode
+SQL;
+
+               //Replace bundle entity name by table name
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
+
+               //Get result set mapping instance
+               //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
+               $rsm = new ResultSetMapping();
+
+               //Declare all fields
+               //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
+               //addScalarResult($sqlColName, $resColName, $type = 'string');
+               $rsm->addScalarResult('id', 'id', 'integer')
+                       ->addScalarResult('city', 'city', 'string')
+                       ->addScalarResult('latitude', 'latitude', 'float')
+                       ->addScalarResult('longitude', 'longitude', 'float')
+                       //XXX: is a string because of \n separator
+                       ->addScalarResult('ids', 'ids', 'string')
+                       //XXX: is a string because of \n separator
+                       ->addScalarResult('titles', 'titles', 'string')
+                       //XXX: is a string because of \n separator
+                       ->addScalarResult('latitudes', 'latitudes', 'string')
+                       //XXX: is a string because of \n separator
+                       ->addScalarResult('longitudes', 'longitudes', 'string')
+                       //XXX: is a string because of \n separator
+                       ->addScalarResult('indoors', 'indoors', 'string')
+                       //XXX: is a string because of \n separator
+                       ->addScalarResult('counts', 'counts', 'string')
+                       ->addScalarResult('modified', 'modified', 'datetime')
+                       ->addIndexByScalar('city');
+
+               //Get result
+               $result = $this->_em
+                       ->createNativeQuery($req, $rsm)
                        ->setParameter('begin', $period->getStartDate())
-                       ->setParameter('end', $period->getEndDate());
+                       ->setParameter('end', $period->getEndDate())
+                       ->getArrayResult();
+
+               //Set return
+               $return = [];
+
+               //Iterate on each city
+               foreach($result as $city => $data) {
+                       //Set titles
+                       $titles = explode("\n", $data['titles']);
+
+                       //Set latitudes
+                       $latitudes = explode("\n", $data['latitudes']);
+
+                       //Set longitudes
+                       $longitudes = explode("\n", $data['longitudes']);
 
-               //Set optional user id
-               if (!empty($userId)) {
-                       $ret->setParameter('uid', $userId);
+                       //Set indoors
+                       $indoors = explode("\n", $data['indoors']);
+
+                       //Set counts
+                       $counts = explode("\n", $data['counts']);
+
+                       //With unsufficient count
+                       if ($count && $counts[0] < $count) {
+                               //Skip empty city
+                               //XXX: count are sorted so only check first
+                               continue;
+                       }
+
+                       //Set locations
+                       $data['locations'] = [];
+
+                       //Iterate on each location
+                       foreach(explode("\n", $data['ids']) as $k => $id) {
+                               //With unsufficient count
+                               if ($count && $counts[$k] < $count) {
+                                       //Skip empty city
+                                       //XXX: count are sorted so only check first
+                                       continue;
+                               }
+
+                               //Add location
+                               $data['locations'][] = [
+                                       'id' => $id,
+                                       'title' => $location = $this->translator->trans($titles[$k]),
+                                       'latitude' => floatval($latitudes[$k]),
+                                       'longitude' => floatval($longitudes[$k]),
+                                       'indoor' => $indoors[$k] == 0 ? $this->translator->trans('outdoor') : $this->translator->trans('indoor'),
+                                       'link' => $this->router->generate('rapsysair_location_view', ['id' => $id, 'location' => $this->slugger->slug($location)])
+                               ];
+                       }
+
+                       //Add to return
+                       $return[$city] = [
+                               'id' => $data['id'],
+                               'city' => $data['city'],
+                               'in' => $this->translator->trans('in '.$data['city']),
+                               'indoors' => array_map(function ($v) { return $v == 0 ? $this->translator->trans('outdoor') : $this->translator->trans('indoor'); }, array_unique($indoors)),
+                               'multimap' => $this->translator->trans($data['city'].' sector map'),
+                               'latitude' => $data['latitude'],
+                               'longitude' => $data['longitude'],
+                               'modified' => $data['modified'],
+                               //XXX: Useless ???
+                               'slug' => $city = $this->slugger->slug($data['city']),
+                               'link' => $this->router->generate('rapsysair_city_view', ['city' => $city, 'latitude' => $data['latitude'], 'longitude' => $data['longitude']]),
+                               'locations' => $data['locations']
+                       ];
                }
 
-               //Get Result
-               $ret = $ret->getResult();
+               //Return return
+               return $return;
+       }
 
-               //Rekey array
-               $ret = array_column($ret, 'title', 'id');
+       /**
+        * Find city by latitude and longitude as array
+        *
+        * @param float $latitude The latitude
+        * @param float $longitude The longitude
+        * @return ?array The cities array
+        */
+       public function findCityByLatitudeLongitudeAsArray(float $latitude, float $longitude): ?array {
+               //Set the request
+               $req = <<<SQL
+SELECT
+       SUBSTRING(l.zipcode, 1, 2) AS id,
+       l.city AS city,
+       ROUND(AVG(l.latitude), 6) AS latitude,
+       ROUND(AVG(l.longitude), 6) AS longitude,
+       MAX(l.updated) AS updated
+FROM Rapsys\AirBundle\Entity\Location AS l
+GROUP BY city, SUBSTRING(l.zipcode, 1, 2)
+ORDER BY ACOS(SIN(RADIANS(:latitude))*SIN(RADIANS(l.latitude))+COS(RADIANS(:latitude))*COS(RADIANS(l.latitude))*COS(RADIANS(:longitude - l.longitude)))*40030.17/2/PI()
+LIMIT 0, 1
+SQL;
+
+               //Replace bundle entity name by table name
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
+
+               //Get result set mapping instance
+               //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
+               $rsm = new ResultSetMapping();
+
+               //Declare all fields
+               //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
+               //addScalarResult($sqlColName, $resColName, $type = 'string');
+               $rsm->addScalarResult('id', 'id', 'integer')
+                       ->addScalarResult('city', 'city', 'string')
+                       ->addScalarResult('latitude', 'latitude', 'float')
+                       ->addScalarResult('longitude', 'longitude', 'float')
+                       ->addScalarResult('updated', 'updated', 'datetime')
+                       ->addIndexByScalar('city');
 
-               //Filter array
-               foreach($ret as $k => $v) {
-                       $ret[$k] = $translator->trans($v);
+               //Get result
+               $result = $this->_em
+                       ->createNativeQuery($req, $rsm)
+                       ->setParameter('latitude', $latitude)
+                       ->setParameter('longitude', $longitude)
+                       ->getOneOrNullResult();
+
+               //Without result
+               if ($result === null) {
+                       //Return result
+                       return $result;
                }
 
-               //Send result
-               return $ret;
+               //Return result
+               return [
+                       'id' => $result['id'],
+                       'city' => $result['city'],
+                       'latitude' => $result['latitude'],
+                       'longitude' => $result['longitude'],
+                       'updated' => $result['updated'],
+                       'in' => $this->translator->trans('in '.$result['city']),
+                       'multimap' => $this->translator->trans($result['city'].' sector map'),
+                       //XXX: Useless ???
+                       'slug' => $slug = $this->slugger->slug($result['city']),
+                       'link' => $this->router->generate('rapsysair_city_view', ['city' => $slug, 'latitude' => $result['latitude'], 'longitude' => $result['longitude']])
+               ];
        }
 
        /**
-        * Fetch translated location title with session by date period
+        * Find locations by latitude and longitude sorted by period as array
         *
-        * @param $translator The TranslatorInterface instance
-        * @param $period The date period
-        * @param $granted The session is granted
-        * TODO: a dropper
+        * @TODO: find all other locations when current one has no sessions ???
+        *
+        * @param float $latitude The latitude
+        * @param float $longitude The longitude
+        * @param DatePeriod $period The period
+        * @param int $count The session count
+        * @param float $distance The distance
+        * @return array The locations array
         */
-       public function fetchTranslatedLocationByDatePeriod(TranslatorInterface $translator, $period, $granted = false) {
-               //Fetch sessions
-               $ret = $this->getEntityManager()
-                       ->createQuery('SELECT l.id, l.title FROM RapsysAirBundle:Session s JOIN RapsysAirBundle:Location l WHERE '.($granted?'s.application IS NOT NULL AND ':'').'l.id = s.location AND s.date BETWEEN :begin AND :end GROUP BY l.id ORDER BY l.id')
+       public function findAllByLatitudeLongitudeAsArray(float $latitude, float $longitude, \DatePeriod $period, int $count = 1, float $distance = 15): array {
+               //Set earth radius
+               $radius = 40030.17/2/pi();
+
+               //Compute min latitude
+               $minlat = min(rad2deg(asin(sin(deg2rad($latitude))*cos($distance/$radius) + cos(deg2rad($latitude))*sin($distance/$radius)*cos(deg2rad(180)))), $latitude);
+
+               //Compute max latitude
+               $maxlat = max(rad2deg(asin(sin(deg2rad($latitude))*cos($distance/$radius) + cos(deg2rad($latitude))*sin($distance/$radius)*cos(deg2rad(0)))), $latitude);
+
+               //Compute min longitude
+               $minlong = fmod((rad2deg((deg2rad($longitude) + atan2(sin(deg2rad(-90))*sin($distance/$radius)*cos(deg2rad($minlat)), cos($distance/$radius) - sin(deg2rad($minlat)) * sin(deg2rad($minlat))))) + 180), 360) - 180;
+
+               //Compute max longi
+               $maxlong = fmod((rad2deg((deg2rad($longitude) + atan2(sin(deg2rad(90))*sin($distance/$radius)*cos(deg2rad($maxlat)), cos($distance/$radius) - sin(deg2rad($maxlat)) * sin(deg2rad($maxlat))))) + 180), 360) - 180;
+
+               //Set the request
+               //TODO: see old request before commit to sort session count, distance and then by id ?
+               //TODO: see to sort by future session count, historical session count, distance and then by id ?
+               //TODO: do the same for cities and city ?
+               $req = <<<SQL
+SELECT
+       a.id,
+       a.title,
+       a.latitude,
+       a.longitude,
+       a.created,
+       a.updated,
+       MAX(GREATEST(a.modified, COALESCE(s.created, '1970-01-01'), COALESCE(s.updated, '1970-01-01'))) AS modified,
+       COUNT(s.id) AS count
+FROM (
+       SELECT
+               l.id,
+               l.title,
+               l.latitude,
+               l.longitude,
+               l.created,
+               l.updated,
+               GREATEST(l.created, l.updated) AS modified
+       FROM Rapsys\AirBundle\Entity\Location AS l
+       WHERE l.latitude BETWEEN :minlat AND :maxlat AND l.longitude BETWEEN :minlong AND :maxlong
+       LIMIT 0, :limit
+) AS a
+LEFT JOIN Rapsys\AirBundle\Entity\Session s ON (s.location_id = a.id)
+GROUP BY a.id
+ORDER BY COUNT(IF(s.date BETWEEN :begin AND :end, s.id, NULL)) DESC, count DESC, a.id
+SQL;
+
+               //Replace bundle entity name by table name
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
+
+               //Get result set mapping instance
+               //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
+               $rsm = new ResultSetMapping();
+
+               //Declare all fields
+               //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
+               //addScalarResult($sqlColName, $resColName, $type = 'string');
+               $rsm->addScalarResult('id', 'id', 'integer')
+                       ->addScalarResult('title', 'title', 'string')
+                       ->addScalarResult('latitude', 'latitude', 'float')
+                       ->addScalarResult('longitude', 'longitude', 'float')
+                       ->addScalarResult('created', 'created', 'datetime')
+                       ->addScalarResult('updated', 'updated', 'datetime')
+                       ->addScalarResult('modified', 'modified', 'datetime')
+                       ->addScalarResult('count', 'count', 'integer');
+
+               //Get result
+               $result = $this->_em
+                       ->createNativeQuery($req, $rsm)
                        ->setParameter('begin', $period->getStartDate())
                        ->setParameter('end', $period->getEndDate())
-                       ->getResult();
+                       ->setParameter('minlat', $minlat)
+                       ->setParameter('maxlat', $maxlat)
+                       ->setParameter('minlong', $minlong)
+                       ->setParameter('maxlong', $maxlong)
+                       ->getArrayResult();
 
-               //Rekey array
-               $ret = array_column($ret, 'title', 'id');
+               //Set return
+               $return = [];
 
-               //Filter array
-               foreach($ret as $k => $v) {
-                       $ret[$k] = $translator->trans($v);
+               //Iterate on each location
+               foreach($result as $id => $data) {
+                       //With active locations
+                       if ($count && $data['count'] < $count) {
+                               //Skip unactive locations
+                               continue;
+                       }
+
+                       //Add location
+                       $return[$id] = [
+                               'id' => $data['id'],
+                               'title' => $title = $this->translator->trans($data['title']),
+                               'latitude' => $data['latitude'],
+                               'longitude' => $data['longitude'],
+                               'created' => $data['created'],
+                               'updated' => $data['updated'],
+                               'modified' => $data['modified'],
+                               'count' => $data['count'],
+                               'slug' => $slug = $this->slugger->slug($title),
+                               'link' => $this->router->generate('rapsysair_location_view', ['id' => $data['id'], 'location' => $slug])
+                       ];
                }
 
-               //Send result
-               return $ret;
+               //Return return
+               return $return;
        }
 
        /**
-        * Fetch translated location title with user session by date period
+        * Find locations by user id sorted by period as array
         *
-        * @param $translator The TranslatorInterface instance
-        * @param $period The date period
-        * @param $userId The user uid
-        * TODO: a dropper
+        * @param int $userId The user id
+        * @param DatePeriod $period The period
+        * @return array The locations array
         */
-       public function fetchTranslatedUserLocationByDatePeriod(TranslatorInterface $translator, $period, $userId) {
-               //Fetch sessions
-               $ret = $this->getEntityManager()
-                       ->createQuery('SELECT l.id, l.title FROM RapsysAirBundle:Application a JOIN RapsysAirBundle:Session s JOIN RapsysAirBundle:Location l WHERE a.user = :uid AND a.session = s.id AND s.date BETWEEN :begin AND :end AND s.location = l.id GROUP BY l.id ORDER BY l.id')
+       public function findAllByUserIdAsArray(int $userId, \DatePeriod $period, $distance = 15): array {
+               //Set the request
+               //TODO: ajouter pays ???
+               $req = <<<SQL
+SELECT
+       a.id,
+       a.title,
+       a.city,
+       a.latitude,
+       a.longitude,
+       a.created,
+       a.updated,
+       MAX(GREATEST(a.modified, COALESCE(s3.created, '1970-01-01'), COALESCE(s3.updated, '1970-01-01'))) AS modified,
+       a.pcount,
+       COUNT(s3.id) AS tcount
+FROM (
+       SELECT
+               b.id,
+               b.title,
+               b.city,
+               b.latitude,
+               b.longitude,
+               b.created,
+               b.updated,
+               MAX(GREATEST(b.modified, COALESCE(s2.created, '1970-01-01'), COALESCE(s2.updated, '1970-01-01'))) AS modified,
+               COUNT(s2.id) AS pcount
+       FROM (
+               SELECT
+                       l2.id,
+                       l2.city,
+                       l2.title,
+                       l2.latitude,
+                       l2.longitude,
+                       l2.created,
+                       l2.updated,
+                       GREATEST(l2.created, l2.updated) AS modified
+               FROM (
+                       SELECT
+                               l.id,
+                               l.latitude,
+                               l.longitude
+                       FROM applications AS a
+                       JOIN sessions AS s ON (s.id = a.session_id)
+                       JOIN locations AS l ON (l.id = s.location_id)
+                       WHERE a.user_id = :id
+                       GROUP BY l.id
+                       ORDER BY NULL
+                       LIMIT 0, :limit
+               ) AS a
+               JOIN locations AS l2
+               WHERE ACOS(SIN(RADIANS(a.latitude))*SIN(RADIANS(l2.latitude))+COS(RADIANS(a.latitude))*COS(RADIANS(l2.latitude))*COS(RADIANS(a.longitude - l2.longitude)))*40030.17/2/PI() BETWEEN 0 AND :distance
+               GROUP BY l2.id
+               ORDER BY NULL
+               LIMIT 0, :limit
+       ) AS b
+       LEFT JOIN sessions AS s2 ON (s2.location_id = b.id AND s2.date BETWEEN :begin AND :end)
+       GROUP BY b.id
+       ORDER BY NULL
+       LIMIT 0, :limit
+) AS a
+LEFT JOIN sessions AS s3 ON (s3.location_id = a.id)
+GROUP BY a.id
+ORDER BY pcount DESC, tcount DESC, a.id
+SQL;
+
+               //Replace bundle entity name by table name
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
+
+               //Get result set mapping instance
+               //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
+               $rsm = new ResultSetMapping();
+
+               //Declare all fields
+               //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
+               //addScalarResult($sqlColName, $resColName, $type = 'string');
+               $rsm->addScalarResult('id', 'id', 'integer')
+                       ->addScalarResult('title', 'title', 'string')
+                       ->addScalarResult('city', 'city', 'string')
+                       ->addScalarResult('latitude', 'latitude', 'float')
+                       ->addScalarResult('longitude', 'longitude', 'float')
+                       ->addScalarResult('created', 'created', 'datetime')
+                       ->addScalarResult('updated', 'updated', 'datetime')
+                       ->addScalarResult('modified', 'modified', 'datetime')
+                       ->addScalarResult('pcount', 'pcount', 'integer')
+                       ->addScalarResult('tcount', 'tcount', 'integer');
+
+               //Get result
+               $result = $this->_em
+                       ->createNativeQuery($req, $rsm)
                        ->setParameter('begin', $period->getStartDate())
                        ->setParameter('end', $period->getEndDate())
-                       ->setParameter('uid', $userId)
-                       ->getResult();
+                       ->setParameter('id', $userId)
+                       ->setParameter('distance', $distance)
+                       ->getArrayResult();
+
+               //Set return
+               $return = [];
 
-               //Rekey array
-               $ret = array_column($ret, 'title', 'id');
+               //Iterate on each location
+               foreach($result as $id => $data) {
+                       //With active locations
+                       if (!empty($result[0]['tcount']) && empty($data['tcount'])) {
+                               //Skip unactive locations
+                               break;
+                       }
 
-               //Filter array
-               foreach($ret as $k => $v) {
-                       $ret[$k] = $translator->trans($v);
+                       //Add location
+                       $return[$id] = [
+                               'id' => $data['id'],
+                               'city' => $data['city'],
+                               'title' => $title = $this->translator->trans($data['title']),
+                               'at' => $this->translator->trans('at '.$data['title']),
+                               'miniature' => $this->translator->trans($data['title'].' miniature'),
+                               'latitude' => $data['latitude'],
+                               'longitude' => $data['longitude'],
+                               'created' => $data['created'],
+                               'updated' => $data['updated'],
+                               'modified' => $data['modified'],
+                               'pcount' => $data['pcount'],
+                               'tcount' => $data['tcount'],
+                               'slug' => $slug = $this->slugger->slug($title),
+                               'link' => $this->router->generate('rapsysair_location_view', ['id' => $data['id'], 'location' => $slug])
+                       ];
                }
 
-               //Send result
-               return $ret;
+               //Return return
+               return $return;
        }
 
        /**
-        * Find locations by user id
+        * Find location as array by id
         *
-        * @param $id The user id
-        * @return array The user locations
+        * @param int $id The location id
+        * @param string $locale The locale
+        * @return array The location data
         */
-       public function findByUserId($userId) {
-               //Get entity manager
-               $em = $this->getEntityManager();
-
-               //Get quote strategy
-               $qs = $em->getConfiguration()->getQuoteStrategy();
-               $dp = $em->getConnection()->getDatabasePlatform();
-
-               //Get quoted table names
-               //XXX: this allow to make this code table name independent
-               $tables = [
-                       'RapsysAirBundle:UserLocation' => $qs->getJoinTableName($em->getClassMetadata('RapsysAirBundle:User')->getAssociationMapping('locations'), $em->getClassMetadata('RapsysAirBundle:User'), $dp),
-                       'RapsysAirBundle:Location' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Location'), $dp)
+       public function findOneByIdAsArray(int $id, string $locale): ?array {
+               //Set the request
+               $req = <<<SQL
+SELECT
+       l.id,
+       l.title,
+       l.city,
+       l.latitude,
+       l.longitude,
+       l.indoor,
+       l.zipcode,
+       MAX(GREATEST(l.created, l.updated, l2.created, l2.updated)) AS modified,
+       SUBSTRING(l.zipcode, 1, 2) AS city_id,
+       ROUND(AVG(l2.latitude), 6) AS city_latitude,
+       ROUND(AVG(l2.longitude), 6) AS city_longitude
+FROM Rapsys\AirBundle\Entity\Location AS l
+JOIN Rapsys\AirBundle\Entity\Location AS l2 ON (l2.city = l.city AND SUBSTRING(l2.zipcode, 1, 2) = SUBSTRING(l.zipcode, 1, 2))
+WHERE l.id = :id
+GROUP BY l.id
+LIMIT 0, 1
+SQL;
+
+               //Replace bundle entity name by table name
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
+
+               //Get result set mapping instance
+               //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
+               $rsm = new ResultSetMapping();
+
+               //Declare all fields
+               //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
+               //addScalarResult($sqlColName, $resColName, $type = 'string');
+               $rsm->addScalarResult('id', 'id', 'integer')
+                       ->addScalarResult('title', 'title', 'string')
+                       ->addScalarResult('city', 'city', 'string')
+                       ->addScalarResult('latitude', 'latitude', 'float')
+                       ->addScalarResult('longitude', 'longitude', 'float')
+                       ->addScalarResult('indoor', 'indoor', 'boolean')
+                       ->addScalarResult('zipcode', 'zipcode', 'string')
+                       ->addScalarResult('modified', 'modified', 'datetime')
+                       ->addScalarResult('city_id', 'city_id', 'integer')
+                       ->addScalarResult('city_latitude', 'city_latitude', 'float')
+                       ->addScalarResult('city_longitude', 'city_longitude', 'float')
+                       ->addIndexByScalar('id');
+
+               //Get result
+               $result = $this->_em
+                       ->createNativeQuery($req, $rsm)
+                       ->setParameter('id', $id)
+                       ->getOneOrNullResult();
+
+               //Without result
+               if ($result === null) {
+                       //Return result
+                       return $result;
+               }
+
+               //Set alternates
+               $result['alternates'] = [];
+
+               //Set route
+               $route = 'rapsysair_location_view';
+
+               //Set route params
+               $routeParams = ['id' => $id];
+
+               //Iterate on each languages
+               foreach($this->languages as $languageId => $language) {
+                       //Without current locale
+                       if ($languageId !== $locale) {
+                               //Set titles
+                               $titles = [];
+
+                               //Set route params locale
+                               $routeParams['_locale'] = $languageId;
+
+                               //Set route params location
+                               $routeParams['location'] = $this->slugger->slug($this->translator->trans($result['title'], [], null, $languageId));
+
+                               //Iterate on each locales
+                               foreach(array_keys($this->languages) as $other) {
+                                       //Without other locale
+                                       if ($other !== $languageId) {
+                                               //Set other locale title
+                                               $titles[$other] = $this->translator->trans($language, [], null, $other);
+                                       }
+                               }
+
+                               //Add alternates locale
+                               $result['alternates'][substr($languageId, 0, 2)] = $result['alternates'][str_replace('_', '-', $languageId)] = [
+                                       'absolute' => $this->router->generate($route, $routeParams, UrlGeneratorInterface::ABSOLUTE_URL),
+                                       'relative' => $this->router->generate($route, $routeParams),
+                                       'title' => implode('/', $titles),
+                                       'translated' => $this->translator->trans($language, [], null, $languageId)
+                               ];
+                       }
+               }
+
+               //Return result
+               return [
+                       'id' => $result['id'],
+                       'city' => [
+                               'id' => $result['city_id'],
+                               'title' => $result['city'],
+                               'in' => $this->translator->trans('in '.$result['city']),
+                               'link' => $this->router->generate('rapsysair_city_view', ['city' => $result['city'], 'latitude' => $result['city_latitude'], 'longitude' => $result['city_longitude']])
+                       ],
+                       'title' => $title = $this->translator->trans($result['title']),
+                       'latitude' => $result['latitude'],
+                       'longitude' => $result['longitude'],
+                       'indoor' => $result['indoor'],
+                       'modified' => $result['modified'],
+                       'around' => $this->translator->trans('around '.$result['title']),
+                       'at' => $this->translator->trans('at '.$result['title']),
+                       'atin' => $this->translator->trans('at '.$result['title']).' '.$this->translator->trans('in '.$result['city']),
+                       'multimap' => $this->translator->trans($result['title'].' sector map'),
+                       //XXX: Useless ???
+                       'slug' => $slug = $this->slugger->slug($title),
+                       'link' => $this->router->generate($route, ['_locale' => $locale, 'location' => $slug]+$routeParams),
+                       'alternates' => $result['alternates']
                ];
+       }
 
+       /**
+        * Find complementary locations by session id
+        *
+        * @param int $id The session id
+        * @return array The other locations
+        */
+       public function findComplementBySessionId(int $id): array {
+               //Fetch complement locations
+               return array_column(
+                       $this->getEntityManager()
+                               #->createQuery('SELECT l.id, l.title FROM Rapsys\AirBundle\Entity\Location l JOIN Rapsys\AirBundle\Entity\Session s WITH s.id = :sid LEFT JOIN Rapsys\AirBundle\Entity\Session s2 WITH s2.id != s.id AND s2.slot = s.slot AND s2.date = s.date WHERE l.id != s.location AND s2.location IS NULL GROUP BY l.id ORDER BY l.id')
+                               ->createQuery('SELECT l.id, l.title FROM Rapsys\AirBundle\Entity\Session s LEFT JOIN Rapsys\AirBundle\Entity\Session s2 WITH s2.id != s.id AND s2.slot = s.slot AND s2.date = s.date LEFT JOIN Rapsys\AirBundle\Entity\Location l WITH l.id != s.location AND (l.id != s2.location OR s2.location IS NULL) WHERE s.id = :sid GROUP BY l.id ORDER BY l.id')
+                               ->setParameter('sid', $id)
+                               ->getArrayResult(),
+                       'id',
+                       'title'
+               );
+       }
+
+       /**
+        * Find locations by user id
+        *
+        * @param int $id The user id
+        * @return array The user locations
+        */
+       public function findByUserId(int $userId): array {
                //Set the request
                $req = 'SELECT l.id, l.title
-FROM RapsysAirBundle:UserLocation AS ul
-JOIN RapsysAirBundle:Location AS l ON (l.id = ul.location_id)
-WHERE ul.user_id = :uid';
+FROM Rapsys\AirBundle\Entity\UserLocation AS ul
+JOIN Rapsys\AirBundle\Entity\Location AS l ON (l.id = ul.location_id)
+WHERE ul.user_id = :id';
 
                //Replace bundle entity name by table name
-               $req = str_replace(array_keys($tables), array_values($tables), $req);
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
 
                //Get result set mapping instance
                //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
                $rsm = new ResultSetMapping();
 
                //Declare result set for our request
-               $rsm->addEntityResult('RapsysAirBundle:Location', 'l');
-               $rsm->addFieldResult('l', 'id', 'id');
-               $rsm->addFieldResult('l', 'title', 'title');
+               $rsm->addEntityResult('Rapsys\AirBundle\Entity\Location', 'l')
+                       ->addFieldResult('l', 'id', 'id')
+                       ->addFieldResult('l', 'title', 'title');
 
                //Send result
-               return $em
+               return $this->_em
                        ->createNativeQuery($req, $rsm)
-                       ->setParameter('uid', $userId)
+                       ->setParameter('id', $userId)
                        ->getResult();
        }
 }
index dd89a9895f43df861d2163787612285760f76c88..c310b5406458295f720cb0755a2bd66c4e865477 100644 (file)
@@ -1,33 +1,37 @@
-<?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\Repository;
 
-use Symfony\Component\Translation\TranslatorInterface;
-use Doctrine\DBAL\Types\Type;
+use Doctrine\DBAL\Types\Types;
+use Doctrine\ORM\AbstractQuery;
 use Doctrine\ORM\Query\ResultSetMapping;
-use Doctrine\ORM\Query;
+
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+
+use Rapsys\AirBundle\Entity\Application;
+use Rapsys\AirBundle\Entity\Location;
+use Rapsys\AirBundle\Entity\Session;
+use Rapsys\AirBundle\Entity\Slot;
+use Rapsys\AirBundle\Repository;
 
 /**
  * SessionRepository
  */
-class SessionRepository extends \Doctrine\ORM\EntityRepository {
-       ///Set accuweather max number of daily pages
-       const ACCUWEATHER_DAILY = 12;
-
-       ///Set accuweather max number of hourly pages
-       const ACCUWEATHER_HOURLY = 3;
-
-       ///Set guest delay
-       const GUEST_DELAY = 2;
-
-       ///Set regular delay
-       const REGULAR_DELAY = 3;
-
-       ///Set senior
-       const SENIOR_DELAY = 4;
-
+class SessionRepository extends Repository {
        ///Set glyphs
        //TODO: document utf-8 codes ?
+       //TODO: use unknown == ? symbol by default ???
+       //💃<= dancer #0001f483
+       //💃<= tanguera #0001f483
        const GLYPHS = [
                //Slots
                'Morning' => '🌅', #0001f305
@@ -40,263 +44,21 @@ class SessionRepository extends \Doctrine\ORM\EntityRepository {
                'Cloudy' => '☁', #2601
                'Winty' => '❄️', #2744
                'Rainy' => '🌂', #0001f302
-               'Stormy' => '☔' #2614
+               'Stormy' => '☔', #2614
+               //Rate
+               'Euro' => '€', #20ac
+               'Free' => '🍺', #0001f37a
+               'Hat' => '🎩' #0001f3a9
        ];
 
        /**
-        * Find session by location, slot and date
-        *
-        * @param $location The location
-        * @param $slot The slot
-        * @param $date The datetime
-        */
-       public function findOneByLocationSlotDate($location, $slot, $date) {
-               //Return sessions
-               return $this->getEntityManager()
-                       ->createQuery('SELECT s FROM RapsysAirBundle:Session s WHERE (s.location = :location AND s.slot = :slot AND s.date = :date)')
-                       ->setParameter('location', $location)
-                       ->setParameter('slot', $slot)
-                       ->setParameter('date', $date)
-                       ->getSingleResult();
-       }
-
-       /**
-        * Find sessions by date period
+        * Find session as array by id
         *
-        * @param $period The date period
-        */
-       public function findAllByDatePeriod($period) {
-               //Return sessions
-               return $this->getEntityManager()
-                       ->createQuery('SELECT s FROM RapsysAirBundle:Session s WHERE s.date BETWEEN :begin AND :end')
-                       ->setParameter('begin', $period->getStartDate())
-                       ->setParameter('end', $period->getEndDate())
-                       ->getResult();
-       }
-
-       /**
-        * Find sessions by location and date period
-        *
-        * @param $location The location
-        * @param $period The date period
-        */
-       public function findAllByLocationDatePeriod($location, $period) {
-               //Return sessions
-               return $this->getEntityManager()
-                       ->createQuery('SELECT s FROM RapsysAirBundle:Session s WHERE (s.location = :location AND s.date BETWEEN :begin AND :end)')
-                       ->setParameter('location', $location)
-                       ->setParameter('begin', $period->getStartDate())
-                       ->setParameter('end', $period->getEndDate())
-                       ->getResult();
-       }
-
-       /**
-        * Find one session by location and user id within last month
-        *
-        * @param $location The location id
-        * @param $user The user id
-        */
-       public function findOneWithinLastMonthByLocationUser($location, $user) {
-               //Get entity manager
-               $em = $this->getEntityManager();
-
-               //Get quote strategy
-               $qs = $em->getConfiguration()->getQuoteStrategy();
-               $dp = $em->getConnection()->getDatabasePlatform();
-
-               //Get quoted table names
-               //XXX: this allow to make this code table name independent
-               $tables = [
-                       'RapsysAirBundle:Session' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Session'), $dp),
-                       'RapsysAirBundle:Application' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Application'), $dp),
-                       "\t" => '',
-                       "\n" => ' '
-               ];
-
-               //Set the request
-               //XXX: give the gooddelay to guest just in case
-               $req =<<<SQL
-SELECT s.id
-FROM RapsysAirBundle:Session s
-JOIN RapsysAirBundle:Application a ON (a.id = s.application_id AND a.user_id = :uid AND (a.canceled IS NULL OR TIMESTAMPDIFF(DAY, a.canceled, ADDTIME(s.date, s.begin)) < 1))
-WHERE s.location_id = :lid AND s.date >= DATE_ADD(DATE_SUB(NOW(), INTERVAL 1 MONTH), INTERVAL :gooddelay DAY)
-SQL;
-
-               //Replace bundle entity name by table name
-               $req = str_replace(array_keys($tables), array_values($tables), $req);
-
-               //Get result set mapping instance
-               $rsm = new ResultSetMapping();
-
-               //Declare all fields
-               //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
-               $rsm->addScalarResult('id', 'id', 'integer')
-                       ->addIndexByScalar('id');
-
-               //Return result
-               return $em
-                       ->createNativeQuery($req, $rsm)
-                       ->setParameter('lid', $location)
-                       ->setParameter('uid', $user)
-                       ->setParameter('gooddelay', self::SENIOR_DELAY)
-                       ->getOneOrNullResult();
-       }
-
-       /**
-        * Fetch sessions by date period
-        *
-        * @param $period The date period
-        * @param $locale The locale
-        */
-       public function fetchAllByDatePeriod($period, $locale = null) {
-               //Get entity manager
-               $em = $this->getEntityManager();
-
-               //Get quote strategy
-               $qs = $em->getConfiguration()->getQuoteStrategy();
-               $dp = $em->getConnection()->getDatabasePlatform();
-
-               //Get quoted table names
-               //XXX: this allow to make this code table name independent
-               $tables = [
-                       'RapsysAirBundle:Application' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Application'), $dp),
-                       'RapsysAirBundle:Location' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Location'), $dp),
-                       'RapsysAirBundle:Session' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Session'), $dp),
-                       'RapsysAirBundle:Snippet' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Snippet'), $dp),
-                       'RapsysAirBundle:User' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:User'), $dp),
-                       ':afterid' => 4,
-                       "\t" => '',
-                       "\n" => ' '
-               ];
-
-               //Set the request
-               //TODO: exclude opera and others ?
-               $req = <<<SQL
-SELECT
-       s.id,
-       s.date,
-       s.locked,
-       s.updated,
-       ADDDATE(ADDTIME(s.date, s.begin), INTERVAL IF(s.slot_id = :afterid, 1, 0) DAY) AS start,
-       ADDDATE(ADDTIME(ADDTIME(s.date, s.begin), s.length), INTERVAL IF(s.slot_id = :afterid, 1, 0) DAY) AS stop,
-       s.location_id AS l_id,
-       l.address AS l_address,
-       l.zipcode AS l_zipcode,
-       l.city AS l_city,
-       l.title AS l_title,
-       l.latitude AS l_latitude,
-       l.longitude AS l_longitude,
-       s.application_id AS a_id,
-       a.canceled AS a_canceled,
-       a.user_id AS au_id,
-       au.forename AS au_forename,
-       au.pseudonym AS au_pseudonym,
-       p.id AS p_id,
-       p.description AS p_description,
-       p.class AS p_class,
-       p.short AS p_short,
-       p.hat AS p_hat,
-       p.rate AS p_rate,
-       p.contact AS p_contact,
-       p.donate AS p_donate,
-       p.link AS p_link,
-       p.profile AS p_profile
-FROM RapsysAirBundle:Session AS s
-JOIN RapsysAirBundle:Location AS l ON (l.id = s.location_id)
-JOIN RapsysAirBundle:Application AS a ON (a.id = s.application_id)
-JOIN RapsysAirBundle:User AS au ON (au.id = a.user_id)
-LEFT JOIN RapsysAirBundle:Snippet AS p ON (p.location_id = s.location_id AND p.user_id = a.user_id AND p.locale = :locale)
-WHERE s.date BETWEEN :begin AND :end
-ORDER BY NULL
-SQL;
-
-               //Replace bundle entity name by table name
-               $req = str_replace(array_keys($tables), array_values($tables), $req);
-
-               //Get result set mapping instance
-               //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
-               $rsm = new ResultSetMapping();
-
-               //Declare all fields
-               //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
-               //addScalarResult($sqlColName, $resColName, $type = 'string');
-               $rsm->addScalarResult('id', 'id', 'integer')
-                       ->addScalarResult('date', 'date', 'date')
-                       ->addScalarResult('locked', 'locked', 'datetime')
-                       ->addScalarResult('updated', 'updated', 'datetime')
-                       ->addScalarResult('start', 'start', 'datetime')
-                       ->addScalarResult('stop', 'stop', 'datetime')
-                       ->addScalarResult('l_id', 'l_id', 'integer')
-                       ->addScalarResult('l_address', 'l_address', 'string')
-                       ->addScalarResult('l_zipcode', 'l_zipcode', 'string')
-                       ->addScalarResult('l_city', 'l_city', 'string')
-                       ->addScalarResult('l_latitude', 'l_latitude', 'float')
-                       ->addScalarResult('l_longitude', 'l_longitude', 'float')
-                       ->addScalarResult('l_title', 'l_title', 'string')
-                       ->addScalarResult('t_id', 't_id', 'integer')
-                       ->addScalarResult('t_title', 't_title', 'string')
-                       ->addScalarResult('a_id', 'a_id', 'integer')
-                       ->addScalarResult('a_canceled', 'a_canceled', 'datetime')
-                       ->addScalarResult('au_id', 'au_id', 'integer')
-                       ->addScalarResult('au_forename', 'au_forename', 'string')
-                       ->addScalarResult('au_pseudonym', 'au_pseudonym', 'string')
-                       ->addScalarResult('p_id', 'p_id', 'integer')
-                       ->addScalarResult('p_description', 'p_description', 'string')
-                       ->addScalarResult('p_class', 'p_class', 'string')
-                       ->addScalarResult('p_short', 'p_short', 'string')
-                       ->addScalarResult('p_hat', 'p_hat', 'integer')
-                       ->addScalarResult('p_rate', 'p_rate', 'integer')
-                       ->addScalarResult('p_contact', 'p_contact', 'string')
-                       ->addScalarResult('p_donate', 'p_donate', 'string')
-                       ->addScalarResult('p_link', 'p_link', 'string')
-                       ->addScalarResult('p_profile', 'p_profile', 'string')
-                       ->addIndexByScalar('id');
-
-               //Fetch result
-               $res = $em
-                       ->createNativeQuery($req, $rsm)
-                       ->setParameter('begin', $period->getStartDate())
-                       ->setParameter('end', $period->getEndDate())
-                       ->setParameter('locale', $locale);
-
-               //Return result
-               return $res->getResult();
-       }
-
-       /**
-        * Fetch session by id
-        *
-        * @param $id The session id
-        * @param $locale The locale
+        * @param int $id The session id
         * @return array The session data
         */
-       public function fetchOneById($id, $locale = null) {
-               //Get entity manager
-               $em = $this->getEntityManager();
-
-               //Get quote strategy
-               $qs = $em->getConfiguration()->getQuoteStrategy();
-               $dp = $em->getConnection()->getDatabasePlatform();
-
-               //Get quoted table names
-               //XXX: this allow to make this code table name independent
-               $tables = [
-                       'RapsysAirBundle:Application' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Application'), $dp),
-                       'RapsysAirBundle:Group' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Group'), $dp),
-                       'RapsysAirBundle:GroupUser' => $qs->getJoinTableName($em->getClassMetadata('RapsysAirBundle:User')->getAssociationMapping('groups'), $em->getClassMetadata('RapsysAirBundle:User'), $dp),
-                       'RapsysAirBundle:Location' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Location'), $dp),
-                       'RapsysAirBundle:Session' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Session'), $dp),
-                       'RapsysAirBundle:Snippet' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Snippet'), $dp),
-                       'RapsysAirBundle:Slot' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Slot'), $dp),
-                       'RapsysAirBundle:User' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:User'), $dp),
-                       ':afterid' => 4,
-                       "\t" => '',
-                       "\n" => ' '
-               ];
-
+       public function findOneByIdAsArray(int $id): ?array {
                //Set the request
-               //TODO: compute scores ?
-               //TODO: compute delivery date ? (J-3/J-4 ?)
                $req =<<<SQL
 SELECT
        s.id,
@@ -318,17 +80,21 @@ SELECT
        s.updated,
        s.location_id AS l_id,
        l.title AS l_title,
+       l.description AS l_description,
        l.address AS l_address,
        l.zipcode AS l_zipcode,
        l.city AS l_city,
        l.latitude AS l_latitude,
        l.longitude AS l_longitude,
+       l.indoor AS l_indoor,
        l.updated AS l_updated,
        s.slot_id AS t_id,
        t.title AS t_title,
-       t.updated AS t_updated,
        s.application_id AS a_id,
        a.canceled AS a_canceled,
+       a.dance_id AS ad_id,
+       ad.name AS ad_name,
+       ad.type AS ad_type,
        a.user_id AS au_id,
        au.pseudonym AS au_pseudonym,
        p.id AS p_id,
@@ -340,7 +106,7 @@ SELECT
        p.profile AS p_profile,
        p.rate AS p_rate,
        p.hat AS p_hat,
-       p.updated AS p_updated,
+       GREATEST(s.created, s.updated, l.created, l.updated, t.created, t.updated, COALESCE(a.created, '1970-01-01'), COALESCE(a.updated, '1970-01-01'), COALESCE(ad.created, '1970-01-01'), COALESCE(ad.updated, '1970-01-01'), COALESCE(au.created, '1970-01-01'), COALESCE(au.updated, '1970-01-01'), COALESCE(p.created, '1970-01-01'), COALESCE(p.updated, '1970-01-01'), MAX(GREATEST(COALESCE(sa.created, '1970-01-01'), COALESCE(sa.updated, '1970-01-01'), COALESCE(sad.created, '1970-01-01'), COALESCE(sad.updated, '1970-01-01'), COALESCE(sau.created, '1970-01-01'), COALESCE(sau.updated, '1970-01-01')))) AS modified,
        GROUP_CONCAT(sa.id ORDER BY sa.user_id SEPARATOR "\\n") AS sa_id,
        GROUP_CONCAT(IFNULL(sa.score, 'NULL') ORDER BY sa.user_id SEPARATOR "\\n") AS sa_score,
        GROUP_CONCAT(sa.created ORDER BY sa.user_id SEPARATOR "\\n") AS sa_created,
@@ -348,21 +114,23 @@ SELECT
        GROUP_CONCAT(IFNULL(sa.canceled, 'NULL') ORDER BY sa.user_id SEPARATOR "\\n") AS sa_canceled,
        GROUP_CONCAT(sa.user_id ORDER BY sa.user_id SEPARATOR "\\n") AS sau_id,
        GROUP_CONCAT(sau.pseudonym ORDER BY sa.user_id SEPARATOR "\\n") AS sau_pseudonym
-FROM RapsysAirBundle:Session AS s
-JOIN RapsysAirBundle:Location AS l ON (l.id = s.location_id)
-JOIN RapsysAirBundle:Slot AS t ON (t.id = s.slot_id)
-LEFT JOIN RapsysAirBundle:Application AS a ON (a.id = s.application_id)
-LEFT JOIN RapsysAirBundle:User AS au ON (au.id = a.user_id)
-LEFT JOIN RapsysAirBundle:Snippet AS p ON (p.location_id = s.location_id AND p.user_id = a.user_id AND p.locale = :locale)
-LEFT JOIN RapsysAirBundle:Application AS sa ON (sa.session_id = s.id)
-LEFT JOIN RapsysAirBundle:User AS sau ON (sau.id = sa.user_id)
-WHERE s.id = :sid
+FROM Rapsys\AirBundle\Entity\Session AS s
+JOIN Rapsys\AirBundle\Entity\Location AS l ON (l.id = s.location_id)
+JOIN Rapsys\AirBundle\Entity\Slot AS t ON (t.id = s.slot_id)
+LEFT JOIN Rapsys\AirBundle\Entity\Application AS a ON (a.id = s.application_id)
+LEFT JOIN Rapsys\AirBundle\Entity\Dance AS ad ON (ad.id = a.dance_id)
+LEFT JOIN Rapsys\AirBundle\Entity\User AS au ON (au.id = a.user_id)
+LEFT JOIN Rapsys\AirBundle\Entity\Snippet AS p ON (p.locale = :locale AND p.location_id = s.location_id AND p.user_id = a.user_id)
+LEFT JOIN Rapsys\AirBundle\Entity\Application AS sa ON (sa.session_id = s.id)
+LEFT JOIN Rapsys\AirBundle\Entity\Dance AS sad ON (sad.id = sa.dance_id)
+LEFT JOIN Rapsys\AirBundle\Entity\User AS sau ON (sau.id = sa.user_id)
+WHERE s.id = :id
 GROUP BY s.id
 ORDER BY NULL
 SQL;
 
                //Replace bundle entity name by table name
-               $req = str_replace(array_keys($tables), array_values($tables), $req);
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
 
                //Get result set mapping instance
                //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
@@ -389,17 +157,21 @@ SQL;
                        ->addScalarResult('updated', 'updated', 'datetime')
                        ->addScalarResult('l_id', 'l_id', 'integer')
                        ->addScalarResult('l_title', 'l_title', 'string')
+                       ->addScalarResult('l_description', 'l_description', 'string')
                        ->addScalarResult('l_address', 'l_address', 'string')
                        ->addScalarResult('l_zipcode', 'l_zipcode', 'string')
                        ->addScalarResult('l_city', 'l_city', 'string')
                        ->addScalarResult('l_latitude', 'l_latitude', 'float')
                        ->addScalarResult('l_longitude', 'l_longitude', 'float')
+                       ->addScalarResult('l_indoor', 'l_indoor', 'boolean')
                        ->addScalarResult('l_updated', 'l_updated', 'datetime')
                        ->addScalarResult('t_id', 't_id', 'integer')
                        ->addScalarResult('t_title', 't_title', 'string')
-                       ->addScalarResult('t_updated', 't_updated', 'datetime')
                        ->addScalarResult('a_id', 'a_id', 'integer')
                        ->addScalarResult('a_canceled', 'a_canceled', 'datetime')
+                       ->addScalarResult('ad_id', 'ad_id', 'integer')
+                       ->addScalarResult('ad_name', 'ad_name', 'string')
+                       ->addScalarResult('ad_type', 'ad_type', 'string')
                        ->addScalarResult('au_id', 'au_id', 'integer')
                        ->addScalarResult('au_pseudonym', 'au_pseudonym', 'string')
                        ->addScalarResult('p_id', 'p_id', 'integer')
@@ -411,7 +183,7 @@ SQL;
                        ->addScalarResult('p_profile', 'p_profile', 'text')
                        ->addScalarResult('p_rate', 'p_rate', 'integer')
                        ->addScalarResult('p_hat', 'p_hat', 'boolean')
-                       ->addScalarResult('p_updated', 'p_updated', 'datetime')
+                       ->addScalarResult('modified', 'modified', 'datetime')
                        //XXX: is a string because of \n separator
                        ->addScalarResult('sa_id', 'sa_id', 'string')
                        //XXX: is a string because of \n separator
@@ -428,371 +200,329 @@ SQL;
                        ->addScalarResult('sau_pseudonym', 'sau_pseudonym', 'string')
                        ->addIndexByScalar('id');
 
-               //Return result
-               return $em
+               //Set result
+               $result = $this->_em
                        ->createNativeQuery($req, $rsm)
-                       ->setParameter('sid', $id)
-                       ->setParameter('locale', $locale)
+                       ->setParameter('id', $id)
                        ->getOneOrNullResult();
-       }
-
-       /**
-        * Fetch sessions calendar with translated location by date period
-        *
-        * @param $translator The TranslatorInterface instance
-        * @param $period The date period
-        * @param $locationId The location id
-        * @param $sessionId The session id
-        * @param $granted The session is granted
-        */
-       public function fetchCalendarByDatePeriod(TranslatorInterface $translator, $period, $locationId = null, $sessionId = null, $granted = false, $locale = null) {
-               //Get entity manager
-               $em = $this->getEntityManager();
-
-               //Get quote strategy
-               $qs = $em->getConfiguration()->getQuoteStrategy();
-               $dp = $em->getConnection()->getDatabasePlatform();
-
-               //Get quoted table names
-               //XXX: this allow to make this code table name independent
-               $tables = [
-                       'RapsysAirBundle:GroupUser' => $qs->getJoinTableName($em->getClassMetadata('RapsysAirBundle:User')->getAssociationMapping('groups'), $em->getClassMetadata('RapsysAirBundle:User'), $dp),
-                       'RapsysAirBundle:Session' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Session'), $dp),
-                       'RapsysAirBundle:Application' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Application'), $dp),
-                       'RapsysAirBundle:Group' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Group'), $dp),
-                       'RapsysAirBundle:Location' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Location'), $dp),
-                       'RapsysAirBundle:Slot' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Slot'), $dp),
-                       'RapsysAirBundle:Snippet' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Snippet'), $dp),
-                       'RapsysAirBundle:User' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:User'), $dp),
-                       ':afterid' => 4,
-                       "\t" => '',
-                       "\n" => ' '
-               ];
-
-               //Init granted sql
-               $grantSql = '';
-
-               //When granted is set
-               if (empty($granted)) {
-                       //Set application and user as optional
-                       $grantSql = 'LEFT ';
-               }
-
-               //Init location sql
-               $locationSql = '';
 
-               //When location id is set
-               if (!empty($locationId)) {
-                       //Add location id clause
-                       $locationSql = "\n\t".'AND s.location_id = :lid';
+               //Without result
+               if ($result === null) {
+                       //Return result
+                       return $result;
                }
 
-               //Set the request
-               $req = <<<SQL
-
-SELECT
-       s.id,
-       s.date,
-       s.rainrisk,
-       s.rainfall,
-       s.realfeel,
-       s.temperature,
-       s.locked,
-       ADDDATE(ADDTIME(s.date, s.begin), INTERVAL IF(s.slot_id = :afterid, 1, 0) DAY) AS start,
-       ADDDATE(ADDTIME(ADDTIME(s.date, s.begin), s.length), INTERVAL IF(s.slot_id = :afterid, 1, 0) DAY) AS stop,
-       s.location_id AS l_id,
-       l.title AS l_title,
-       s.slot_id AS t_id,
-       t.title AS t_title,
-       s.application_id AS a_id,
-       a.canceled AS a_canceled,
-       a.user_id AS au_id,
-       au.pseudonym AS au_pseudonym,
-       p.rate AS p_rate,
-       p.hat AS p_hat,
-       GROUP_CONCAT(sa.user_id ORDER BY sa.user_id SEPARATOR "\\n") AS sau_id,
-       GROUP_CONCAT(sau.pseudonym ORDER BY sa.user_id SEPARATOR "\\n") AS sau_pseudonym
-FROM RapsysAirBundle:Session AS s
-JOIN RapsysAirBundle:Location AS l ON (l.id = s.location_id)
-JOIN RapsysAirBundle:Slot AS t ON (t.id = s.slot_id)
-${grantSql}JOIN RapsysAirBundle:Application AS a ON (a.id = s.application_id)
-${grantSql}JOIN RapsysAirBundle:User AS au ON (au.id = a.user_id)
-LEFT JOIN RapsysAirBundle:Application AS sa ON (sa.session_id = s.id)
-LEFT JOIN RapsysAirBundle:Snippet AS p ON (p.location_id = s.location_id AND p.user_id = a.user_id AND p.locale = :locale)
-LEFT JOIN RapsysAirBundle:User AS sau ON (sau.id = sa.user_id)
-WHERE s.date BETWEEN :begin AND :end${locationSql}
-GROUP BY s.id
-ORDER BY NULL
-SQL;
-
-               //Replace bundle entity name by table name
-               $req = str_replace(array_keys($tables), array_values($tables), $req);
-
-               //Get result set mapping instance
-               //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
-               $rsm = new ResultSetMapping();
-
-               //Declare all fields
-               //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
-               //addScalarResult($sqlColName, $resColName, $type = 'string');
-               $rsm->addScalarResult('id', 'id', 'integer')
-                       ->addScalarResult('date', 'date', 'date')
-                       ->addScalarResult('rainrisk', 'rainrisk', 'float')
-                       ->addScalarResult('rainfall', 'rainfall', 'float')
-                       ->addScalarResult('realfeel', 'realfeel', 'float')
-                       ->addScalarResult('temperature', 'temperature', 'float')
-                       ->addScalarResult('locked', 'locked', 'datetime')
-                       ->addScalarResult('start', 'start', 'datetime')
-                       ->addScalarResult('stop', 'stop', 'datetime')
-                       ->addScalarResult('t_id', 't_id', 'integer')
-                       ->addScalarResult('t_title', 't_title', 'string')
-                       ->addScalarResult('l_id', 'l_id', 'integer')
-                       ->addScalarResult('l_title', 'l_title', 'string')
-                       ->addScalarResult('a_id', 'a_id', 'integer')
-                       ->addScalarResult('a_canceled', 'a_canceled', 'datetime')
-                       ->addScalarResult('au_id', 'au_id', 'integer')
-                       ->addScalarResult('au_pseudonym', 'au_pseudonym', 'string')
-                       ->addScalarResult('p_rate', 'p_rate', 'integer')
-                       ->addScalarResult('p_hat', 'p_hat', 'boolean')
-                       //XXX: is a string because of \n separator
-                       ->addScalarResult('sau_id', 'sau_id', 'string')
-                       //XXX: is a string because of \n separator
-                       ->addScalarResult('sau_pseudonym', 'sau_pseudonym', 'string')
-                       ->addIndexByScalar('id');
-
-               //Fetch result
-               $res = $em
-                       ->createNativeQuery($req, $rsm)
-                       ->setParameter('begin', $period->getStartDate())
-                       ->setParameter('end', $period->getEndDate())
-                       ->setParameter('locale', $locale);
+               //Set route
+               $route = 'rapsysair_session_view';
+
+               //Set route params
+               $routeParams = ['id' => $id, 'location' => $this->slugger->slug($this->translator->trans($result['l_title']))];
+
+               //Set session
+               $session = [
+                       'id' => $id,
+                       'date' => $result['date'],
+                       'begin' => $result['begin'],
+                       'start' => $result['start'],
+                       'length' => $result['length'],
+                       'stop' => $result['stop'],
+                       'rainfall' => $result['rainfall'] !== null ? $result['rainfall'].' mm' : $result['rainfall'],
+                       'rainrisk' => $result['rainrisk'] !== null ? ($result['rainrisk']*100).' %' : $result['rainrisk'],
+                       'realfeel' => $result['realfeel'] !== null ? $result['realfeel'].' °C' : $result['realfeel'],
+                       'realfeelmin' => $result['realfeelmin'] !== null ? $result['realfeelmin'].' °C' : $result['realfeelmin'],
+                       'realfeelmax' => $result['realfeelmax'] !== null ? $result['realfeelmax'].' °C' : $result['realfeelmax'],
+                       'temperature' => $result['temperature'] !== null ? $result['temperature'].' °C' : $result['temperature'],
+                       'temperaturemin' => $result['temperaturemin'] !== null ? $result['temperaturemin'].' °C' : $result['temperaturemin'],
+                       'temperaturemax' => $result['temperaturemax'] !== null ? $result['temperaturemax'].' °C' : $result['temperaturemax'],
+                       'locked' => $result['locked'],
+                       'created' => $result['created'],
+                       'updated' => $result['updated'],
+                       'title' => $this->translator->trans('Session %id%', ['%id%' => $id]),
+                       'modified' => $result['modified'],
+                       'application' => null,
+                       'location' => [
+                               'id' => $result['l_id'],
+                               'at' => $this->translator->trans('at '.$result['l_title']),
+                               'title' => $locationTitle = $this->translator->trans($result['l_title']),
+                               'description' => $this->translator->trans($result['l_description']??'None'),
+                               'address' => $result['l_address'],
+                               'zipcode' => $result['l_zipcode'],
+                               'city' => $result['l_city'],
+                               'in' => $this->translator->trans('in '.$result['l_city']),
+                               'map' => $this->translator->trans($result['l_title'].' access map'),
+                               'multimap' => $this->translator->trans($result['l_title'].' sector map'),
+                               'latitude' => $result['l_latitude'],
+                               'longitude' => $result['l_longitude'],
+                               'indoor' => $result['l_indoor'],
+                               'slug' => $routeParams['location'],
+                               'link' => $this->router->generate('rapsysair_location_view', ['id' => $result['l_id'], 'location' => $routeParams['location']])
+                       ],
+                       'slot' => [
+                               'id' => $result['t_id'],
+                               'the' => $this->translator->trans('the '.lcfirst($result['t_title'])),
+                               'title' => $this->translator->trans($result['t_title'])
+                       ],
+                       'snippet' => null,
+                       'applications' => null
+               ];
 
-               //Add optional location id
-               if (!empty($locationId)) {
-                       $res->setParameter('lid', $locationId);
+               //With application
+               if (!empty($result['a_id'])) {
+                       $session['application'] = [
+                               'dance' => [
+                                       'id' => $result['ad_id'],
+                                       'title' => $this->translator->trans($result['ad_name'].' '.lcfirst($result['ad_type'])),
+                                       'name' => $this->translator->trans($result['ad_name']),
+                                       'type' => $this->translator->trans($result['ad_type']),
+                                       'slug' => $routeParams['dance'] = $this->slugger->slug($this->translator->trans($result['ad_name'].' '.lcfirst($result['ad_type']))),
+                                       'link' => $this->router->generate('rapsysair_dance_view', ['id' => $result['ad_id'], 'name' => $this->slugger->slug($this->translator->trans($result['ad_name'])), 'type' => $this->slugger->slug($this->translator->trans($result['ad_type']))])
+                               ],
+                               'user' => [
+                                       'id' => $result['au_id'],
+                                       'by' => $this->translator->trans('by %pseudonym%', [ '%pseudonym%' => $result['au_pseudonym'] ]),
+                                       'title' => $result['au_pseudonym'],
+                                       'slug' => $routeParams['user'] =  $this->slugger->slug($result['au_pseudonym']),
+                                       'link' => $result['au_id'] == 1 && $routeParams['user'] == 'milonga-raphael' ? $this->router->generate('rapsysair_user_milongaraphael') : $this->router->generate('rapsysair_user_view', ['id' => $result['au_id'], 'user' => $routeParams['user']]),
+                                       'contact' => $this->router->generate('rapsysair_contact', ['id' => $result['au_id'], 'user' => $routeParams['user']])
+                               ],
+                               'id' => $result['a_id'],
+                               'canceled' => $result['a_canceled']
+                       ];
                }
 
-               //Get result
-               $res = $res->getResult();
-
-               //Init calendar
-               $calendar = [];
-
-               //Init month
-               $month = null;
-
-               //Iterate on each day
-               foreach($period as $date) {
-                       //Init day in calendar
-                       $calendar[$Ymd = $date->format('Ymd')] = [
-                               'title' => $translator->trans($date->format('l')).' '.$date->format('d'),
-                               'class' => [],
-                               'sessions' => []
+               //With snippet
+               if (!empty($result['p_id'])) {
+                       $session['snippet'] = [
+                               'id' => $result['p_id'],
+                               'description' => $result['p_description'],
+                               'class' => $result['p_class'],
+                               'contact' => $result['p_contact'],
+                               'donate' => $result['p_donate'],
+                               'link' => $result['p_link'],
+                               'profile' => $result['p_profile'],
+                               'rate' => $result['p_rate'],
+                               'hat' => $result['p_hat']
                        ];
+               }
 
-                       //Detect month change
-                       if ($month != $date->format('m')) {
-                               $month = $date->format('m');
-                               //Append month for first day of month
-                               //XXX: except if today to avoid double add
-                               if ($date->format('U') != strtotime('today')) {
-                                       $calendar[$Ymd]['title'] .= '/'.$month;
+               //With applications
+               if (!empty($result['sa_id'])) {
+                       //Extract applications id
+                       $result['sa_id'] = explode("\n", $result['sa_id']);
+                       //Extract applications score
+                       //XXX: score may be null before grant or for bad behaviour, replace NULL with 'NULL' to avoid silent drop in mysql
+                       $result['sa_score'] = array_map(function($v){return $v==='NULL'?null:$v;}, explode("\n", $result['sa_score']));
+                       //Extract applications created
+                       $result['sa_created'] = array_map(function($v){return new \DateTime($v);}, explode("\n", $result['sa_created']));
+                       //Extract applications updated
+                       $result['sa_updated'] = array_map(function($v){return new \DateTime($v);}, explode("\n", $result['sa_updated']));
+                       //Extract applications canceled
+                       //XXX: canceled is null before cancelation, replace NULL with 'NULL' to avoid silent drop in mysql
+                       $result['sa_canceled'] = array_map(function($v){return $v==='NULL'?null:new \DateTime($v);}, explode("\n", $result['sa_canceled']));
+
+                       //Extract applications user id
+                       $result['sau_id'] = explode("\n", $result['sau_id']);
+                       //Extract applications user pseudonym
+                       $result['sau_pseudonym'] = explode("\n", $result['sau_pseudonym']);
+
+                       //Init applications
+                       $session['applications'] = [];
+
+                       //Iterate on each applications id
+                       foreach($result['sa_id'] as $i => $sa_id) {
+                               $session['applications'][$sa_id] = [
+                                       'user' => null,
+                                       'score' => $result['sa_score'][$i],
+                                       'created' => $result['sa_created'][$i],
+                                       'updated' => $result['sa_updated'][$i],
+                                       'canceled' => $result['sa_canceled'][$i]
+                               ];
+                               if (!empty($result['sau_id'][$i])) {
+                                       $session['applications'][$sa_id]['user'] = [
+                                               'id' => $result['sau_id'][$i],
+                                               'title' => $result['sau_pseudonym'][$i],
+                                               'slug' => $this->slugger->slug($result['sau_pseudonym'][$i])
+                                       ];
                                }
                        }
-                       //Deal with today
-                       if ($date->format('U') == ($today = strtotime('today'))) {
-                               $calendar[$Ymd]['title'] .= '/'.$month;
-                               $calendar[$Ymd]['current'] = true;
-                               $calendar[$Ymd]['class'][] = 'current';
-                       }
-                       //Disable passed days
-                       if ($date->format('U') < $today) {
-                               $calendar[$Ymd]['disabled'] = true;
-                               $calendar[$Ymd]['class'][] = 'disabled';
-                       }
-                       //Set next month days
-                       if ($date->format('m') > date('m')) {
-                               $calendar[$Ymd]['next'] = true;
-                               #$calendar[$Ymd]['class'][] = 'next';
-                       }
-
-                       //Detect sunday
-                       if ($date->format('w') == 0) {
-                               $calendar[$Ymd]['class'][] = 'sunday';
-                       }
-
-                       //Iterate on each session to find the one of the day
-                       foreach($res as $session) {
-                               if (($sessionYmd = $session['date']->format('Ymd')) == $Ymd) {
-                                       //Count number of application
-                                       $count = count(explode("\n", $session['sau_id']));
-
-                                       //Compute classes
-                                       $class = [];
-                                       if (!empty($session['a_id'])) {
-                                               $applications = [ $session['au_id'] => $session['au_pseudonym'] ];
-                                               if (!empty($session['a_canceled'])) {
-                                                       $class[] = 'canceled';
-                                               } else {
-                                                       $class[] = 'granted';
-                                               }
-                                       } elseif ($count > 1) {
-                                               $class[] = 'disputed';
-                                       } elseif (!empty($session['locked'])) {
-                                               $class[] = 'locked';
-                                       } else {
-                                               $class[] = 'pending';
-                                       }
-
-                                       if ($sessionId == $session['id']) {
-                                               $class[] = 'highlight';
-                                       }
-
-                                       //Set temperature
-                                       //XXX: realfeel may be null, temperature should not
-                                       $temperature = $session['realfeel'] !== null ? $session['realfeel'] : $session['temperature'];
-
-                                       //Compute weather
-                                       //XXX: rainfall may be null
-                                       if ($session['rainrisk'] > 0.50 || $session['rainfall'] > 2) {
-                                               $weather = self::GLYPHS['Stormy'];
-                                       } elseif ($session['rainrisk'] > 0.40 || $session['rainfall'] > 1) {
-                                               $weather = self::GLYPHS['Rainy'];
-                                       } elseif ($temperature > 24) {
-                                               $weather = self::GLYPHS['Cleary'];
-                                       } elseif ($temperature > 17) {
-                                               $weather = self::GLYPHS['Sunny'];
-                                       } elseif ($temperature > 10) {
-                                               $weather = self::GLYPHS['Cloudy'];
-                                       } elseif ($temperature !== null) {
-                                               $weather = self::GLYPHS['Winty'];
-                                       } else {
-                                               $weather = null;
-                                       }
-
-                                       //Init weathertitle
-                                       $weathertitle = [];
-
-                                       //Check if realfeel is available
-                                       if ($session['realfeel'] !== null) {
-                                               $weathertitle[] = $session['realfeel'].'°R';
-                                       }
-
-                                       //Check if temperature is available
-                                       if ($session['temperature'] !== null) {
-                                               $weathertitle[] = $session['temperature'].'°C';
-                                       }
+               }
 
-                                       //Check if rainrisk is available
-                                       if ($session['rainrisk'] !== null) {
-                                               $weathertitle[] = ($session['rainrisk']*100).'%';
-                                       }
+               //Set link
+               $session['link'] = $this->router->generate($route, $routeParams);
 
-                                       //Check if rainfall is available
-                                       if ($session['rainfall'] !== null) {
-                                               $weathertitle[] = $session['rainfall'].'mm';
-                                       }
+               //Set canonical
+               $session['canonical'] = $this->router->generate($route, $routeParams, UrlGeneratorInterface::ABSOLUTE_URL);
 
-                                       //Set applications
-                                       $applications = [
-                                               0 => $translator->trans($session['t_title']).' '.$translator->trans('at '.$session['l_title']).$translator->trans(':')
-                                       ];
+               //Set alternates
+               $session['alternates'] = [];
 
-                                       //Fetch pseudonyms from session applications
-                                       $applications += array_combine(explode("\n", $session['sau_id']), array_map(function ($v) {return '- '.$v;}, explode("\n", $session['sau_pseudonym'])));
+               //Iterate on each locales
+               foreach($this->translator->getFallbackLocales() as $fallback) {
+                       //Set titles
+                       $titles = [];
 
-                                       //Set pseudonym
-                                       $pseudonym = null;
+                       //Set route params location
+                       $routeParams['location'] = $this->slugger->slug($this->translator->trans($result['l_title'], [], null, $fallback));
 
-                                       //Check that session is not granted
-                                       if (empty($session['a_id'])) {
-                                               //With location id and unique application
-                                               if ($count == 1) {
-                                                       //Set unique application pseudonym
-                                                       $pseudonym = $session['sau_pseudonym'];
-                                               }
-                                       //Session is granted
-                                       } else {
-                                               //Replace granted application
-                                               $applications[$session['au_id']] = '* '.$session['au_pseudonym'];
+                       //With route params dance
+                       if (!empty($routeParams['dance'])) {
+                              $routeParams['dance'] = $this->slugger->slug($this->translator->trans($result['ad_name'].' '.lcfirst($result['ad_type']), [], null, $fallback));
+                       }
 
-                                               //Set pseudonym
-                                               $pseudonym = $session['au_pseudonym'].($count > 1 ? ' ['.$count.']':'');
-                                       }
+                       //With route params user
+                       if (!empty($routeParams['user'])) {
+                              $routeParams['user'] = $this->slugger->slug($result['au_pseudonym']);
+                       }
 
-                                       //Add the session
-                                       $calendar[$Ymd]['sessions'][$session['t_id'].sprintf('%05d', $session['id'])] = [
-                                               'id' => $session['id'],
-                                               'start' => $session['start'],
-                                               'stop' => $session['stop'],
-                                               'location' => $translator->trans($session['l_title']),
-                                               'pseudonym' => $pseudonym,
-                                               'class' => $class,
-                                               'slot' => self::GLYPHS[$session['t_title']],
-                                               'slottitle' => $translator->trans($session['t_title']),
-                                               'weather' => $weather,
-                                               'weathertitle' => implode(' ', $weathertitle),
-                                               'applications' => $applications,
-                                               'rate' => $session['p_rate'],
-                                               'hat' => $session['p_hat']
-                                       ];
+                       //With current locale
+                       if ($fallback === $this->locale) {
+                               //Set current locale title
+                               $titles[$this->locale] = $this->translator->trans($this->languages[$this->locale]);
+                       //Without current locale
+                       } else {
+                               //Iterate on other locales
+                               foreach(array_diff($this->translator->getFallbackLocales(), [$fallback]) as $other) {
+                                       //Set other locale title
+                                       $titles[$other] = $this->translator->trans($this->languages[$fallback], [], null, $other);
                                }
+
+                               //Add alternates locale
+                               $session['alternates'][str_replace('_', '-', $fallback)] = [
+                                       'absolute' => $this->router->generate($route, ['_locale' => $fallback]+$routeParams, UrlGeneratorInterface::ABSOLUTE_URL),
+                                       'relative' => $this->router->generate($route, ['_locale' => $fallback]+$routeParams),
+                                       'title' => implode('/', $titles),
+                                       'translated' => $this->translator->trans($this->languages[$fallback], [], null, $fallback)
+                               ];
                        }
 
-                       //Sort sessions
-                       ksort($calendar[$Ymd]['sessions']);
+                       //Add alternates shorter locale
+                       if (empty($parameters['alternates'][$shortFallback = substr($fallback, 0, 2)])) {
+                               //Set locale locales context
+                               $session['alternates'][$shortFallback] = [
+                                       'absolute' => $this->router->generate($route, ['_locale' => $fallback]+$routeParams, UrlGeneratorInterface::ABSOLUTE_URL),
+                                       'relative' => $this->router->generate($route, ['_locale' => $fallback]+$routeParams),
+                                       'title' => implode('/', $titles),
+                                       'translated' => $this->translator->trans($this->languages[$fallback], [], null, $fallback)
+                               ];
+                       }
                }
 
-               //Send result
-               return $calendar;
+               //Return session
+               return $session;
        }
 
        /**
-        * Fetch sessions calendar with translated location by date period and user
+        * Find sessions as calendar array by date period
         *
-        * @param $translator The TranslatorInterface instance
-        * @param $period The date period
-        * @param $userId The user id
-        * @param $sessionId The session id
+        * @param DatePeriod $period The date period
+        * @param ?bool $granted The session is granted
+        * @param ?float $latitude The latitude
+        * @param ?float $longitude The longitude
+        * @param ?int $userId The user id
+        * @return array The session data
         */
-       public function fetchUserCalendarByDatePeriod(TranslatorInterface $translator, $period, $userId = null, $sessionId = null, $locale = null) {
-               //Get entity manager
-               $em = $this->getEntityManager();
-
-               //Get quote strategy
-               $qs = $em->getConfiguration()->getQuoteStrategy();
-               $dp = $em->getConnection()->getDatabasePlatform();
-
-               //Get quoted table names
-               //XXX: this allow to make this code table name independent
-               $tables = [
-                       'RapsysAirBundle:GroupUser' => $qs->getJoinTableName($em->getClassMetadata('RapsysAirBundle:User')->getAssociationMapping('groups'), $em->getClassMetadata('RapsysAirBundle:User'), $dp),
-                       'RapsysAirBundle:Session' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Session'), $dp),
-                       'RapsysAirBundle:Application' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Application'), $dp),
-                       'RapsysAirBundle:Group' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Group'), $dp),
-                       'RapsysAirBundle:Location' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Location'), $dp),
-                       'RapsysAirBundle:Slot' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Slot'), $dp),
-                       'RapsysAirBundle:Snippet' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Snippet'), $dp),
-                       'RapsysAirBundle:User' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:User'), $dp),
-                       ':afterid' => 4,
-                       "\t" => '',
-                       "\n" => ' '
-               ];
+       public function findAllByPeriodAsCalendarArray(\DatePeriod $period, ?bool $granted = null, ?float $latitude = null, ?float $longitude = null, ?int $userId = null): array {
+               //Init granted sql
+               $grantSql = '';
 
-               //Init user sql
-               $userJoinSql = $userWhereSql = '';
+               //When granted is set
+               if (empty($granted)) {
+                       //Set application and user as optional
+                       $grantSql = 'LEFT ';
+               }
+
+               //Init location sql
+               $locationSql = '';
+
+               //When latitude and longitude
+               if ($latitude !== null && $longitude !== null) {
+                       //Set the request
+                       //XXX: get every location between 0 and 15 km of latitude and longitude
+                       $req = <<<SQL
+SELECT l.id
+FROM Rapsys\AirBundle\Entity\Location AS l
+WHERE ACOS(SIN(RADIANS(:latitude))*SIN(RADIANS(l.latitude))+COS(RADIANS(:latitude))*COS(RADIANS(l.latitude))*COS(RADIANS(:longitude - l.longitude)))*40030.17/2/PI() BETWEEN 0 AND 15
+SQL;
 
-               //When user id is set
-               if (!empty($userId)) {
-                       //Add user join
-                       $userJoinSql = 'JOIN RapsysAirBundle:Application AS sua ON (sua.session_id = s.id)'."\n";
-                       //Add user id clause
-                       $userWhereSql = "\n\t".'AND sua.user_id = :uid';
+                       //Replace bundle entity name by table name
+                       $req = str_replace($this->tableKeys, $this->tableValues, $req);
+
+                       //Get result set mapping instance
+                       //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
+                       $rsm = new ResultSetMapping();
+
+                       //Declare all fields
+                       //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
+                       //addScalarResult($sqlColName, $resColName, $type = 'string');
+                       $rsm->addScalarResult('id', 'id', 'integer')
+                              ->addIndexByScalar('id');
+
+                       //Set location ids
+                       //XXX: check that latitude and longitude have not be swapped !!!
+                       //XXX: latitude ~= 48.x longitude ~= 2.x
+                       $locationIds = array_keys(
+                               $this->_em
+                                       ->createNativeQuery($req, $rsm)
+                                       ->setParameter('latitude', $latitude)
+                                       ->setParameter('longitude', $longitude)
+                                       ->getArrayResult()
+                       );
+
+                       //Add location id clause
+                       $locationSql = "\n\t".'AND s.location_id IN (:lids)';
+               //When user id
+               } elseif ($userId !== null) {
+                       //Set the request
+                       //XXX: get every location between 0 and 15 km
+                       $req = <<<SQL
+SELECT l2.id
+FROM (
+       SELECT l.id, l.latitude, l.longitude
+       FROM Rapsys\AirBundle\Entity\Application AS a
+       JOIN Rapsys\AirBundle\Entity\Session AS s ON (s.id = a.session_id)
+       JOIN Rapsys\AirBundle\Entity\Location AS l ON (l.id = s.location_id)
+       WHERE a.user_id = :id
+       GROUP BY l.id
+       ORDER BY NULL
+       LIMIT 0, :limit
+) AS a
+JOIN Rapsys\AirBundle\Entity\Location AS l2
+WHERE ACOS(SIN(RADIANS(a.latitude))*SIN(RADIANS(l2.latitude))+COS(RADIANS(a.latitude))*COS(RADIANS(l2.latitude))*COS(RADIANS(a.longitude - l2.longitude)))*40030.17/2/PI() BETWEEN 0 AND 15
+GROUP BY l2.id
+ORDER BY NULL
+SQL;
+
+                       //Replace bundle entity name by table name
+                       $req = str_replace($this->tableKeys, $this->tableValues, $req);
+
+                       //Get result set mapping instance
+                       //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
+                       $rsm = new ResultSetMapping();
+
+                       //Declare all fields
+                       //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
+                       //addScalarResult($sqlColName, $resColName, $type = 'string');
+                       $rsm->addScalarResult('id', 'id', 'integer')
+                              ->addIndexByScalar('id');
+
+                       //Set location ids
+                       $locationIds = array_keys(
+                               $this->_em
+                                       ->createNativeQuery($req, $rsm)
+                                       ->setParameter('id', $userId)
+                                       ->getArrayResult()
+                       );
+
+                       //With location ids
+                       if (!empty($locationIds)) {
+                               //Add location id clause
+                               $locationSql = "\n\t".'AND s.location_id IN (:lids)';
+                       }
                }
 
                //Set the request
-               //TODO: change as_u_* in sau_*, a_u_* in au_*, etc, see request up
                $req = <<<SQL
+
 SELECT
        s.id,
        s.date,
@@ -805,30 +535,47 @@ SELECT
        ADDDATE(ADDTIME(ADDTIME(s.date, s.begin), s.length), INTERVAL IF(s.slot_id = :afterid, 1, 0) DAY) AS stop,
        s.location_id AS l_id,
        l.title AS l_title,
+       l.address AS l_address,
+       l.zipcode AS l_zipcode,
+       l.city AS l_city,
+       l.latitude AS l_latitude,
+       l.longitude AS l_longitude,
+       l.indoor AS l_indoor,
        s.slot_id AS t_id,
        t.title AS t_title,
        s.application_id AS a_id,
+       a.canceled AS a_canceled,
+       a.dance_id AS ad_id,
+       ad.name AS ad_name,
+       ad.type AS ad_type,
        a.user_id AS au_id,
        au.pseudonym AS au_pseudonym,
-       p.rate AS p_rate,
        p.hat AS p_hat,
+       p.rate AS p_rate,
+       p.short AS p_short,
+       GREATEST(s.created, s.updated, l.created, l.updated, t.created, t.updated, COALESCE(a.created, '1970-01-01'), COALESCE(a.updated, '1970-01-01'), COALESCE(ad.created, '1970-01-01'), COALESCE(ad.updated, '1970-01-01'), COALESCE(au.created, '1970-01-01'), COALESCE(au.updated, '1970-01-01'), COALESCE(p.created, '1970-01-01'), COALESCE(p.updated, '1970-01-01'), MAX(GREATEST(COALESCE(sa.created, '1970-01-01'), COALESCE(sa.updated, '1970-01-01'), COALESCE(sad.created, '1970-01-01'), COALESCE(sad.updated, '1970-01-01'), COALESCE(sau.created, '1970-01-01'), COALESCE(sau.updated, '1970-01-01')))) AS modified,
+       GROUP_CONCAT(sa.dance_id ORDER BY sa.user_id SEPARATOR "\\n") AS sad_id,
+       GROUP_CONCAT(sad.name ORDER BY sa.user_id SEPARATOR "\\n") AS sad_name,
+       GROUP_CONCAT(sad.type ORDER BY sa.user_id SEPARATOR "\\n") AS sad_type,
        GROUP_CONCAT(sa.user_id ORDER BY sa.user_id SEPARATOR "\\n") AS sau_id,
-       GROUP_CONCAT(CONCAT("- ", sau.pseudonym) ORDER BY sa.user_id SEPARATOR "\\n") AS sau_pseudonym
-FROM RapsysAirBundle:Session AS s
-JOIN RapsysAirBundle:Location AS l ON (l.id = s.location_id)
-JOIN RapsysAirBundle:Slot AS t ON (t.id = s.slot_id)
-${userJoinSql}LEFT JOIN RapsysAirBundle:Application AS a ON (a.id = s.application_id)
-LEFT JOIN RapsysAirBundle:Snippet AS p ON (p.location_id = s.location_id AND p.user_id = a.user_id AND p.locale = :locale)
-LEFT JOIN RapsysAirBundle:User AS au ON (au.id = a.user_id)
-LEFT JOIN RapsysAirBundle:Application AS sa ON (sa.session_id = s.id)
-LEFT JOIN RapsysAirBundle:User AS sau ON (sau.id = sa.user_id)
-WHERE s.date BETWEEN :begin AND :end${userWhereSql}
+       GROUP_CONCAT(sau.pseudonym ORDER BY sa.user_id SEPARATOR "\\n") AS sau_pseudonym
+FROM Rapsys\AirBundle\Entity\Session AS s
+JOIN Rapsys\AirBundle\Entity\Location AS l ON (l.id = s.location_id)
+JOIN Rapsys\AirBundle\Entity\Slot AS t ON (t.id = s.slot_id)
+{$grantSql}JOIN Rapsys\AirBundle\Entity\Application AS a ON (a.id = s.application_id)
+{$grantSql}JOIN Rapsys\AirBundle\Entity\Dance AS ad ON (ad.id = a.dance_id)
+{$grantSql}JOIN Rapsys\AirBundle\Entity\User AS au ON (au.id = a.user_id)
+LEFT JOIN Rapsys\AirBundle\Entity\Snippet AS p ON (p.locale = :locale AND p.location_id = s.location_id AND p.user_id = a.user_id)
+LEFT JOIN Rapsys\AirBundle\Entity\Application AS sa ON (sa.session_id = s.id)
+LEFT JOIN Rapsys\AirBundle\Entity\Dance AS sad ON (sad.id = sa.dance_id)
+LEFT JOIN Rapsys\AirBundle\Entity\User AS sau ON (sau.id = sa.user_id)
+WHERE s.date BETWEEN :begin AND :end{$locationSql}
 GROUP BY s.id
 ORDER BY NULL
 SQL;
 
                //Replace bundle entity name by table name
-               $req = str_replace(array_keys($tables), array_values($tables), $req);
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
 
                //Get result set mapping instance
                //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
@@ -846,15 +593,33 @@ SQL;
                        ->addScalarResult('locked', 'locked', 'datetime')
                        ->addScalarResult('start', 'start', 'datetime')
                        ->addScalarResult('stop', 'stop', 'datetime')
+                       ->addScalarResult('modified', 'modified', 'datetime')
                        ->addScalarResult('t_id', 't_id', 'integer')
                        ->addScalarResult('t_title', 't_title', 'string')
                        ->addScalarResult('l_id', 'l_id', 'integer')
                        ->addScalarResult('l_title', 'l_title', 'string')
+                       ->addScalarResult('l_address', 'l_address', 'string')
+                       ->addScalarResult('l_zipcode', 'l_zipcode', 'string')
+                       ->addScalarResult('l_city', 'l_city', 'string')
+                       ->addScalarResult('l_latitude', 'l_latitude', 'float')
+                       ->addScalarResult('l_longitude', 'l_longitude', 'float')
+                       ->addScalarResult('l_indoor', 'l_indoor', 'boolean')
                        ->addScalarResult('a_id', 'a_id', 'integer')
+                       ->addScalarResult('a_canceled', 'a_canceled', 'datetime')
+                       ->addScalarResult('ad_id', 'ad_id', 'string')
+                       ->addScalarResult('ad_name', 'ad_name', 'string')
+                       ->addScalarResult('ad_type', 'ad_type', 'string')
                        ->addScalarResult('au_id', 'au_id', 'integer')
                        ->addScalarResult('au_pseudonym', 'au_pseudonym', 'string')
-                       ->addScalarResult('p_rate', 'p_rate', 'integer')
                        ->addScalarResult('p_hat', 'p_hat', 'boolean')
+                       ->addScalarResult('p_rate', 'p_rate', 'integer')
+                       ->addScalarResult('p_short', 'p_short', 'string')
+                       //XXX: is a string because of \n separator
+                       ->addScalarResult('sad_id', 'sad_id', 'string')
+                       //XXX: is a string because of \n separator
+                       ->addScalarResult('sad_name', 'sad_name', 'string')
+                       //XXX: is a string because of \n separator
+                       ->addScalarResult('sad_type', 'sad_type', 'string')
                        //XXX: is a string because of \n separator
                        ->addScalarResult('sau_id', 'sau_id', 'string')
                        //XXX: is a string because of \n separator
@@ -862,13 +627,18 @@ SQL;
                        ->addIndexByScalar('id');
 
                //Fetch result
-               $res = $em
+               $res = $this->_em
                        ->createNativeQuery($req, $rsm)
                        ->setParameter('begin', $period->getStartDate())
-                       ->setParameter('end', $period->getEndDate())
-                       ->setParameter('uid', $userId)
-                       ->setParameter('locale', $locale)
-                       ->getResult();
+                       ->setParameter('end', $period->getEndDate());
+
+               //Add optional location ids
+               if (!empty($locationIds)) {
+                       $res->setParameter('lids', $locationIds);
+               }
+
+               //Get result
+               $result = $res->getResult();
 
                //Init calendar
                $calendar = [];
@@ -876,11 +646,15 @@ SQL;
                //Init month
                $month = null;
 
+               //Set route
+               $route = 'rapsysair_session_view';
+
                //Iterate on each day
                foreach($period as $date) {
                        //Init day in calendar
                        $calendar[$Ymd = $date->format('Ymd')] = [
-                               'title' => $translator->trans($date->format('l')).' '.$date->format('d'),
+                               'title' => $this->translator->trans($date->format('l')).' '.$date->format('d'),
+                               'modified' => null,
                                'class' => [],
                                'sessions' => []
                        ];
@@ -917,122 +691,238 @@ SQL;
                        }
 
                        //Iterate on each session to find the one of the day
-                       foreach($res as $session) {
+                       foreach($result as $session) {
                                if (($sessionYmd = $session['date']->format('Ymd')) == $Ymd) {
-                                       //Count number of application
-                                       $count = count(explode("\n", $session['sau_id']));
+                                       //With empty or greatest modified
+                                       if ($calendar[$Ymd]['modified'] === null || $session['modified'] >= $calendar[$Ymd]['modified']) {
+                                               //Update modified
+                                               $calendar[$Ymd]['modified'] = $session['modified'];
+                                       }
+
+                                       //Set applications
+                                       $applications = array_combine($candidates = explode("\n", $session['sau_id']), explode("\n", $session['sau_pseudonym']));
 
                                        //Compute classes
                                        $class = [];
-                                       if (!empty($session['a_id'])) {
-                                               $applications = [ $session['au_id'] => $session['au_pseudonym'] ];
-                                               if ($session['au_id'] == $userId) {
-                                                       $class[] = 'granted';
-                                               } else {
-                                                       $class[] = 'disputed';
-                                               }
-                                       } elseif ($count > 1) {
-                                               $class[] = 'disputed';
-                                       } elseif (!empty($session['locked'])) {
+
+                                       //With locked
+                                       if (!empty($session['locked'])) {
                                                $class[] = 'locked';
+                                       //Without locked
                                        } else {
-                                               $class[] = 'pending';
-                                       }
+                                               //With application
+                                               if (!empty($session['a_id'])) {
+                                                       //With canceled session
+                                                       if (!empty($session['a_canceled'])) {
+                                                               $class[] = 'canceled';
+                                                       //With disputed session
+                                                       } elseif ($userId !== null && $session['au_id'] != $userId && !empty($candidates[$userId])) {
+                                                               $class[] = 'disputed';
+                                                       //Session is granted
+                                                       } else {
+                                                               $class[] = 'granted';
+                                                       }
+
+                                                       //With user id
+                                                       if ($userId !== null && $session['au_id'] == $userId) {
+                                                               $class[] = 'highlight';
+                                                       }
+                                               } else {
+                                                       $class[] = 'pending';
+                                               }
 
-                                       if ($sessionId == $session['id']) {
-                                               $class[] = 'highlight';
+                                               //With latitude and longitude
+                                               if ($latitude !== null && $longitude !== null && $session['l_latitude'] == $latitude && $session['l_longitude'] == $longitude) {
+                                                       $class[] = 'highlight';
+                                               }
                                        }
 
                                        //Set temperature
-                                       //XXX: realfeel may be null, temperature should not
-                                       $temperature = $session['realfeel'] !== null ? $session['realfeel'] : $session['temperature'];
+                                       $temperature = [
+                                               'glyph' => self::GLYPHS['Cleary'],
+                                               'title' => []
+                                       ];
 
-                                       //Compute weather
-                                       //XXX: rainfall may be null
-                                       if ($session['rainrisk'] > 0.50 || $session['rainfall'] > 2) {
-                                               $weather = self::GLYPHS['Stormy'];
-                                       } elseif ($session['rainrisk'] > 0.40 || $session['rainfall'] > 1) {
-                                               $weather = self::GLYPHS['Rainy'];
-                                       } elseif ($temperature > 24) {
-                                               $weather = self::GLYPHS['Cleary'];
-                                       } elseif ($temperature > 17) {
-                                               $weather = self::GLYPHS['Sunny'];
-                                       } elseif ($temperature > 10) {
-                                               $weather = self::GLYPHS['Cloudy'];
-                                       } elseif ($temperature !== null) {
-                                               $weather = self::GLYPHS['Winty'];
-                                       } else {
-                                               $weather = null;
+                                       //Compute temperature glyph
+                                       //XXX: temperature may be null
+                                       if ($session['temperature'] >= 17 && $session['temperature'] < 24) {
+                                               $temperature['glyph'] = self::GLYPHS['Sunny'];
+                                       } elseif ($session['temperature'] >= 10 && $session['temperature'] < 17) {
+                                               $temperature['glyph'] = self::GLYPHS['Cloudy'];
+                                       } elseif ($session['temperature'] !== null && $session['temperature'] < 10) {
+                                               $temperature['glyph'] = self::GLYPHS['Winty'];
                                        }
 
-                                       //Init weathertitle
-                                       $weathertitle = [];
+                                       //Check if temperature is available
+                                       if ($session['temperature'] !== null) {
+                                               $temperature['title'][] = $session['temperature'].'°C';
+                                       }
 
                                        //Check if realfeel is available
                                        if ($session['realfeel'] !== null) {
-                                               $weathertitle[] = $session['realfeel'].'°R';
+                                               $temperature['title'][] = $session['realfeel'].'°R';
                                        }
 
-                                       //Check if temperature is available
-                                       if ($session['temperature'] !== null) {
-                                               $weathertitle[] = $session['temperature'].'°C';
+                                       //Compute temperature title
+                                       $temperature['title'] = implode(' ', $temperature['title']);
+
+                                       //Set rain
+                                       $rain = [
+                                               'glyph' => self::GLYPHS['Cleary'],
+                                               'title' => []
+                                       ];
+
+                                       //Compute rain glyph
+                                       //XXX: rainfall and rainrisk may be null
+                                       if ($session['rainrisk'] > 0.50 || $session['rainfall'] > 2) {
+                                               $rain['glyph'] = self::GLYPHS['Stormy'];
+                                       } elseif ($session['rainrisk'] > 0.40 || $session['rainfall'] > 1) {
+                                               $rain['glyph'] = self::GLYPHS['Rainy'];
                                        }
 
                                        //Check if rainrisk is available
                                        if ($session['rainrisk'] !== null) {
-                                               $weathertitle[] = ($session['rainrisk']*100).'%';
+                                               $rain['title'][] = ($session['rainrisk']*100).'%';
                                        }
 
                                        //Check if rainfall is available
                                        if ($session['rainfall'] !== null) {
-                                               $weathertitle[] = $session['rainfall'].'mm';
+                                               $rain['title'][] = $session['rainfall'].'mm';
                                        }
 
-                                       //Set applications
-                                       $applications = [
-                                               0 => $translator->trans($session['t_title']).' '.$translator->trans('at '.$session['l_title']).$translator->trans(':')
-                                       ];
+                                       //Compute rain title
+                                       $rain['title'] = implode(' ', $rain['title']);
+
+                                       //Set application
+                                       $application = null;
+
+                                       //Set rate
+                                       $rate = null;
+
+                                       //Set route params
+                                       $routeParams = ['id' => $session['id'], 'location' => $this->slugger->slug($this->translator->trans($session['l_title']))];
 
-                                       //Fetch pseudonyms from session applications
-                                       $applications += array_combine(explode("\n", $session['sau_id']), array_map(function ($v) {return '- '.$v;}, explode("\n", $session['sau_pseudonym'])));
+                                       //With application
+                                       if (!empty($session['a_id'])) {
+                                               //Set dance
+                                               $routeParams['dance'] = $this->slugger->slug($dance = $this->translator->trans($session['ad_name'].' '.lcfirst($session['ad_type'])));
+
+                                               //Set user
+                                               $routeParams['user'] =  $this->slugger->slug($session['au_pseudonym']);
 
-                                       //Set pseudonym
-                                       $pseudonym = null;
+                                               //Set title
+                                               $title = $this->translator->trans('%dance% %id% by %pseudonym% %location% %city%', ['%dance%' => $dance, '%id%' => $session['id'], '%pseudonym%' => $session['au_pseudonym'], '%location%' => $this->translator->trans('at '.$session['l_title']), '%city%' => $this->translator->trans('in '.$session['l_city'])]);
 
-                                       //Check that session is not granted
-                                       if (empty($session['a_id'])) {
-                                               //With location id and unique application
-                                               if ($count == 1) {
-                                                       //Set unique application pseudonym
-                                                       $pseudonym = $session['sau_pseudonym'];
+                                               //Set pseudonym
+                                               $application = [
+                                                       'dance' => [
+                                                               'id' => $session['ad_id'],
+                                                               'name' => $this->translator->trans($session['ad_name']),
+                                                               'type' => $this->translator->trans($session['ad_type']),
+                                                               'title' => $dance
+                                                       ],
+                                                       'user' => [
+                                                               'id' => $session['au_id'],
+                                                               'title' => $session['au_pseudonym']
+                                                       ]
+                                               ];
+
+                                               //Set rate
+                                               $rate = [
+                                                       'glyph' => self::GLYPHS['Free'],
+                                                       'rate' => null,
+                                                       'short' => $session['p_short'],
+                                                       'title' => $this->translator->trans('Free')
+                                               ];
+
+                                               //With hat
+                                               if (!empty($session['p_hat'])) {
+                                                       //Set glyph
+                                                       $rate['glyph'] = self::GLYPHS['Hat'];
+
+                                                       //With rate
+                                                       if (!empty($session['p_rate'])) {
+                                                               //Set rate
+                                                               $rate['rate'] = $session['p_rate'];
+
+                                                               //Set title
+                                                               $rate['title'] = $this->translator->trans('%rate%€ to the hat', ['%rate%' => $session['p_rate']]);
+                                                       //Without rate
+                                                       } else {
+                                                               //Set title
+                                                               $rate['title'] = $this->translator->trans('To the hat');
+                                                       }
+                                               //With rate
+                                               } elseif (!empty($session['p_rate'])) {
+                                                       //Set glyph
+                                                       $rate['glyph'] = self::GLYPHS['Euro'];
+
+                                                       //Set rate
+                                                       $rate['rate'] = $session['p_rate'];
+
+                                                       //Set title
+                                                       $rate['title'] = $session['p_rate'].' €';
                                                }
-                                       //Session is granted
-                                       } else {
-                                               //Replace granted application
-                                               $applications[$session['au_id']] = '* '.$session['au_pseudonym'];
+                                       //With unique application
+                                       } elseif (count($applications) == 1) {
+                                               //Set dance
+                                               $dance = $this->translator->trans($session['sad_name'].' '.lcfirst($session['sad_type']));
+
+                                               //Set title
+                                               $title = $this->translator->trans('%dance% %id% by %pseudonym% %location% %city%', ['%dance%' => $dance, '%id%' => $session['id'], '%pseudonym%' => $session['sau_pseudonym'], '%location%' => $this->translator->trans('at '.$session['l_title']), '%city%' => $this->translator->trans('in '.$session['l_city'])]);
 
                                                //Set pseudonym
-                                               $pseudonym = $session['au_pseudonym'].($count > 1 ? ' ['.$count.']':'');
+                                               $application = [
+                                                       'dance' => [
+                                                               'id' => $session['sad_id'],
+                                                               'name' => $this->translator->trans($session['sad_name']),
+                                                               'type' => $this->translator->trans($session['sad_type']),
+                                                               'title' => $dance
+                                                       ],
+                                                       'user' => [
+                                                               'id' => $session['sau_id'],
+                                                               'title' => $session['sau_pseudonym']
+                                                       ]
+                                               ];
+
+                                               //TODO: glyph stuff ???
+                                       //Without application
+                                       } else {
+                                               //Set title
+                                               $title = $this->translator->trans('%slot% %id% %location%', ['%slot%' => $this->translator->trans($session['t_title']), '%id%' => $session['id'], '%location%' => $this->translator->trans('at '.$session['l_title'])]);
                                        }
 
-                                       //Set title
-                                       $title = $translator->trans($session['l_title']).($count > 1 ? ' ['.$count.']':'');
-
                                        //Add the session
-                                       $calendar[$Ymd]['sessions'][$session['t_id'].sprintf('%02d', $session['l_id'])] = [
+                                       $calendar[$Ymd]['sessions'][$session['t_id'].sprintf('%05d', $session['id'])] = [
                                                'id' => $session['id'],
                                                'start' => $session['start'],
                                                'stop' => $session['stop'],
-                                               'location' => $translator->trans($session['l_title']),
-                                               'pseudonym' => $pseudonym,
                                                'class' => $class,
-                                               'slot' => self::GLYPHS[$session['t_title']],
-                                               'slottitle' => $translator->trans($session['t_title']),
-                                               'weather' => $weather,
-                                               'weathertitle' => implode(' ', $weathertitle),
-                                               'applications' => $applications,
-                                               'rate' => $session['p_rate'],
-                                               'hat' => $session['p_hat']
+                                               'temperature' => $temperature,
+                                               'rain' => $rain,
+                                               'title' => $title,
+                                               'link' => $this->router->generate($route, $routeParams),
+                                               'location' => [
+                                                       'id' => $session['l_id'],
+                                                       'title' => $this->translator->trans($session['l_title']),
+                                                       'address' => $session['l_address'],
+                                                       'latitude' => $session['l_latitude'],
+                                                       'longitude' => $session['l_longitude'],
+                                                       'indoor' => $session['l_indoor'],
+                                                       'at' => $at = $this->translator->trans('at '.$session['l_title']),
+                                                       'in' => $in = $this->translator->trans('in '.$session['l_city']),
+                                                       'atin' => $at.' '.$in,
+                                                       'city' => $session['l_city'],
+                                                       'zipcode' => $session['l_zipcode']
+                                               ],
+                                               'application' => $application,
+                                               'slot' => [
+                                                       'glyph' => self::GLYPHS[$session['t_title']],
+                                                       'title' => $this->translator->trans($session['t_title'])
+                                               ],
+                                               'rate' => $rate,
+                                               'modified' => $session['modified'],
+                                               'applications' => $applications
                                        ];
                                }
                        }
@@ -1045,50 +935,324 @@ SQL;
                return $calendar;
        }
 
+
        /**
-        * Find all session pending hourly weather
+        * Find sessions by user id and synchronized date time
         *
-        * @return array<Session> The sessions to update
+        * @param int $userId The user id
+        * @param DateTime $synchronized The synchronized datetime
+        * @return array The session data
         */
-       public function findAllPendingHourlyWeather() {
-               //Get entity manager
-               $em = $this->getEntityManager();
-
-               //Get quote strategy
-               $qs = $em->getConfiguration()->getQuoteStrategy();
-               $dp = $em->getConnection()->getDatabasePlatform();
-
-               //Get quoted table names
-               //XXX: this allow to make this code table name independent
-               $tables = [
-                       'RapsysAirBundle:Session' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Session'), $dp),
-                       'RapsysAirBundle:Location' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Location'), $dp),
-                       //Accuweather
-                       ':accuhourly' => self::ACCUWEATHER_HOURLY,
-                       //Delay
-                       ':afterid' => 4,
-                       "\t" => '',
-                       "\n" => ' '
-               ];
+       public function findAllByUserIdSynchronized(int $userId, \DateTime $synchronized = new \DateTime('1970-01-01')): array {
+               //Set the request
+               $req = <<<SQL
+SELECT ud.dance_id
+FROM Rapsys\AirBundle\Entity\UserDance AS ud
+WHERE ud.user_id = :uid
+SQL;
+
+               //Replace bundle entity name by table name
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
+
+               //Get result set mapping instance
+               $rsm = new ResultSetMapping();
+
+               //Declare dance id field
+               $rsm->addScalarResult('dance_id', 'dance_id', 'integer');
+
+               //Set dance sql part
+               $danceSql = '';
+
+               //With user dance
+               if (
+                       $userDances = $this->_em
+                               ->createNativeQuery($req, $rsm)
+                               ->setParameter('uid', $userId)
+                               ->getResult(AbstractQuery::HYDRATE_SCALAR_COLUMN)
+               ) {
+                       //Set dance sql part
+                       $danceSql = ' AND a.dance_id IN (:dids)';
+               }
+
+               //Set the request
+               $req = <<<SQL
+SELECT us.subscribed_id
+FROM Rapsys\AirBundle\Entity\UserSubscription AS us
+WHERE us.user_id = :uid
+SQL;
+
+               //Replace bundle entity name by table name
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
+
+               //Get result set mapping instance
+               $rsm = new ResultSetMapping();
+
+               //Declare user id field
+               $rsm->addScalarResult('subscribed_id', 'subscribed_id', 'integer');
+
+               //Set subscription sql part
+               $subscriptionSql = '';
+
+               //With user subscription
+               if (
+                       $userSubscriptions = $this->_em
+                               ->createNativeQuery($req, $rsm)
+                               ->setParameter('uid', $userId)
+                               ->getResult(AbstractQuery::HYDRATE_SCALAR_COLUMN)
+               ) {
+                       //Set subscription sql part
+                       $subscriptionSql = ' AND a.user_id IN (:uids)';
+               }
+
+               //Set the request
+               $req = <<<SQL
+SELECT
+       s.id,
+       s.date,
+       s.locked,
+       GREATEST(s.created, s.updated, l.created, l.updated, a.created, a.updated, ad.created, ad.updated, au.created, au.updated, COALESCE(p.created, '1970-01-01'), COALESCE(p.updated, '1970-01-01')) AS modified,
+       ADDDATE(ADDTIME(s.date, s.begin), INTERVAL IF(s.slot_id = :afterid, 1, 0) DAY) AS start,
+       ADDDATE(ADDTIME(ADDTIME(s.date, s.begin), s.length), INTERVAL IF(s.slot_id = :afterid, 1, 0) DAY) AS stop,
+       s.location_id AS l_id,
+       l.address AS l_address,
+       l.zipcode AS l_zipcode,
+       l.city AS l_city,
+       l.title AS l_title,
+       l.description AS l_description,
+       l.latitude AS l_latitude,
+       l.longitude AS l_longitude,
+       s.application_id AS a_id,
+       a.canceled AS a_canceled,
+       ad.name AS ad_name,
+       ad.type AS ad_type,
+       a.user_id AS au_id,
+       au.forename AS au_forename,
+       au.pseudonym AS au_pseudonym,
+       p.id AS p_id,
+       p.description AS p_description,
+       p.class AS p_class,
+       p.short AS p_short,
+       p.hat AS p_hat,
+       p.rate AS p_rate,
+       p.contact AS p_contact,
+       p.donate AS p_donate,
+       p.link AS p_link,
+       p.profile AS p_profile
+FROM Rapsys\AirBundle\Entity\Session AS s
+JOIN Rapsys\AirBundle\Entity\Location AS l ON (l.id = s.location_id)
+JOIN Rapsys\AirBundle\Entity\Application AS a ON (a.id = s.application_id{$danceSql}{$subscriptionSql})
+JOIN Rapsys\AirBundle\Entity\Dance AS ad ON (ad.id = a.dance_id)
+JOIN Rapsys\AirBundle\Entity\User AS au ON (au.id = a.user_id)
+LEFT JOIN Rapsys\AirBundle\Entity\Snippet AS p ON (p.locale = :locale AND p.location_id = s.location_id AND p.user_id = a.user_id)
+WHERE GREATEST(GREATEST(s.created, s.updated, l.created, l.updated, a.created, a.updated, ad.created, ad.updated, au.created, au.updated, COALESCE(p.created, '1970-01-01'), COALESCE(p.updated, '1970-01-01')), ADDDATE(ADDTIME(ADDTIME(s.date, s.begin), s.length), INTERVAL IF(s.slot_id = :afterid, 1, 0) DAY)) >= :synchronized
+SQL;
+
+               //Replace bundle entity name by table name
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
+
+               //Get result set mapping instance
+               $rsm = new ResultSetMapping();
+
+               //Declare all fields
+               //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
+               //addScalarResult($sqlColName, $resColName, $type = 'string');
+               $rsm
+                       ->addScalarResult('id', 'id', 'integer')
+                       ->addScalarResult('date', 'date', 'date')
+                       ->addScalarResult('locked', 'locked', 'datetime')
+                       ->addScalarResult('modified', 'modified', 'datetime')
+                       ->addScalarResult('start', 'start', 'datetime')
+                       ->addScalarResult('stop', 'stop', 'datetime')
+                       ->addScalarResult('l_id', 'l_id', 'integer')
+                       ->addScalarResult('l_address', 'l_address', 'string')
+                       ->addScalarResult('l_zipcode', 'l_zipcode', 'string')
+                       ->addScalarResult('l_city', 'l_city', 'string')
+                       ->addScalarResult('l_latitude', 'l_latitude', 'float')
+                       ->addScalarResult('l_longitude', 'l_longitude', 'float')
+                       ->addScalarResult('l_title', 'l_title', 'string')
+                       ->addScalarResult('l_description', 'l_description', 'string')
+                       ->addScalarResult('t_id', 't_id', 'integer')
+                       ->addScalarResult('t_title', 't_title', 'string')
+                       ->addScalarResult('a_id', 'a_id', 'integer')
+                       ->addScalarResult('a_canceled', 'a_canceled', 'datetime')
+                       ->addScalarResult('ad_name', 'ad_name', 'string')
+                       ->addScalarResult('ad_type', 'ad_type', 'string')
+                       ->addScalarResult('au_id', 'au_id', 'integer')
+                       ->addScalarResult('au_forename', 'au_forename', 'string')
+                       ->addScalarResult('au_pseudonym', 'au_pseudonym', 'string')
+                       ->addScalarResult('p_id', 'p_id', 'integer')
+                       ->addScalarResult('p_description', 'p_description', 'string')
+                       ->addScalarResult('p_class', 'p_class', 'string')
+                       ->addScalarResult('p_short', 'p_short', 'string')
+                       ->addScalarResult('p_hat', 'p_hat', 'integer')
+                       ->addScalarResult('p_rate', 'p_rate', 'integer')
+                       ->addScalarResult('p_contact', 'p_contact', 'string')
+                       ->addScalarResult('p_donate', 'p_donate', 'string')
+                       ->addScalarResult('p_link', 'p_link', 'string')
+                       ->addScalarResult('p_profile', 'p_profile', 'string')
+                       ->addIndexByScalar('id');
+
+               //Return sessions
+               //TODO: XXX: finish here
+               return $this->_em
+                       ->createNativeQuery($req, $rsm)
+                       ->setParameter('dids', $userDances)
+                       ->setParameter('uids', $userSubscriptions)
+                       ->setParameter('synchronized', $synchronized)
+                       ->getResult(AbstractQuery::HYDRATE_ARRAY);
+       }
+
+       /**
+        * Find session by location, slot and date
+        *
+        * @param Location $location The location
+        * @param Slot $slot The slot
+        * @param DateTime $date The datetime
+        * @return ?Session The found session
+        */
+       public function findOneByLocationSlotDate(Location $location, Slot $slot, \DateTime $date): ?Session {
+               //Return sessions
+               return $this->getEntityManager()
+                       ->createQuery('SELECT s FROM Rapsys\AirBundle\Entity\Session s WHERE (s.location = :location AND s.slot = :slot AND s.date = :date)')
+                       ->setParameter('location', $location)
+                       ->setParameter('slot', $slot)
+                       ->setParameter('date', $date)
+                       ->getSingleResult();
+       }
+
+       /**
+        * Fetch sessions by date period
+        *
+        * @XXX: used in calendar command
+        *
+        * @param DatePeriod $period The date period
+        * @return array The session array
+        */
+       public function fetchAllByDatePeriod(\DatePeriod $period): array {
+               //Set the request
+               //TODO: exclude opera and others ?
+               $req = <<<SQL
+SELECT
+       s.id,
+       s.date,
+       s.locked,
+       s.updated,
+       ADDDATE(ADDTIME(s.date, s.begin), INTERVAL IF(s.slot_id = :afterid, 1, 0) DAY) AS start,
+       ADDDATE(ADDTIME(ADDTIME(s.date, s.begin), s.length), INTERVAL IF(s.slot_id = :afterid, 1, 0) DAY) AS stop,
+       s.location_id AS l_id,
+       l.address AS l_address,
+       l.zipcode AS l_zipcode,
+       l.city AS l_city,
+       l.title AS l_title,
+       l.description AS l_description,
+       l.latitude AS l_latitude,
+       l.longitude AS l_longitude,
+       s.application_id AS a_id,
+       a.canceled AS a_canceled,
+       ad.name AS ad_name,
+       ad.type AS ad_type,
+       a.user_id AS au_id,
+       au.forename AS au_forename,
+       au.pseudonym AS au_pseudonym,
+       p.id AS p_id,
+       p.description AS p_description,
+       p.class AS p_class,
+       p.short AS p_short,
+       p.hat AS p_hat,
+       p.rate AS p_rate,
+       p.contact AS p_contact,
+       p.donate AS p_donate,
+       p.link AS p_link,
+       p.profile AS p_profile
+FROM Rapsys\AirBundle\Entity\Session AS s
+JOIN Rapsys\AirBundle\Entity\Location AS l ON (l.id = s.location_id)
+JOIN Rapsys\AirBundle\Entity\Application AS a ON (a.id = s.application_id)
+JOIN Rapsys\AirBundle\Entity\Dance AS ad ON (ad.id = a.dance_id)
+JOIN Rapsys\AirBundle\Entity\User AS au ON (au.id = a.user_id)
+LEFT JOIN Rapsys\AirBundle\Entity\Snippet AS p ON (p.locale = :locale AND p.location_id = s.location_id AND p.user_id = a.user_id)
+WHERE s.date BETWEEN :begin AND :end
+ORDER BY NULL
+SQL;
+
+               //Replace bundle entity name by table name
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
+
+               //Get result set mapping instance
+               //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
+               $rsm = new ResultSetMapping();
+
+               //Declare all fields
+               //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
+               //addScalarResult($sqlColName, $resColName, $type = 'string');
+               $rsm->addScalarResult('id', 'id', 'integer')
+                       ->addScalarResult('date', 'date', 'date')
+                       ->addScalarResult('locked', 'locked', 'datetime')
+                       ->addScalarResult('updated', 'updated', 'datetime')
+                       ->addScalarResult('start', 'start', 'datetime')
+                       ->addScalarResult('stop', 'stop', 'datetime')
+                       ->addScalarResult('l_id', 'l_id', 'integer')
+                       ->addScalarResult('l_address', 'l_address', 'string')
+                       ->addScalarResult('l_zipcode', 'l_zipcode', 'string')
+                       ->addScalarResult('l_city', 'l_city', 'string')
+                       ->addScalarResult('l_latitude', 'l_latitude', 'float')
+                       ->addScalarResult('l_longitude', 'l_longitude', 'float')
+                       ->addScalarResult('l_title', 'l_title', 'string')
+                       ->addScalarResult('l_description', 'l_description', 'string')
+                       ->addScalarResult('t_id', 't_id', 'integer')
+                       ->addScalarResult('t_title', 't_title', 'string')
+                       ->addScalarResult('a_id', 'a_id', 'integer')
+                       ->addScalarResult('a_canceled', 'a_canceled', 'datetime')
+                       ->addScalarResult('ad_name', 'ad_name', 'string')
+                       ->addScalarResult('ad_type', 'ad_type', 'string')
+                       ->addScalarResult('au_id', 'au_id', 'integer')
+                       ->addScalarResult('au_forename', 'au_forename', 'string')
+                       ->addScalarResult('au_pseudonym', 'au_pseudonym', 'string')
+                       ->addScalarResult('p_id', 'p_id', 'integer')
+                       ->addScalarResult('p_description', 'p_description', 'string')
+                       ->addScalarResult('p_class', 'p_class', 'string')
+                       ->addScalarResult('p_short', 'p_short', 'string')
+                       ->addScalarResult('p_hat', 'p_hat', 'integer')
+                       ->addScalarResult('p_rate', 'p_rate', 'integer')
+                       ->addScalarResult('p_contact', 'p_contact', 'string')
+                       ->addScalarResult('p_donate', 'p_donate', 'string')
+                       ->addScalarResult('p_link', 'p_link', 'string')
+                       ->addScalarResult('p_profile', 'p_profile', 'string')
+                       ->addIndexByScalar('id');
+
+               //Fetch result
+               $res = $this->_em
+                       ->createNativeQuery($req, $rsm)
+                       ->setParameter('begin', $period->getStartDate())
+                       ->setParameter('end', $period->getEndDate());
+
+               //Return result
+               return $res->getResult();
+       }
 
+       /**
+        * Find all session pending hourly weather
+        *
+        * @return array The sessions to update
+        */
+       public function findAllPendingHourlyWeather(): array {
                //Select all sessions starting and stopping in the next 3 days
                //XXX: select session starting after now and stopping before date(now)+3d as accuweather only provide hourly data for the next 3 days (INTERVAL 3 DAY)
                $req = <<<SQL
 SELECT s.id, s.slot_id, s.location_id, s.date, s.begin, s.length, s.rainfall, s.rainrisk, s.realfeel, s.realfeelmin, s.realfeelmax, s.temperature, s.temperaturemin, s.temperaturemax, l.zipcode
-FROM RapsysAirBundle:Session AS s
-JOIN RapsysAirBundle:Location AS l ON (l.id = s.location_id)
+FROM Rapsys\AirBundle\Entity\Session AS s
+JOIN Rapsys\AirBundle\Entity\Location AS l ON (l.id = s.location_id)
 WHERE ADDDATE(ADDTIME(s.date, s.begin), INTERVAL IF(s.slot_id = :afterid, 1, 0) DAY) >= NOW() AND ADDDATE(ADDTIME(ADDTIME(s.date, s.begin), s.length), INTERVAL IF(s.slot_id = :afterid, 1, 0) DAY) < DATE(ADDDATE(NOW(), INTERVAL :accuhourly DAY))
 SQL;
 
                //Replace bundle entity name by table name
-               $req = str_replace(array_keys($tables), array_values($tables), $req);
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
 
                //Get result set mapping instance
                $rsm = new ResultSetMapping();
 
                //Declare all fields
                $rsm
-                       ->addEntityResult('RapsysAirBundle:Session', 's')
+                       ->addEntityResult('Rapsys\AirBundle\Entity\Session', 's')
                        ->addFieldResult('s', 'id', 'id')
                        ->addFieldResult('s', 'date', 'date')
                        ->addFieldResult('s', 'begin', 'begin')
@@ -1101,15 +1265,15 @@ SQL;
                        ->addFieldResult('s', 'temperature', 'temperature')
                        ->addFieldResult('s', 'temperaturemin', 'temperaturemin')
                        ->addFieldResult('s', 'temperaturemax', 'temperaturemax')
-                       ->addJoinedEntityResult('RapsysAirBundle:Slot', 'o', 's', 'slot')
+                       ->addJoinedEntityResult('Rapsys\AirBundle\Entity\Slot', 'o', 's', 'slot')
                        ->addFieldResult('o', 'slot_id', 'id')
-                       ->addJoinedEntityResult('RapsysAirBundle:Location', 'l', 's', 'location')
+                       ->addJoinedEntityResult('Rapsys\AirBundle\Entity\Location', 'l', 's', 'location')
                        ->addFieldResult('l', 'location_id', 'id')
                        ->addFieldResult('l', 'zipcode', 'zipcode')
                        ->addIndexBy('s', 'id');
 
                //Send result
-               return $em
+               return $this->_em
                        ->createNativeQuery($req, $rsm)
                        ->getResult();
        }
@@ -1117,48 +1281,27 @@ SQL;
        /**
         * Find all session pending daily weather
         *
-        * @return array<Session> The sessions to update
+        * @return array The sessions to update
         */
-       public function findAllPendingDailyWeather() {
-               //Get entity manager
-               $em = $this->getEntityManager();
-
-               //Get quote strategy
-               $qs = $em->getConfiguration()->getQuoteStrategy();
-               $dp = $em->getConnection()->getDatabasePlatform();
-
-               //Get quoted table names
-               //XXX: this allow to make this code table name independent
-               $tables = [
-                       'RapsysAirBundle:Session' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Session'), $dp),
-                       'RapsysAirBundle:Location' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Location'), $dp),
-                       //Accuweather
-                       ':accudaily' => self::ACCUWEATHER_DAILY,
-                       ':accuhourly' => self::ACCUWEATHER_HOURLY,
-                       //Delay
-                       ':afterid' => 4,
-                       "\t" => '',
-                       "\n" => ' '
-               ];
-
+       public function findAllPendingDailyWeather(): array {
                //Select all sessions stopping after next 3 days
                //XXX: select session stopping after or equal date(now)+3d as accuweather only provide hourly data for the next 3 days (INTERVAL 3 DAY)
                $req = <<<SQL
 SELECT s.id, s.slot_id, s.location_id, s.date, s.begin, s.length, s.rainfall, s.rainrisk, s.realfeel, s.realfeelmin, s.realfeelmax, s.temperature, s.temperaturemin, s.temperaturemax, l.zipcode
-FROM RapsysAirBundle:Session AS s
-JOIN RapsysAirBundle:Location AS l ON (l.id = s.location_id)
+FROM Rapsys\AirBundle\Entity\Session AS s
+JOIN Rapsys\AirBundle\Entity\Location AS l ON (l.id = s.location_id)
 WHERE ADDDATE(ADDTIME(ADDTIME(s.date, s.begin), s.length), INTERVAL IF(s.slot_id = :afterid, 1, 0) DAY) >= DATE(ADDDATE(NOW(), INTERVAL :accuhourly DAY)) AND ADDDATE(ADDTIME(ADDTIME(s.date, s.begin), s.length), INTERVAL IF(s.slot_id = :afterid, 1, 0) DAY) < DATE(ADDDATE(NOW(), INTERVAL :accudaily DAY))
 SQL;
 
                //Replace bundle entity name by table name
-               $req = str_replace(array_keys($tables), array_values($tables), $req);
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
 
                //Get result set mapping instance
                $rsm = new ResultSetMapping();
 
                //Declare all fields
                $rsm
-                       ->addEntityResult('RapsysAirBundle:Session', 's')
+                       ->addEntityResult('Rapsys\AirBundle\Entity\Session', 's')
                        ->addFieldResult('s', 'id', 'id')
                        ->addFieldResult('s', 'date', 'date')
                        ->addFieldResult('s', 'begin', 'begin')
@@ -1171,15 +1314,15 @@ SQL;
                        ->addFieldResult('s', 'temperature', 'temperature')
                        ->addFieldResult('s', 'temperaturemin', 'temperaturemin')
                        ->addFieldResult('s', 'temperaturemax', 'temperaturemax')
-                       ->addJoinedEntityResult('RapsysAirBundle:Slot', 'o', 's', 'slot')
+                       ->addJoinedEntityResult('Rapsys\AirBundle\Entity\Slot', 'o', 's', 'slot')
                        ->addFieldResult('o', 'slot_id', 'id')
-                       ->addJoinedEntityResult('RapsysAirBundle:Location', 'l', 's', 'location')
+                       ->addJoinedEntityResult('Rapsys\AirBundle\Entity\Location', 'l', 's', 'location')
                        ->addFieldResult('l', 'location_id', 'id')
                        ->addFieldResult('l', 'zipcode', 'zipcode')
                        ->addIndexBy('s', 'id');
 
                //Send result
-               return $em
+               return $this->_em
                        ->createNativeQuery($req, $rsm)
                        ->getResult();
        }
@@ -1187,63 +1330,41 @@ SQL;
        /**
         * Find every session pending application
         *
-        * @return array<Session> The sessions to update
+        * @return array The sessions to update
         */
-       public function findAllPendingApplication() {
-               //Get entity manager
-               $em = $this->getEntityManager();
-
-               //Get quote strategy
-               $qs = $em->getConfiguration()->getQuoteStrategy();
-               $dp = $em->getConnection()->getDatabasePlatform();
-
-               //Get quoted table names
-               //XXX: this allow to make this code table name independent
-               $tables = [
-                       'RapsysAirBundle:Application' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Application'), $dp),
-                       'RapsysAirBundle:Session' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Session'), $dp),
-                       //Delay
-                       ':regulardelay' => self::REGULAR_DELAY * 24 * 3600,
-                       ':seniordelay' => self::SENIOR_DELAY * 24 * 3600,
-                       //Slot
-                       ':afterid' => 4,
-                       "\t" => '',
-                       "\n" => ' '
-               ];
-
+       public function findAllPendingApplication(): array {
                //Select all sessions not locked without application or canceled application within attribution period
                //XXX: DIFF(start, now) <= IF(DIFF(start, created) <= SENIOR_DELAY in DAY, DIFF(start, created) * 3 / 4, SENIOR_DELAY)
                //TODO: remonter les données pour le mail ?
                $req =<<<SQL
 SELECT s.id
-FROM RapsysAirBundle:Session as s
-LEFT JOIN RapsysAirBundle:Application AS a ON (a.id = s.application_id AND a.canceled IS NULL)
-JOIN RapsysAirBundle:Application AS a2 ON (a2.session_id = s.id AND a2.canceled IS NULL)
-WHERE s.locked IS NULL AND a.id IS NULL AND
-TIME_TO_SEC(TIMEDIFF(@dt_start := ADDDATE(ADDTIME(s.date, s.begin), INTERVAL IF(s.slot_id = :afterid, 1, 0) DAY), NOW())) <= IF(
-       TIME_TO_SEC(@td_sc := TIMEDIFF(@dt_start, s.created)) <= :seniordelay,
-       ROUND(TIME_TO_SEC(@td_sc) * :regulardelay / :seniordelay),
+FROM Rapsys\AirBundle\Entity\Session as s
+LEFT JOIN Rapsys\AirBundle\Entity\Application AS a ON (a.id = s.application_id AND a.canceled IS NULL)
+JOIN Rapsys\AirBundle\Entity\Application AS a2 ON (a2.session_id = s.id AND a2.canceled IS NULL)
+WHERE s.locked IS NULL AND s.application_id IS NULL AND
+(UNIX_TIMESTAMP(@dt_start := ADDDATE(ADDTIME(s.date, s.begin), INTERVAL IF(s.slot_id = :afterid, 1, 0) DAY)) - UNIX_TIMESTAMP()) <= IF(
+       (@td_sc := UNIX_TIMESTAMP(@dt_start) - UNIX_TIMESTAMP(s.created)) <= :seniordelay,
+       ROUND(@td_sc * :regulardelay / :seniordelay),
        :seniordelay
 )
 GROUP BY s.id
 ORDER BY @dt_start ASC, s.created ASC
 SQL;
 
-
                //Replace bundle entity name by table name
-               $req = str_replace(array_keys($tables), array_values($tables), $req);
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
 
                //Get result set mapping instance
                $rsm = new ResultSetMapping();
 
                //Declare all fields
                $rsm
-                       ->addEntityResult('RapsysAirBundle:Session', 's')
+                       ->addEntityResult('Rapsys\AirBundle\Entity\Session', 's')
                        ->addFieldResult('s', 'id', 'id')
                        ->addIndexBy('s', 'id');
 
                //Send result
-               return $em
+               return $this->_em
                        ->createNativeQuery($req, $rsm)
                        ->getResult();
        }
@@ -1252,49 +1373,9 @@ SQL;
         * Fetch session best application by session id
         *
         * @param int $id The session id
-        * @return Application|null The application or null
+        * @return ?Application The application or null
         */
-       public function findBestApplicationById($id) {
-               //Get entity manager
-               $em = $this->getEntityManager();
-
-               //Get quote strategy
-               $qs = $em->getConfiguration()->getQuoteStrategy();
-               $dp = $em->getConnection()->getDatabasePlatform();
-
-               //Get quoted table names
-               //XXX: this allow to make this code table name independent
-               $tables = [
-                       'RapsysAirBundle:Application' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Application'), $dp),
-                       'RapsysAirBundle:GroupUser' => $qs->getJoinTableName($em->getClassMetadata('RapsysAirBundle:User')->getAssociationMapping('groups'), $em->getClassMetadata('RapsysAirBundle:User'), $dp),
-                       'RapsysAirBundle:Location' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Location'), $dp),
-                       'RapsysAirBundle:Session' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Session'), $dp),
-                       //XXX: Set limit used to workaround mariadb subselect optimization
-                       ':limit' => PHP_INT_MAX,
-                       //Delay
-                       ':guestdelay' => self::GUEST_DELAY * 24 * 3600,
-                       ':regulardelay' => self::REGULAR_DELAY * 24 * 3600,
-                       ':seniordelay' => self::SENIOR_DELAY * 24 * 3600,
-                       //Group
-                       ':guestid' => 2,
-                       ':regularid' => 3,
-                       ':seniorid' => 4,
-                       //Slot
-                       ':afternoonid' => 2,
-                       ':eveningid' => 3,
-                       ':afterid' => 4,
-                       //XXX: days since last session after which guest regain normal priority
-                       ':guestwait' => 30,
-                       //XXX: session count until considered at regular delay
-                       ':scount' => 5,
-                       //XXX: pn_ratio over which considered at regular delay
-                       ':pnratio' => 1,
-                       //XXX: tr_ratio diff over which considered at regular delay
-                       ':trdiff' => 5,
-                       "\t" => '',
-                       "\n" => ' '
-               ];
-
+       public function findBestApplicationById(int $id): ?Application {
                /**
                 * Query session applications ranked by location score, global score, created and user_id
                 *
@@ -1327,7 +1408,7 @@ FROM (
                d.l_previous,
                d.g_score,
                d.o_tr_ratio,
-               MAX(gu.group_id) AS group_id,
+               MAX(ug.group_id) AS group_id,
                d.remaining,
                d.premium,
                d.hotspot,
@@ -1377,33 +1458,33 @@ FROM (
                                        AVG(IF(a2.id IS NOT NULL AND s2.temperature IS NOT NULL AND s2.rainfall IS NOT NULL, s2.temperature/(1+s2.rainfall), NULL)) AS l_tr_ratio,
                                        (SUM(IF(a2.id IS NOT NULL AND s2.premium = 1, 1, 0))+1)/(SUM(IF(a2.id IS NOT NULL AND s2.premium = 0, 1, 0))+1) AS l_pn_ratio,
                                        MIN(IF(a2.id IS NOT NULL, DATEDIFF(ADDDATE(s.date, INTERVAL IF(s.slot_id = :afterid, 1, 0) DAY), ADDDATE(s2.date, INTERVAL IF(s2.slot_id = :afterid, 1, 0) DAY)), NULL)) AS l_previous,
-                                       TIME_TO_SEC(TIMEDIFF(ADDDATE(ADDTIME(s.date, s.begin), INTERVAL IF(s.slot_id = :afterid, 1, 0) DAY), NOW())) AS remaining,
+                                       UNIX_TIMESTAMP(ADDDATE(ADDTIME(s.date, s.begin), INTERVAL IF(s.slot_id = :afterid, 1, 0) DAY)) - UNIX_TIMESTAMP() AS remaining,
                                        s.premium,
                                        l.hotspot,
                                        a.created
-                               FROM RapsysAirBundle:Session AS s
-                               JOIN RapsysAirBundle:Location AS l ON (l.id = s.location_id)
-                               JOIN RapsysAirBundle:Application AS a ON (a.session_id = s.id AND a.canceled IS NULL)
-                               LEFT JOIN RapsysAirBundle:Session AS s2 ON (s2.id != s.id AND s2.location_id = s.location_id AND s2.slot_id IN (:afternoonid, :eveningid) AND s2.application_id IS NOT NULL AND s2.locked IS NULL AND s2.date > s.date - INTERVAL 1 YEAR)
-                               LEFT JOIN RapsysAirBundle:Application AS a2 ON (a2.id = s2.application_id AND a2.user_id = a.user_id AND (a2.canceled IS NULL OR TIMESTAMPDIFF(DAY, a2.canceled, ADDDATE(ADDTIME(s2.date, s2.begin), INTERVAL IF(s2.slot_id = :afterid, 1, 0) DAY)) < 1))
+                               FROM Rapsys\AirBundle\Entity\Session AS s
+                               JOIN Rapsys\AirBundle\Entity\Location AS l ON (l.id = s.location_id)
+                               JOIN Rapsys\AirBundle\Entity\Application AS a ON (a.session_id = s.id AND a.canceled IS NULL)
+                               LEFT JOIN Rapsys\AirBundle\Entity\Session AS s2 ON (s2.id != s.id AND s2.location_id = s.location_id AND s2.slot_id IN (:afternoonid, :eveningid) AND s2.application_id IS NOT NULL AND s2.locked IS NULL AND s2.date > s.date - INTERVAL 1 YEAR)
+                               LEFT JOIN Rapsys\AirBundle\Entity\Application AS a2 ON (a2.id = s2.application_id AND a2.user_id = a.user_id AND (a2.canceled IS NULL OR TIMESTAMPDIFF(DAY, a2.canceled, ADDDATE(ADDTIME(s2.date, s2.begin), INTERVAL IF(s2.slot_id = :afterid, 1, 0) DAY)) < 1))
                                WHERE s.id = :sid
                                GROUP BY a.id
                                ORDER BY NULL
                                LIMIT 0, :limit
                        ) AS b
-                       LEFT JOIN RapsysAirBundle:Session AS s3 ON (s3.id != b.session_id AND s3.application_id IS NOT NULL AND s3.locked IS NULL AND s3.date > b.date - INTERVAL 1 YEAR)
-                       LEFT JOIN RapsysAirBundle:Application AS a3 ON (a3.id = s3.application_id AND a3.user_id = b.user_id AND (a3.canceled IS NULL OR TIMESTAMPDIFF(DAY, a3.canceled, ADDDATE(ADDTIME(s3.date, s3.begin), INTERVAL IF(s3.slot_id = :afterid, 1, 0) DAY)) < 1))
+                       LEFT JOIN Rapsys\AirBundle\Entity\Session AS s3 ON (s3.id != b.session_id AND s3.application_id IS NOT NULL AND s3.locked IS NULL AND s3.date > b.date - INTERVAL 1 YEAR)
+                       LEFT JOIN Rapsys\AirBundle\Entity\Application AS a3 ON (a3.id = s3.application_id AND a3.user_id = b.user_id AND (a3.canceled IS NULL OR TIMESTAMPDIFF(DAY, a3.canceled, ADDDATE(ADDTIME(s3.date, s3.begin), INTERVAL IF(s3.slot_id = :afterid, 1, 0) DAY)) < 1))
                        GROUP BY b.id
                        ORDER BY NULL
                        LIMIT 0, :limit
                ) AS c
-               LEFT JOIN RapsysAirBundle:Session AS s4 ON (s4.id != c.session_id AND s4.location_id = c.location_id AND s4.application_id IS NOT NULL AND s4.locked IS NULL AND s4.date > c.date - INTERVAL 1 YEAR)
-               LEFT JOIN RapsysAirBundle:Application AS a4 ON (a4.id = s4.application_id AND a4.user_id != c.user_id AND (a4.canceled IS NULL OR TIMESTAMPDIFF(DAY, a4.canceled, ADDDATE(ADDTIME(s4.date, s4.begin), INTERVAL IF(s4.slot_id = :afterid, 1, 0) DAY)) < 1))
+               LEFT JOIN Rapsys\AirBundle\Entity\Session AS s4 ON (s4.id != c.session_id AND s4.location_id = c.location_id AND s4.application_id IS NOT NULL AND s4.locked IS NULL AND s4.date > c.date - INTERVAL 1 YEAR)
+               LEFT JOIN Rapsys\AirBundle\Entity\Application AS a4 ON (a4.id = s4.application_id AND a4.user_id != c.user_id AND (a4.canceled IS NULL OR TIMESTAMPDIFF(DAY, a4.canceled, ADDDATE(ADDTIME(s4.date, s4.begin), INTERVAL IF(s4.slot_id = :afterid, 1, 0) DAY)) < 1))
                GROUP BY c.id
                ORDER BY NULL
                LIMIT 0, :limit
        ) AS d
-       LEFT JOIN RapsysAirBundle:GroupUser AS gu ON (gu.user_id = d.user_id)
+       LEFT JOIN Rapsys\AirBundle\Entity\UserGroup AS ug ON (ug.user_id = d.user_id)
        GROUP BY d.id
        LIMIT 0, :limit
 ) AS e
@@ -1417,31 +1498,29 @@ ORDER BY e.l_score ASC, e.g_score ASC, e.created ASC, e.user_id ASC
 SQL;
 
                //Replace bundle entity name by table name
-               $req = str_replace(array_keys($tables), array_values($tables), $req);
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
 
                //Set update request
-               $upreq = 'UPDATE RapsysAirBundle:Application SET score = :score, updated = NOW() WHERE id = :id';
+               $upreq = 'UPDATE Rapsys\AirBundle\Entity\Application SET score = :score, updated = NOW() WHERE id = :id';
 
                //Replace bundle entity name by table name
-               $upreq = str_replace(array_keys($tables), array_values($tables), $upreq);
+               $upreq = str_replace($this->tableKeys, $this->tableValues, $upreq);
 
                //Get result set mapping instance
                $rsm = new ResultSetMapping();
 
                //Declare all fields
                $rsm
-                       ->addEntityResult('RapsysAirBundle:Application', 'a')
+                       ->addEntityResult('Rapsys\AirBundle\Entity\Application', 'a')
                        ->addFieldResult('a', 'id', 'id')
                        ->addFieldResult('a', 'score', 'score')
                        ->addIndexBy('a', 'id');
 
                //Get result
                //XXX: setting limit in subqueries is required to prevent mariadb optimisation
-               $applications = $em
+               $applications = $this->_em
                        ->createNativeQuery($req, $rsm)
                        ->setParameter('sid', $id)
-                       //XXX: removed, we update score before returning best candidate
-                       //->getOneOrNullResult(Query::HYDRATE_SINGLE_SCALAR);
                        ->getResult();
 
                //Init ret
@@ -1457,39 +1536,21 @@ SQL;
 
                        //Update application updated field
                        //XXX: updated field is not modified for user with bad behaviour as application is not retrieved until delay is reached
-                       $em->getConnection()->executeUpdate($upreq, ['id' => $application->getId(), 'score' => $application->getScore()], ['id' => Type::INTEGER, 'score' => Type::FLOAT]);
+                       $this->_em->getConnection()->executeUpdate($upreq, ['id' => $application->getId(), 'score' => $application->getScore()], ['id' => Types::INTEGER, 'score' => Types::FLOAT]);
                }
 
                //Return best ranked application
                return $ret;
        }
 
-
        /**
         * Rekey sessions and applications by chronological session id
         *
         * @return bool The rekey success or failure
         */
        function rekey(): bool {
-               //Get entity manager
-               $em = $this->getEntityManager();
-
                //Get connection
-               $cnx = $em->getConnection();
-
-               //Get quote strategy
-               $qs = $em->getConfiguration()->getQuoteStrategy();
-               $dp = $em->getConnection()->getDatabasePlatform();
-
-               //Get quoted table names
-               //XXX: this allow to make this code table name independent
-               $tables = [
-                       'RapsysAirBundle:Application' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Application'), $dp),
-                       'RapsysAirBundle:Session' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Session'), $dp),
-                       ':afterid' => 4,
-                       "\t" => '',
-                       "\n" => ' '
-               ];
+               $cnx = $this->_em->getConnection();
 
                //Set the request
                $req = <<<SQL
@@ -1503,8 +1564,8 @@ FROM (
                s.begin,
                s.slot_id,
                GROUP_CONCAT(sa.id ORDER BY sa.id SEPARATOR "\\n") AS sa_id
-       FROM RapsysAirBundle:Session AS s
-       LEFT JOIN RapsysAirBundle:Application AS sa ON (sa.session_id = s.id)
+       FROM Rapsys\AirBundle\Entity\Session AS s
+       LEFT JOIN Rapsys\AirBundle\Entity\Application AS sa ON (sa.session_id = s.id)
        GROUP BY s.id
        ORDER BY NULL
 ) AS a
@@ -1512,7 +1573,7 @@ ORDER BY ADDDATE(ADDTIME(a.date, a.begin), INTERVAL IF(a.slot_id = :afterid, 1,
 SQL;
 
                //Replace bundle entity name by table name
-               $req = str_replace(array_keys($tables), array_values($tables), $req);
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
 
                //Get result set mapping instance
                //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
@@ -1526,7 +1587,7 @@ SQL;
                        #->addIndexByScalar('id');
 
                //Fetch result
-               $rnq = $em->createNativeQuery($req, $rsm);
+               $rnq = $this->_em->createNativeQuery($req, $rsm);
 
                //Get result set
                $res = $rnq->getResult();
@@ -1536,23 +1597,23 @@ SQL;
 
                //Set update session request
                $sreq = <<<SQL
-UPDATE RapsysAirBundle:Session
+UPDATE Rapsys\AirBundle\Entity\Session
 SET id = :nid, updated = NOW()
 WHERE id = :id
 SQL;
 
                //Replace bundle entity name by table name
-               $sreq = str_replace(array_keys($tables), array_values($tables), $sreq);
+               $sreq = str_replace($this->tableKeys, $this->tableValues, $sreq);
 
                //Set update application request
                $areq = <<<SQL
-UPDATE RapsysAirBundle:Application
+UPDATE Rapsys\AirBundle\Entity\Application
 SET session_id = :nid, updated = NOW()
 WHERE session_id = :id
 SQL;
 
                //Replace bundle entity name by table name
-               $areq = str_replace(array_keys($tables), array_values($tables), $areq);
+               $areq = str_replace($this->tableKeys, $this->tableValues, $areq);
 
                //Set max value
                $max = max(array_keys($res));
@@ -1607,12 +1668,12 @@ SQL;
 
                                //Set update auto_increment request
                                $ireq = <<<SQL
-ALTER TABLE RapsysAirBundle:Session
+ALTER TABLE Rapsys\AirBundle\Entity\Session
 auto_increment = 1
 SQL;
 
                                //Replace bundle entity name by table name
-                               $ireq = str_replace(array_keys($tables), array_values($tables), $ireq);
+                               $ireq = str_replace($this->tableKeys, $this->tableValues, $ireq);
 
                                //Reset auto_increment
                                $cnx->exec($ireq);
index b54fd5ac2bd07401badce928171efe014c9a05d1..d2b6eb36a1fd290aaaff580e065dc760479ba7a9 100644 (file)
@@ -1,30 +1,36 @@
-<?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\Repository;
 
-use Symfony\Component\Translation\TranslatorInterface;
 use Doctrine\ORM\Query\ResultSetMapping;
 
+use Rapsys\AirBundle\Repository;
+
 /**
  * SlotRepository
  */
-class SlotRepository extends \Doctrine\ORM\EntityRepository {
+class SlotRepository extends Repository {
        /**
         * Find slots with translated title
         *
-        * @param $translator The TranslatorInterface instance
+        * @return array The slots id keyed by translated title
         */
-       public function findAllWithTranslatedTitle(TranslatorInterface $translator) {
-               //Get entity manager
-               $em = $this->getEntityManager();
-
-               //Get quote strategy
-               $qs = $em->getConfiguration()->getQuoteStrategy();
-               $dp = $em->getConnection()->getDatabasePlatform();
-
+       public function findAllWithTranslatedTitle(): array {
                //Set the request from quoted table name
                //XXX: this allow to make this code table name independent
-               $req = 'SELECT s.id, s.title FROM '.$qs->getTableName($em->getClassMetadata('RapsysAirBundle:Slot'), $dp).' AS s';
+               $req = 'SELECT s.id, s.title FROM Rapsys\AirBundle\Entity\Slot AS s';
+
+               //Replace bundle entity name by table name
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
 
                //Get result set mapping instance
                //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
@@ -38,7 +44,7 @@ class SlotRepository extends \Doctrine\ORM\EntityRepository {
                        ->addIndexByScalar('id');
 
                //Fetch result
-               $res = $em
+               $res = $this->_em
                        ->createNativeQuery($req, $rsm)
                        ->getResult();
 
@@ -48,7 +54,7 @@ class SlotRepository extends \Doctrine\ORM\EntityRepository {
                //Process result
                foreach($res as $data) {
                        //Get translated slot
-                       $slot = $translator->trans($data['title']);
+                       $slot = $this->translator->trans($data['title']);
                        //Set data
                        //XXX: ChoiceType use display string as key
                        $ret[$slot] = $data['id'];
index a1d696b720f9856987ad7f3f684c0706e63cd2df..3d53487c22901fd3089dd1fd5bee8cb3c3241c2e 100644 (file)
@@ -1,27 +1,33 @@
-<?php
+<?php declare(strict_types=1);
 
-namespace Rapsys\AirBundle\Repository;
+/*
+ * 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.
+ */
 
-use Symfony\Component\Translation\TranslatorInterface;
-use Doctrine\ORM\Query\ResultSetMapping;
+namespace Rapsys\AirBundle\Repository;
 
 /**
  * SnippetRepository
  */
 class SnippetRepository extends \Doctrine\ORM\EntityRepository {
        /**
-        * Find snippets by locale and user id
+        * Find snippets by user id, locale and index by location id
         *
+        * @param int $user The user
         * @param string $locale The locale
-        * @param User|int $user The user
-        * @return array The snippets or empty array
+        * @return array The snippets array
         */
-       public function findByLocaleUserId($locale, $user) {
+       public function findByUserIdLocaleIndexByLocationId(int $userId, string $locale): array {
                //Fetch snippets
-               $ret = $this->getEntityManager()
-                       ->createQuery('SELECT s FROM RapsysAirBundle:Snippet s WHERE s.locale = :locale and s.user = :user')
+               $ret = $this->_em
+                       ->createQuery('SELECT s FROM Rapsys\AirBundle\Entity\Snippet s INDEX BY s.location WHERE s.locale = :locale and s.user = :user')
+                       ->setParameter('user', $userId)
                        ->setParameter('locale', $locale)
-                       ->setParameter('user', $user)
                        ->getResult();
 
                //Send result
index 2d88e3d54bb50af04fb0b673cf5acb45a287ef26..725e3ed3f482e3a08001d9f523549bb7ce4cfacc 100644 (file)
@@ -1,46 +1,59 @@
-<?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\Repository;
 
-use Symfony\Component\Translation\TranslatorInterface;
+use Doctrine\ORM\AbstractQuery;
 use Doctrine\ORM\Query\ResultSetMapping;
 
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+
+use Rapsys\AirBundle\Repository;
+
 /**
  * UserRepository
  */
-class UserRepository extends \Doctrine\ORM\EntityRepository {
+class UserRepository extends Repository {
        /**
         * Find users with translated highest group and civility
         *
-        * @param $translator The TranslatorInterface instance
+        * @return array The user ids keyed by group and pseudonym
         */
-       public function findAllWithTranslatedGroupAndCivility(TranslatorInterface $translator) {
-               //Get entity manager
-               $em = $this->getEntityManager();
-
-               //Get quote strategy
-               $qs = $em->getConfiguration()->getQuoteStrategy();
-               $dp = $em->getConnection()->getDatabasePlatform();
-
-               //Get quoted table names
-               //XXX: this allow to make this code table name independent
-               $tables = [
-                       'RapsysAirBundle:UserGroup' => $qs->getJoinTableName($em->getClassMetadata('RapsysAirBundle:User')->getAssociationMapping('groups'), $em->getClassMetadata('RapsysAirBundle:User'), $dp),
-                       'RapsysAirBundle:Group' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Group'), $dp),
-                       'RapsysAirBundle:User' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:User'), $dp)
-               ];
-
+       public function findChoicesAsArray(): array {
                //Set the request
-               $req = 'SELECT a.id, a.pseudonym, a.g_id, a.g_title FROM (
-                       SELECT u.id, u.pseudonym, g.id AS g_id, g.title AS g_title
-                       FROM RapsysAirBundle:User AS u
-                       LEFT JOIN RapsysAirBundle:UserGroup AS gu ON (gu.user_id = u.id)
-                       LEFT JOIN RapsysAirBundle:Group AS g ON (g.id = gu.group_id)
-                       ORDER BY g.id DESC, NULL LIMIT '.PHP_INT_MAX.'
-               ) AS a GROUP BY a.id ORDER BY NULL';
+               $req =<<<SQL
+SELECT
+       a.id,
+       a.pseudonym,
+       a.g_id,
+       a.g_title
+FROM (
+       SELECT
+               u.id,
+               u.pseudonym,
+               g.id AS g_id,
+               g.title AS g_title
+       FROM Rapsys\AirBundle\Entity\User AS u
+       JOIN Rapsys\AirBundle\Entity\UserGroup AS gu ON (gu.user_id = u.id)
+       JOIN Rapsys\AirBundle\Entity\Group AS g ON (g.id = gu.group_id)
+       WHERE g.title <> 'User'
+       ORDER BY g.id DESC, u.pseudonym ASC
+       LIMIT 0, :limit
+) AS a
+GROUP BY a.id
+ORDER BY NULL
+SQL;
 
                //Replace bundle entity name by table name
-               $req = str_replace(array_keys($tables), array_values($tables), $req);
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
 
                //Get result set mapping instance
                //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
@@ -56,7 +69,7 @@ class UserRepository extends \Doctrine\ORM\EntityRepository {
                        ->addIndexByScalar('id');
 
                //Fetch result
-               $res = $em
+               $res = $this->_em
                        ->createNativeQuery($req, $rsm)
                        ->getResult();
 
@@ -66,19 +79,23 @@ class UserRepository extends \Doctrine\ORM\EntityRepository {
                //Process result
                foreach($res as $data) {
                        //Without group or simple user
-                       if (empty($data['g_title']) || $data['g_title'] == 'User') {
-                               //Skip it
-                               continue;
-                       }
+                       #XXX: moved in sql by removing LEFT JOIN and excluding user group
+                       #if (empty($data['g_title']) || $data['g_title'] == 'User') {
+                       #       //Skip it
+                       #       continue;
+                       #}
+
                        //Get translated group
-                       $group = $translator->trans($data['g_title']);
+                       $group = $this->translator->trans($data['g_title']);
+
                        //Init group subarray
                        if (!isset($ret[$group])) {
                                $ret[$group] = [];
                        }
+
                        //Set data
                        //XXX: ChoiceType use display string as key
-                       $ret[$group][$data['pseudonym']] = $data['id'];
+                       $ret[$group][trim($data['pseudonym'].' ('.$data['id'].')')] = intval($data['id']);
                }
 
                //Send result
@@ -86,64 +103,324 @@ class UserRepository extends \Doctrine\ORM\EntityRepository {
        }
 
        /**
-        * Find all applicant by session
+        * Find user ids by pseudonym
         *
-        * @param $session The Session
+        * @param array $pseudonym The pseudonym filter
+        * @return array The user ids
         */
-       public function findAllApplicantBySession($session) {
-               //Get entity manager
-               $em = $this->getEntityManager();
-
-               //Fetch sessions
-               $ret = $this->getEntityManager()
-                       ->createQuery('SELECT u.id, u.pseudonym FROM RapsysAirBundle:Application a JOIN RapsysAirBundle:User u WITH u.id = a.user WHERE a.session = :session')
-                       ->setParameter('session', $session)
-                       ->getResult();
+       public function findIdByPseudonymAsArray(array $pseudonym): array {
+               //Set the request
+               $req =<<<SQL
+SELECT
+       a.id
+FROM (
+       SELECT
+               u.id
+       FROM Rapsys\AirBundle\Entity\User AS u
+       LEFT JOIN Rapsys\AirBundle\Entity\UserGroup AS gu ON (gu.user_id = u.id)
+       WHERE u.pseudonym IN (:pseudonym)
+       ORDER BY gu.group_id DESC, u.pseudonym ASC
+       LIMIT 0, :limit
+) AS a
+GROUP BY a.id
+ORDER BY NULL
+SQL;
 
-               //Process result
-               $ret = array_column($ret, 'id', 'pseudonym');
+               //Replace bundle entity name by table name
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
 
-               //Send result
-               return $ret;
+               //Get result set mapping instance
+               //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
+               $rsm = new ResultSetMapping();
+
+               //Declare all fields
+               //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
+               //XXX: we don't use a result set as we want to translate group and civility
+               $rsm->addScalarResult('id', 'id', 'integer');
+
+               //Return result
+               return $this->_em
+                       ->createNativeQuery($req, $rsm)
+                       ->setParameter('pseudonym', $pseudonym)
+                       //XXX: instead of array_column on the result
+                       ->getResult(AbstractQuery::HYDRATE_SCALAR_COLUMN);
        }
 
        /**
-        * Find all users grouped by translated group
+        * Find applicant by session id
         *
-        * @param $translator The TranslatorInterface instance
-        * @return array|null The user array or null
+        * @param int $sessionId The Session id
+        * @return array The pseudonym array keyed by id
         */
-       public function findUserGroupedByTranslatedGroup(TranslatorInterface $translator) {
-               //Get entity manager
-               $em = $this->getEntityManager();
-
-               //Get quote strategy
-               $qs = $em->getConfiguration()->getQuoteStrategy();
-               $dp = $em->getConnection()->getDatabasePlatform();
-
-               //Get quoted table names
-               //XXX: this allow to make this code table name independent
-               $tables = [
-                       'RapsysAirBundle:UserGroup' => $qs->getJoinTableName($em->getClassMetadata('RapsysAirBundle:User')->getAssociationMapping('groups'), $em->getClassMetadata('RapsysAirBundle:User'), $dp),
-                       'RapsysAirBundle:Group' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:Group'), $dp),
-                       'RapsysAirBundle:User' => $qs->getTableName($em->getClassMetadata('RapsysAirBundle:User'), $dp),
-                       //XXX: Set limit used to workaround mariadb subselect optimization
-                       ':limit' => PHP_INT_MAX,
-                       "\t" => '',
-                       "\n" => ' '
+       public function findBySessionId(int $sessionId): array {
+               //Set the request
+               $req =<<<SQL
+SELECT u.id, u.pseudonym
+FROM Rapsys\AirBundle\Entity\Application AS a
+JOIN Rapsys\AirBundle\Entity\User AS u ON (u.id = a.user_id)
+WHERE a.session_id = :id
+SQL;
+
+               //Replace bundle entity name by table name
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
+
+               //Get result set mapping instance
+               //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
+               $rsm = new ResultSetMapping();
+
+               //Declare all fields
+               //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
+               //XXX: we don't use a result set as we want to translate group and civility
+               $rsm->addScalarResult('id', 'id', 'integer')
+                       ->addIndexByScalar('pseudonym');
+
+               //Get result
+               $result = $this->_em
+                       ->createNativeQuery($req, $rsm)
+                       ->setParameter('id', $sessionId)
+                       ->getArrayResult();
+
+               //Set return
+               $return = [];
+
+               //Iterate on each result
+               foreach($result as $id => $data) {
+                       //Add to return
+                       $return[$id] = $data['id'];
+               }
+
+               //Return return
+               return $return;
+       }
+
+       /**
+        * Find user as array by id
+        *
+        * @param int $id The location id
+        * @param string $locale The locale
+        * @return array The location data
+        */
+       public function findOneByIdAsArray(int $id, string $locale): ?array {
+               //Set the request
+               //TODO: zipcode/city/country (on pourra matcher les locations avec ça ?)
+               $req =<<<SQL
+SELECT
+       u.id,
+       u.city,
+       u.forename,
+       u.mail,
+       u.phone,
+       u.pseudonym,
+       u.surname,
+       u.updated,
+       u.zipcode,
+       u.civility_id AS c_id,
+       c.title AS c_title,
+       u.country_id AS o_id,
+       o.title AS o_title,
+       GREATEST(u.created, u.updated, COALESCE(c.created, '1970-01-01'), COALESCE(c.updated, '1970-01-01'), COALESCE(o.created, '1970-01-01'), COALESCE(o.updated, '1970-01-01'), COALESCE(g.created, '1970-01-01'), COALESCE(g.updated, '1970-01-01')) AS modified,
+       GROUP_CONCAT(g.id ORDER BY g.id SEPARATOR "\\n") AS ids,
+       GROUP_CONCAT(g.title ORDER BY g.id SEPARATOR "\\n") AS titles
+FROM Rapsys\AirBundle\Entity\User AS u
+LEFT JOIN Rapsys\AirBundle\Entity\Civility AS c ON (c.id = u.civility_id)
+LEFT JOIN Rapsys\AirBundle\Entity\Country AS o ON (o.id = u.country_id)
+LEFT JOIN Rapsys\AirBundle\Entity\UserGroup AS gu ON (gu.user_id = u.id)
+LEFT JOIN Rapsys\AirBundle\Entity\Group AS g ON (g.id = gu.group_id)
+WHERE u.id = :id
+SQL;
+
+               //Replace bundle entity name by table name
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
+
+               //Get result set mapping instance
+               //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
+               $rsm = new ResultSetMapping();
+
+               //Declare all fields
+               //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
+               //addScalarResult($sqlColName, $resColName, $type = 'string');
+               $rsm->addScalarResult('id', 'id', 'integer')
+                       ->addScalarResult('city', 'city', 'string')
+                       ->addScalarResult('forename', 'forename', 'string')
+                       ->addScalarResult('mail', 'mail', 'string')
+                       ->addScalarResult('phone', 'phone', 'string')
+                       ->addScalarResult('pseudonym', 'pseudonym', 'string')
+                       ->addScalarResult('surname', 'surname', 'string')
+                       ->addScalarResult('updated', 'updated', 'datetime')
+                       ->addScalarResult('zipcode', 'zipcode', 'string')
+                       ->addScalarResult('c_id', 'c_id', 'integer')
+                       ->addScalarResult('c_title', 'c_title', 'string')
+                       ->addScalarResult('o_id', 'o_id', 'integer')
+                       ->addScalarResult('o_title', 'o_title', 'string')
+                       ->addScalarResult('modified', 'modified', 'datetime')
+                       //XXX: is a string because of \n separator
+                       ->addScalarResult('ids', 'ids', 'string')
+                       //XXX: is a string because of \n separator
+                       ->addScalarResult('titles', 'titles', 'string')
+                       ->addIndexByScalar('id');
+
+               //Get result
+               $result = $this->_em
+                       ->createNativeQuery($req, $rsm)
+                       ->setParameter('id', $id)
+                       ->getOneOrNullResult();
+
+               //Without result
+               if ($result === null) {
+                       //Return result
+                       return $result;
+               }
+
+               //Set alternates
+               $result['alternates'] = [];
+
+               //Set route
+               $route = 'rapsysair_user_view';
+
+               //Set route params
+               $routeParams = ['id' => $id, 'user' => $this->slugger->slug($result['pseudonym'])];
+
+               //Milonga Raphaël exception
+               if ($routeParams['id'] == 1 && $routeParams['user'] == 'milonga-raphael') {
+                       //Set route
+                       $route = 'rapsysair_user_milongaraphael';
+                       //Set route params
+                       $routeParams = [];
+               }
+
+               //Iterate on each languages
+               foreach($this->languages as $languageId => $language) {
+                       //Without current locale
+                       if ($languageId !== $locale) {
+                               //Set titles
+                               $titles = [];
+
+                               //Set route params locale
+                               $routeParams['_locale'] = $languageId;
+
+                               //Iterate on each locales
+                               foreach(array_keys($this->languages) as $other) {
+                                       //Without other locale
+                                       if ($other !== $languageId) {
+                                               //Set other locale title
+                                               $titles[$other] = $this->translator->trans($language, [], null, $other);
+                                       }
+                               }
+
+                               //Add alternates locale
+                               $result['alternates'][substr($languageId, 0, 2)] = $result['alternates'][str_replace('_', '-', $languageId)] = [
+                                       'absolute' => $this->router->generate($route, $routeParams, UrlGeneratorInterface::ABSOLUTE_URL),
+                                       'relative' => $this->router->generate($route, $routeParams),
+                                       'title' => implode('/', $titles),
+                                       'translated' => $this->translator->trans($language, [], null, $languageId)
+                               ];
+                       }
+               }
+
+               //Set titles
+               $titles = explode("\n", $result['titles']);
+
+               //Set groups and roles
+               $groups = $roles = [];
+
+               //Iterate on each location
+               foreach(explode("\n", $result['ids']) as $k => $id) {
+                       //Add role
+                       //XXX: roles are keyes by id
+                       $roles[$id] = 'ROLE_'.strtoupper($titles[$k]);
+
+                       //Add group
+                       $groups[$id] = $this->translator->trans($titles[$k]);
+               }
+
+               //Return result
+               return [
+                       'id' => $result['id'],
+                       'mail' => $result['mail'],
+                       'pseudonym' => $result['pseudonym'],
+                       'forename' => $result['forename'],
+                       'surname' => $result['surname'],
+                       'phone' => $result['phone'],
+                       'zipcode' => $result['zipcode'],
+                       'city' => $result['city'],
+                       'civility' => [
+                               'id' => $result['c_id'],
+                               'title' => $this->translator->trans($result['c_title'])
+                       ],
+                       'country' => [
+                               'id' => $result['o_id'],
+                               //XXX: without country, o_title is empty
+                               'title' => $this->translator->trans($result['o_title'])
+                       ],
+                       'updated' => $result['updated'],
+                       'roles' => $roles,
+                       'groups' => $groups,
+                       'modified' => $result['modified'],
+                       'multimap' => $this->translator->trans('%pseudonym% sector map', ['%pseudonym%' => $result['pseudonym']]),
+                       'slug' => $this->slugger->slug($result['pseudonym']),
+                       'link' => $this->router->generate($route, ['_locale' => $locale]+$routeParams),
+                       'alternates' => $result['alternates']
                ];
+       }
 
+       /**
+        * Find all users grouped by translated group
+        *
+        * @return array The user mail and pseudonym keyed by group and id
+        */
+       public function findIndexByGroupId(): array {
                //Set the request
                $req = <<<SQL
-SELECT u.id, u.mail, u.pseudonym, g.id AS g_id, g.title AS g_title
-FROM RapsysAirBundle:User AS u
-JOIN RapsysAirBundle:UserGroup AS gu ON (gu.user_id = u.id)
-JOIN RapsysAirBundle:Group AS g ON (g.id = gu.group_id)
-ORDER BY g.id DESC, u.id ASC
+SELECT
+       t.id,
+       t.mail,
+       t.forename,
+       t.surname,
+       t.pseudonym,
+       t.g_id,
+       t.g_title,
+       GROUP_CONCAT(t.d_id ORDER BY t.d_id SEPARATOR "\\n") AS d_ids,
+       GROUP_CONCAT(t.d_name ORDER BY t.d_id SEPARATOR "\\n") AS d_names,
+       GROUP_CONCAT(t.d_type ORDER BY t.d_id SEPARATOR "\\n") AS d_types
+FROM (
+       SELECT
+               c.id,
+               c.mail,
+               c.forename,
+               c.surname,
+               c.pseudonym,
+               c.g_id,
+               c.g_title,
+               d.id AS d_id,
+               d.name AS d_name,
+               d.type AS d_type
+       FROM (
+               SELECT
+                       u.id,
+                       u.mail,
+                       u.forename,
+                       u.surname,
+                       u.pseudonym,
+                       g.id AS g_id,
+                       g.title AS g_title
+               FROM Rapsys\AirBundle\Entity\User AS u
+               JOIN Rapsys\AirBundle\Entity\UserGroup AS gu ON (gu.user_id = u.id)
+               JOIN Rapsys\AirBundle\Entity\Group AS g ON (g.id = gu.group_id)
+               ORDER BY NULL
+               LIMIT 0, :limit
+       ) AS c
+       LEFT JOIN Rapsys\AirBundle\Entity\Application AS a ON (a.user_id = c.id)
+       LEFT JOIN Rapsys\AirBundle\Entity\Dance AS d ON (d.id = a.dance_id)
+       GROUP BY d.id
+       ORDER BY NULL
+       LIMIT 0, :limit
+) AS t
+GROUP BY t.g_id, t.id
+ORDER BY t.g_id DESC, t.id ASC
 SQL;
 
                //Replace bundle entity name by table name
-               $req = str_replace(array_keys($tables), array_values($tables), $req);
+               $req = str_replace($this->tableKeys, $this->tableValues, $req);
 
                //Get result set mapping instance
                //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
@@ -154,12 +431,20 @@ SQL;
                //addScalarResult($sqlColName, $resColName, $type = 'string');
                $rsm->addScalarResult('id', 'id', 'integer')
                        ->addScalarResult('mail', 'mail', 'string')
+                       ->addScalarResult('forename', 'forename', 'string')
+                       ->addScalarResult('surname', 'surname', 'string')
                        ->addScalarResult('pseudonym', 'pseudonym', 'string')
                        ->addScalarResult('g_id', 'g_id', 'integer')
-                       ->addScalarResult('g_title', 'g_title', 'string');
+                       ->addScalarResult('g_title', 'g_title', 'string')
+                       //XXX: is a string because of \n separator
+                       ->addScalarResult('d_ids', 'd_ids', 'string')
+                       //XXX: is a string because of \n separator
+                       ->addScalarResult('d_names', 'd_names', 'string')
+                       //XXX: is a string because of \n separator
+                       ->addScalarResult('d_types', 'd_types', 'string');
 
                //Fetch result
-               $res = $em
+               $res = $this->_em
                        ->createNativeQuery($req, $rsm)
                        ->getResult();
 
@@ -169,18 +454,51 @@ SQL;
                //Process result
                foreach($res as $data) {
                        //Get translated group
-                       $group = $translator->trans($data['g_title']);
+                       $group = $this->translator->trans($data['g_title']);
 
                        //Init group subarray
                        if (!isset($ret[$group])) {
                                $ret[$group] = [];
                        }
 
+                       //Set dances
+                       $dances = [];
+
                        //Set data
                        $ret[$group][$data['id']] = [
                                'mail' => $data['mail'],
-                               'pseudonym' => $data['pseudonym']
+                               'forename' => $data['forename'],
+                               'surname' => $data['surname'],
+                               'pseudonym' => $data['pseudonym'],
+                               'dances' => [],
+                               'slug' => $slug = $this->slugger->slug($data['pseudonym']),
+                               //Milonga Raphaël exception
+                               'link' => $data['id'] == 1 && $slug == 'milonga-raphael' ? $this->router->generate('rapsysair_user_milongaraphael', []) : $this->router->generate('rapsysair_user_view', ['id' => $data['id'], 'user' => $slug]),
+                               'edit' => $this->router->generate('rapsysuser_edit', ['mail' => $short = $this->slugger->short($data['mail']), 'hash' => $this->slugger->hash($short)])
                        ];
+
+                       //With dances
+                       if (!empty($data['d_ids'])) {
+                               //Set names
+                               $names = explode("\n", $data['d_names']);
+
+                               //Set types
+                               $types = explode("\n", $data['d_types']);
+
+                               //Iterate on each dance
+                               foreach(explode("\n", $data['d_ids']) as $k => $id) {
+                                       //Init dance when missing
+                                       if (!isset($ret[$group][$data['id']]['dances'][$name = $this->translator->trans($names[$k])])) {
+                                               $ret[$group][$data['id']]['dances'][$name] = [
+                                                       'link' => $this->router->generate('rapsysair_dance_name', ['name' => $this->slugger->short($names[$k]), 'dance' => $this->slugger->slug($name)]),
+                                                       'types' => []
+                                               ];
+                                       }
+
+                                       //Set type
+                                       $ret[$group][$data['id']]['dances'][$name]['types'][$type = $this->translator->trans($types[$k])] = $this->router->generate('rapsysair_dance_view', ['id' => $id, 'name' => $this->slugger->slug($name), 'type' => $this->slugger->slug($type)]);
+                               }
+                       }
                }
 
                //Send result
diff --git a/Resources/config/doctrine/Country.orm.yml b/Resources/config/doctrine/Country.orm.yml
new file mode 100644 (file)
index 0000000..e0a5527
--- /dev/null
@@ -0,0 +1,39 @@
+Rapsys\AirBundle\Entity\Country:
+    type: entity
+    #repositoryClass: Rapsys\AirBundle\Repository\CountryRepository
+    table: countries
+    id:
+        id:
+            type: integer
+            generator:
+                strategy: AUTO
+            options:
+                unsigned: true
+    fields:
+        code:
+            type: string
+            length: 2
+            nullable: false
+        alpha:
+            type: string
+            length: 3
+            nullable: false
+        title:
+            type: string
+            length: 64
+            nullable: false
+        created:
+            type: datetime
+        updated:
+            type: datetime
+    oneToMany:
+        users:
+            targetEntity: Rapsys\AirBundle\Entity\User
+            mappedBy: country
+    uniqueConstraints:
+        code:
+            columns: [ code ]
+        alpha:
+            columns: [ alpha ]
+    lifecycleCallbacks:
+        preUpdate: ['preUpdate']
index e1cba845a5dbf10b1712e41057fa141e3002ed48..0d241cc153545cc065315095c3e8db83993857c7 100644 (file)
@@ -10,7 +10,10 @@ Rapsys\AirBundle\Entity\Dance:
             options:
                 unsigned: true
     fields:
-        title:
+        name:
+            type: string
+            length: 32
+        type:
             type: string
             length: 32
         created:
@@ -25,5 +28,8 @@ Rapsys\AirBundle\Entity\Dance:
         users:
             targetEntity: Rapsys\AirBundle\Entity\User
             mappedBy: dances
+    uniqueConstraints:
+        name_type:
+            columns: [ name, type ]
     lifecycleCallbacks:
         preUpdate: ['preUpdate']
diff --git a/Resources/config/doctrine/GoogleCalendar.orm.yml b/Resources/config/doctrine/GoogleCalendar.orm.yml
new file mode 100644 (file)
index 0000000..d8d0fbc
--- /dev/null
@@ -0,0 +1,44 @@
+Rapsys\AirBundle\Entity\GoogleCalendar:
+    type: entity
+    #repositoryClass: Rapsys\AirBundle\Repository\GoogleCalendarRepository
+    table: google_calendars
+    id:
+        id:
+            type: integer
+            generator:
+                strategy: AUTO
+            options:
+                unsigned: true
+    fields:
+        mail:
+            type: string
+            length: 1024
+        summary:
+            type: string
+            length: 255
+#        description:
+#            type: string
+#            length: 200
+#        location:
+#            type: string
+#            length: 1024
+#        timezone:
+#            type: string
+#            length: 32
+        synchronized:
+            type: datetime
+        created:
+            type: datetime
+        updated:
+            type: datetime
+    manyToOne:
+        googleToken:
+            targetEntity: Rapsys\AirBundle\Entity\GoogleToken
+            inversedBy: googleCalendars
+            joinColumn:
+                nullable: false
+#    uniqueConstraints:
+#        user_mail:
+#            columns: [ user_id, mail ]
+    lifecycleCallbacks:
+        preUpdate: ['preUpdate']
diff --git a/Resources/config/doctrine/GoogleToken.orm.yml b/Resources/config/doctrine/GoogleToken.orm.yml
new file mode 100644 (file)
index 0000000..a32c963
--- /dev/null
@@ -0,0 +1,43 @@
+Rapsys\AirBundle\Entity\GoogleToken:
+    type: entity
+    repositoryClass: Rapsys\AirBundle\Repository\GoogleTokenRepository
+    table: google_tokens
+    id:
+        id:
+            type: integer
+            generator:
+                strategy: AUTO
+            options:
+                unsigned: true
+    fields:
+        mail:
+            type: string
+            length: 254
+        access:
+            type: string
+            length: 2048
+        refresh:
+            type: string
+            length: 512
+            nullable: true
+        expired:
+            type: datetime
+        created:
+            type: datetime
+        updated:
+            type: datetime
+    manyToOne:
+        user:
+            targetEntity: Rapsys\AirBundle\Entity\User
+            inversedBy: googleTokens
+            joinColumn:
+                nullable: false
+    oneToMany:
+        googleCalendars:
+            targetEntity: Rapsys\AirBundle\Entity\GoogleCalendar
+            mappedBy: googleToken
+    uniqueConstraints:
+        user_mail:
+            columns: [ user_id, mail ]
+    lifecycleCallbacks:
+        preUpdate: ['preUpdate']
index 0cb8dfca3870c3763015fa925a71f9223e4d472c..2393106c0a38053d8214d438cc7d08a10b513502 100644 (file)
@@ -13,6 +13,9 @@ Rapsys\AirBundle\Entity\Location:
         title:
             type: string
             length: 32
+        description:
+            type: text
+            nullable: true
         address:
             type: string
             length: 32
@@ -30,6 +33,8 @@ Rapsys\AirBundle\Entity\Location:
             type: decimal
             precision: 9
             scale: 6
+        indoor:
+            type: boolean
         hotspot:
             type: boolean
         created:
@@ -51,5 +56,10 @@ Rapsys\AirBundle\Entity\Location:
         #XXX: used in SessionRepository::(findAllPendingDailyWeather|findAllPendingHourlyWeather)
         zipcode:
             columns: [ zipcode ]
+        city_zipcode2:
+            columns: [ city, zipcode ]
+            #XXX: see https://github.com/doctrine/dbal/pull/2412 and https://stackoverflow.com/questions/32539973/configuring-index-text-length-mysql-in-doctrine
+            options:
+                lengths: [ ~, 2 ]
     lifecycleCallbacks:
         preUpdate: ['preUpdate']
index 9631e932bcdb2d7dcb858d0d4b5b1814d87f589c..fb1e962ba00f7f5fd5eb52451995ea9c131db256 100644 (file)
@@ -70,6 +70,9 @@ Rapsys\AirBundle\Entity\Session:
         date_location_slot:
             columns: [ date, location_id, slot_id ]
     indexes:
+        #XXX: used in SessionRepository::findAllByPeriodAsCalendarArray
+        date_location:
+            columns: [ date, location_id ]
         #XXX: used in SessionRepository::findAllPendingApplication
         locked_date_begin_created:
             columns: [ locked, date, begin, created ]
index 19493b24a5569163d5f4c9f27ff66a0bbcca5f70..d09120d8bcb001419d36eef9704c61099cfbdd67 100644 (file)
@@ -1,6 +1,6 @@
 Rapsys\AirBundle\Entity\Slot:
     type: entity
-    repositoryClass: Rapsys\AirBundle\Repository\SlotRepository
+    #repositoryClass: Rapsys\AirBundle\Repository\SlotRepository
     table: slots
     id:
         id:
index 8a9b7de6fcf787cb82a6b63bd8b832038c40b6fb..fcc5712ab64019680e735b7b42974b1d4a6528ae 100644 (file)
@@ -3,19 +3,22 @@ Rapsys\AirBundle\Entity\User:
     repositoryClass: Rapsys\AirBundle\Repository\UserRepository
     table: users
     fields:
-        pseudonym:
+        city:
             type: string
-            length: 32
+            length: 64
             nullable: true
         phone:
             type: string
             length: 16
             nullable: true
-        slug:
+        pseudonym:
             type: string
-            unique: true
             length: 32
             nullable: true
+        zipcode:
+            type: string
+            length: 5
+            nullable: true
     oneToMany:
         applications:
             targetEntity: Rapsys\AirBundle\Entity\Application
@@ -23,6 +26,9 @@ Rapsys\AirBundle\Entity\User:
         snippets:
             targetEntity: Rapsys\AirBundle\Entity\Snippet
             mappedBy: user
+        googleTokens:
+            targetEntity: Rapsys\AirBundle\Entity\GoogleToken
+            mappedBy: user
     manyToMany:
         dances:
             targetEntity: Rapsys\AirBundle\Entity\Dance
@@ -37,7 +43,10 @@ Rapsys\AirBundle\Entity\User:
                         name: dance_id
         subscribers:
             targetEntity: Rapsys\AirBundle\Entity\User
-            inversedBy: subscriptions
+            mappedBy: subscriptions
+        subscriptions:
+            targetEntity: Rapsys\AirBundle\Entity\User
+            inversedBy: subscribers
             joinTable:
                 name: users_subscriptions
                 joinColumns:
@@ -45,10 +54,7 @@ Rapsys\AirBundle\Entity\User:
                         name: user_id
                 inverseJoinColumns:
                     id:
-                        name: subscriber_id
-        subscriptions:
-            targetEntity: Rapsys\AirBundle\Entity\User
-            mappedBy: subscribers
+                        name: subscribed_id
         locations:
             targetEntity: Rapsys\AirBundle\Entity\Location
             inversedBy: users
@@ -60,6 +66,10 @@ Rapsys\AirBundle\Entity\User:
                 inverseJoinColumns:
                     id:
                         name: location_id
+    manyToOne:
+        country:
+            targetEntity: Rapsys\AirBundle\Entity\Country
+            inversedBy: users
 #    manyToMany:
 #        groups:
 #            targetEntity: Group
diff --git a/Resources/config/packages/rapsys_air.yaml b/Resources/config/packages/rapsys_air.yaml
deleted file mode 100644 (file)
index 84062fe..0000000
+++ /dev/null
@@ -1,348 +0,0 @@
-#RapsysAir configuration
-rapsys_air:
-
-#RapsysUser configuration
-rapsys_user:
-    #Class replacement
-    class:
-        group: 'Rapsys\AirBundle\Entity\Group'
-        civility: 'Rapsys\AirBundle\Entity\Civility'
-        user: 'Rapsys\AirBundle\Entity\User'
-    #Default replacement
-    default:
-        group: [ 'User' ]
-        civility: 'Mister'
-    #Route replacement
-    route:
-        index:
-            name: 'rapsys_air'
-    #Translate replacement
-    translate: [ 'title', 'password', 'site.title', 'copy.by', 'copy.long', 'copy.short', 'copy.title' ]
-    #Languages replacement
-    languages:
-        en_gb: 'English'
-        fr_fr: 'French'
-    #Contact replacement
-    contact:
-        title: '%rapsys_air.contact.title%'
-        mail: '%rapsys_air.contact.mail%'
-    #Context replacement
-    context:
-        copy:
-            by: '%rapsys_air.copy.by%'
-            link: '%rapsys_air.copy.link%'
-            long: '%rapsys_air.copy.long%'
-            short: '%rapsys_air.copy.short%'
-            title: '%rapsys_air.copy.title%'
-        site:
-            ico: '%rapsys_air.site.ico%'
-            logo: '%rapsys_air.site.logo%'
-            png: '%rapsys_air.site.png%'
-            svg: '%rapsys_air.site.svg%'
-            title: '%rapsys_air.site.title%'
-            url: '%rapsys_air.site.url%'
-    #Edit replacement
-    edit:
-        field:
-            pseudonym: false
-            slug: false
-        route:
-            index: 'site.url'
-        view:
-            edit: 'Rapsys\AirBundle\Form\RegisterType'
-            name: '@RapsysAir/form/edit.html.twig'
-            context:
-                title: 'Modify account'
-                password: 'Modify password'
-    #Login replacement
-    login:
-        route:
-            index: 'site.url'
-        view:
-            name: '@RapsysAir/form/login.html.twig'
-            context:
-                title: 'Login'
-    #Recover replacement
-    recover:
-        route:
-            index: 'site.url'
-            recover: 'recover_url'
-        view:
-            name: '@RapsysAir/form/recover.html.twig'
-            context:
-                title: 'Recover'
-        mail:
-            subject: 'Welcome back %%recipient_name%% to %%site.title%%'
-            html: '@RapsysAir/mail/recover.html.twig'
-            text: '@RapsysAir/mail/recover.text.twig'
-            context:
-    #Register replacement
-    register:
-        field:
-            pseudonym: false
-            slug: false
-        route:
-            index: 'site.url'
-            confirm: 'confirm_url'
-        view:
-            form: 'Rapsys\AirBundle\Form\RegisterType'
-            name: '@RapsysAir/form/register.html.twig'
-            context:
-                title: 'Register'
-        mail:
-            subject: 'Welcome %%recipient_name%% to %%site.title%%'
-            html: '@RapsysAir/mail/register.html.twig'
-            text: '@RapsysAir/mail/register.text.twig'
-            context:
-
-#Doctrine configuration
-doctrine:
-    #Dbal configuration
-    dbal:
-        mapping_types:
-            enum: string
-    #Orm configuration
-    orm:
-        #Force resolution of UserBundle entities to AirBundle one
-        #XXX: without these lines, relations are lookup in parent namespace ignoring AirBundle extension
-        resolve_target_entities:
-            Rapsys\UserBundle\Entity\Group: Rapsys\AirBundle\Entity\Group
-            Rapsys\UserBundle\Entity\Civility: Rapsys\AirBundle\Entity\Civility
-            Rapsys\UserBundle\Entity\User: Rapsys\AirBundle\Entity\User
-
-#Security configuration
-security:
-    #Set encoders
-    encoders:
-        #Rapsys\AirBundle\Entity\User: plaintext
-        Rapsys\AirBundle\Entity\User:
-            algorithm: 'bcrypt'
-
-    #Set providers
-    providers:
-        database:
-            entity:
-                class: Rapsys\AirBundle\Entity\User
-                property: mail
-
-    #Set firewall
-    firewalls:
-        #Disables authentication for assets and the profiler, adapt it according to your needs
-        dev:
-            pattern: ^/(_(profiler|wdt)|css|images|js)/
-            security: false
-
-        main:
-            #Allow anonymous access
-            anonymous: ~
-
-            #Disable logout on user change
-            logout_on_user_change: true
-
-            #Activate database provider
-            provider: database
-
-            #XXX: https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate
-            #http_basic: ~
-
-            #Set form login
-            #XXX: https://symfony.com/doc/current/security/form_login_setup.html
-            #TODO: https://symfony.com/doc/current/security/guard_authentication.html
-            form_login:
-                #Redirect to referer if different from login route
-                use_referer: true
-                #Login path
-                login_path: rapsys_user_login
-                #Check path
-                check_path: rapsys_user_login
-                #Username parameter
-                username_parameter: 'login[mail]'
-                #Password parameter
-                password_parameter: 'login[password]'
-
-            #Set logout route
-            logout:
-                #Logout route
-                path: rapsys_user_logout
-                #Logout default target
-                target: rapsys_air
-
-            #Set custom access denied handler
-            access_denied_handler: Rapsys\AirBundle\Handler\AccessDeniedHandler
-
-            #Remember me
-            #XXX: see https://symfony.com/doc/current/security/remember_me.html
-            remember_me:
-                #Use APP_SECRET
-                secret: '%kernel.secret%'
-                #Always remember me
-                always_remember_me: true
-
-    #Set role hierarchy
-    role_hierarchy:
-        ROLE_GUEST: [ 'ROLE_USER' ]
-        ROLE_REGULAR: [ 'ROLE_USER', 'ROLE_GUEST' ]
-        ROLE_SENIOR: [ 'ROLE_USER', 'ROLE_GUEST', 'ROLE_REGULAR' ]
-        ROLE_ADMIN: [ 'ROLE_USER', 'ROLE_GUEST', 'ROLE_REGULAR', 'ROLE_SENIOR' ]
-
-#Framework configuration
-framework:
-    default_locale: 'fr_fr'
-    #error_controller: 'Rapsys\AirBundle\Controller\ErrorController::show'
-    translator:
-        fallbacks: [ 'fr_fr', 'en_gb' ]
-    session:
-        enabled: true
-        handler_id: ~
-        cookie_secure: 'auto'
-        cookie_samesite: 'lax'
-    disallow_search_engine_index: false
-    secret: '%env(APP_SECRET)%'
-#framework:
-#    error_controller: Rapsys\AirBundle\Controller\ErrorController::show
-#
-#    #Assets configuration
-#    XXX: don't use that shit, it breaks assets._default_package url generation
-#    assets:
-#        #Set default base path
-#        #base_path: '/bundles/%%s'
-#        #Set default version
-#        version: 'x'
-#        #Set default format
-#        version_format: '%%s?v=%%s'
-#
-#        packages:
-#            pack:
-#                base_path: '/bundles/%%s'
-
-#Service configuration
-services:
-    #Register twig file_get_contents extension
-    #XXX: obsolete by email.image twig filter in "twig/extensions" or "twig/html-extra"
-    rapsys_air.twig.file_get_contents:
-        class: 'Rapsys\AirBundle\Twig\FileGetContentsExtension'
-        tags: [ 'twig.extension' ]
-    #Register twig base64 extension
-    rapsys_air.twig.base64:
-        class: 'Rapsys\AirBundle\Twig\Base64Extension'
-        tags: [ 'twig.extension' ]
-    #Register twig bb2html extension
-    rapsys_air.twig.bb2html:
-        class: 'Rapsys\AirBundle\Twig\Bb2htmlExtension'
-        tags: [ 'twig.extension' ]
-    #Register twig intl extension
-    #XXX: https://www.php.net/manual/en/class.intldateformatter.php
-    #XXX: https://stackoverflow.com/questions/25948853/how-to-install-the-intl-extension-for-twig
-    rapsys_air.twig.intl:
-        class: 'Twig\Extensions\IntlExtension'
-        tags: [ 'twig.extension' ]
-    #new TwigFilter('markdown_to_html', ['Twig\\Extra\\Markdown\\MarkdownRuntime', 'convert'], ['is_safe' => ['all']]),
-    #new TwigFilter('html_to_markdown', 'Twig\\Extra\\Markdown\\twig_html_to_markdown', ['is_safe' => ['all']]),
-    #Register twig markdown_to_html extension
-    #    #class: 'Twig\Extra\Markdown\DefaultMarkdown'
-    #rapsys_air.twig.markdown_eruse:
-    #    class: 'Twig\Extra\Markdown\DefaultMarkdown'
-    #rapsys_air.twig.markdown_runtime:
-    #    class: 'Twig\Extra\Markdown\MarkdownRuntime'
-    #    arguments: [ '@rapsys_air.twig.markdown_eruse' ]
-    #rapsys_air.markdown:
-    #    class: 'Twig\Extra\Markdown\MarkdownExtension'
-    #    tags: [ 'twig.extension' ]
-    #Register twig pack extension
-    #rapsys_pack.pack_extension:
-    #    class: 'Rapsys\PackBundle\Twig\PackExtension'
-    #    arguments: [ '@file_locator', '@service_container', '@rapsys_pack.path_package' ]
-    #    tags: [ 'twig.extension' ]
-    #Register application controller
-    Rapsys\AirBundle\Controller\ApplicationController:
-        arguments: [ '@service_container', '@router', '@translator' ]
-        tags: [ 'controller.service_arguments' ]
-    #Register calendar controller
-    Rapsys\AirBundle\Controller\CalendarController:
-        arguments: [ '@service_container', '@router', '@translator' ]
-        tags: [ 'controller.service_arguments' ]
-    #Register default controller
-    Rapsys\AirBundle\Controller\DefaultController:
-        arguments: [ '@service_container', '@router', '@translator' ]
-        tags: [ 'controller.service_arguments' ]
-    #Register location controller
-    Rapsys\AirBundle\Controller\LocationController:
-        arguments: [ '@service_container', '@router', '@translator' ]
-        tags: [ 'controller.service_arguments' ]
-    #Register user controller
-    Rapsys\AirBundle\Controller\UserController:
-        arguments: [ '@service_container', '@router', '@translator' ]
-        tags: [ 'controller.service_arguments' ]
-    #Register session controller
-    Rapsys\AirBundle\Controller\SessionController:
-        arguments: [ '@service_container', '@router', '@translator' ]
-        tags: [ 'controller.service_arguments' ]
-    #Register snippet controller
-    Rapsys\AirBundle\Controller\SnippetController:
-        arguments: [ '@service_container', '@router', '@translator' ]
-        tags: [ 'controller.service_arguments' ]
-    #Register error controller
-    Rapsys\AirBundle\Controller\ErrorController:
-        arguments: [ '@service_container', '@router', '@translator' ]
-        tags: [ 'controller.service_arguments' ]
-    #Register locale event subscriber
-    #TODO: remove that shit now ???
-    #Rapsys\AirBundle\EventSubscriber\LocaleSubscriber:
-    #    arguments: [ '@router', '%rapsys_air.locales%' ]
-    #    tags: [ 'kernel.event_subscriber' ]
-    #Register facebook event subscriber
-    Rapsys\AirBundle\EventSubscriber\FacebookSubscriber:
-        arguments: [ '@router', '%rapsys_air.locales%' ]
-        tags: [ 'kernel.event_subscriber' ]
-    #Register access denied handler
-    Rapsys\AirBundle\Handler\AccessDeniedHandler:
-        arguments: [ '@service_container', '@router', '@translator' ]
-    #Register air fixtures
-    Rapsys\AirBundle\DataFixtures\AirFixtures:
-        tags: [ 'doctrine.fixture.orm' ]
-#   #Set version strategy
-#   assets.static_version_strategy:
-#       class: Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy
-#       arguments: [ 'x', '%%s?v=%%s' ]
-    Rapsys\AirBundle\Form\ApplicationType:
-        arguments: [ '@doctrine', '@translator' ]
-        tags: [ 'form.type' ]
-    Rapsys\AirBundle\Form\LocationType:
-        tags: [ 'form.type' ]
-    Rapsys\AirBundle\Form\SessionType:
-        arguments: [ '@doctrine' ]
-        tags: [ 'form.type' ]
-    Rapsys\AirBundle\Form\Extension\Type\HiddenEntityType:
-        arguments: [ '@doctrine' ]
-        tags: [ 'form.type' ]
-    Rapsys\AirBundle\Command\AttributeCommand:
-        arguments: [ '@doctrine' ]
-        tags: [ 'console.command' ]
-    Rapsys\AirBundle\Command\CalendarCommand:
-        arguments: [ '@service_container', '@doctrine', '@router', '@translator' ]
-        tags: [ 'console.command' ]
-    Rapsys\AirBundle\Command\RekeyCommand:
-        arguments: [ '@doctrine' ]
-        tags: [ 'console.command' ]
-    Rapsys\AirBundle\Command\WeatherCommand:
-        arguments: [ '@doctrine' ]
-        tags: [ 'console.command' ]
-
-#Twig Configuration
-twig:
-    #Enforce debug
-    #debug: true
-    #auto_reload: ~
-    #Disable cache
-    #XXX: enable forced regeneration of css and js at each page load
-    cache: false
-    #Fix form layout for css
-    #XXX: @RapsysAir is a shortcut to vendor/rapsys/airbundle/Resources/views directory here
-    form_theme: [ '@RapsysAir/form/form_div_layout.html.twig' ]
-    #Set twig paths
-    paths:
-        #Required by email.image(site_logo) directive
-        #XXX: Allow twig to resolve @RapsysAir/png/logo.png in vendor/rapsys/airbundle/Resources/public/png/logo.png
-        '%kernel.project_dir%/vendor/rapsys/airbundle/Resources/public': 'RapsysAir'
-    #Override default exception controller
-    #exception_controller: Rapsys\AirBundle\Controller\ErrorController::preview
diff --git a/Resources/config/packages/rapsysair.yaml b/Resources/config/packages/rapsysair.yaml
new file mode 100644 (file)
index 0000000..151ec25
--- /dev/null
@@ -0,0 +1,412 @@
+# Parameters configuration
+parameters:
+    # Google project
+    env(RAPSYSAIR_GOOGLE_PROJECT): "Ch4ng3m3!"
+    # Hostname
+    env(RAPSYSAIR_HOSTNAME): "Ch4ng3m3!"
+    # Scheme
+    env(RAPSYSAIR_SCHEME): "https"
+
+# RapsysAir configuration
+rapsysair:
+    languages:
+        en_gb: 'English'
+        fr_fr: 'French'
+    locale: 'fr_fr'
+    locales: [ 'fr_fr', 'en_gb' ]
+    logo:
+        alt: 'Libre Air logo'
+    title: 'Libre Air'
+
+# RapsysUser configuration
+rapsysuser:
+    # Class replacement
+    class:
+        civility: 'Rapsys\AirBundle\Entity\Civility'
+        country: 'Rapsys\AirBundle\Entity\Country'
+        dance: 'Rapsys\AirBundle\Entity\Dance'
+        group: 'Rapsys\AirBundle\Entity\Group'
+        user: 'Rapsys\AirBundle\Entity\User'
+    # Default replacement
+    default:
+        group: [ 'User' ]
+        civility: 'Mister'
+        languages: '%rapsysair.languages%'
+        locales: '%rapsysair.locales%'
+        country: 'France'
+        country_favorites: [ 'France', 'Belgium', 'Germany', 'Italy', 'Luxembourg', 'Portugal', 'Spain', 'Switzerland' ]
+        dance_favorites: [ 'Argentine Tango Ball', 'Argentine Tango Ball and class', 'Argentine Tango Ball and concert' ]
+        subscription_favorites: [ 'Milonga Raphaël' ]
+    # Route replacement
+    route:
+        index:
+            name: 'rapsysair'
+    # Translate replacement
+    translate: [ 'title.page', 'title.section', 'title.site', 'password', 'copy.by', 'copy.long', 'copy.short', 'copy.title', 'logo.alt' ]
+    # Contact replacement
+    contact:
+        address: '%rapsysair.contact.address%'
+        name: '%rapsysair.contact.name%'
+    # Context replacement
+    context:
+        copy: '%rapsysair.copy%'
+        icon: '%rapsysair.icon%'
+        logo: '%rapsysair.logo%'
+        root: '%rapsysair.root%'
+        title:
+            section: 'User'
+            site: '%rapsysair.title%'
+    # Edit replacement
+    edit:
+        field:
+            pseudonym: false
+        route:
+            index: 'root'
+        view:
+            edit: 'Rapsys\AirBundle\Form\RegisterType'
+            name: '@RapsysAir/form/edit.html.twig'
+            context:
+                title:
+                    page: 'Modify account'
+                password: 'Modify password'
+    # Login replacement
+    login:
+        route:
+            index: 'root'
+        view:
+            name: '@RapsysAir/form/login.html.twig'
+            context:
+                title:
+                    page: 'Login'
+    # Recover replacement
+    recover:
+        route:
+            index: 'root'
+            recover: 'recover_url'
+        view:
+            name: '@RapsysAir/form/recover.html.twig'
+            context:
+                title:
+                    page: 'Recover'
+        mail:
+            subject: 'Welcome back %%recipient_name%% to %%title.site%%'
+            html: '@RapsysAir/mail/recover.html.twig'
+            text: '@RapsysAir/mail/recover.text.twig'
+            context:
+    # Register replacement
+    register:
+        field:
+            city: false
+            country: false
+            phone: false
+            pseudonym: false
+            zipcode: false
+        route:
+            index: 'root'
+            confirm: 'confirm_url'
+        view:
+            form: 'Rapsys\AirBundle\Form\RegisterType'
+            name: '@RapsysAir/form/register.html.twig'
+            context:
+                title:
+                    page: 'Register'
+        mail:
+            subject: 'Welcome %%recipient_name%% to %%title.site%%'
+            html: '@RapsysAir/mail/register.html.twig'
+            text: '@RapsysAir/mail/register.text.twig'
+            context:
+
+# Doctrine configuration
+doctrine:
+    # Dbal configuration
+    dbal:
+        charset: 'utf8mb4'
+        default_table_options:
+            charset: 'utf8mb4'
+            collation: 'utf8mb4_unicode_ci'
+
+    # Orm configuration
+    orm:
+        # Controller resolver
+        controller_resolver:
+            # Disable auto mapping
+            auto_mapping: false
+
+        # Replace repository factory
+        repository_factory: 'Rapsys\AirBundle\Factory'
+
+        # Force resolution of UserBundle entities to AirBundle one
+        # XXX: without these lines, relations are lookup in parent namespace ignoring AirBundle extension
+        resolve_target_entities:
+            Rapsys\UserBundle\Entity\Group: 'Rapsys\AirBundle\Entity\Group'
+            Rapsys\UserBundle\Entity\Civility: 'Rapsys\AirBundle\Entity\Civility'
+            Rapsys\UserBundle\Entity\User: 'Rapsys\AirBundle\Entity\User'
+
+        # Force mappings
+        mappings:
+            # Map entities
+            # XXX: Entity short syntax was removed
+            # XXX: see https://github.com/doctrine/orm/issues/8818
+            RapsysAirBundle:
+                type: 'yml'
+                is_bundle: true
+                dir: 'Resources/config/doctrine'
+                prefix: 'Rapsys\AirBundle\Entity'
+                alias: 'RapsysAirBundle'
+
+# Security configuration
+security:
+    # Set password hashers
+    password_hashers:
+        # Rapsys\AirBundle\Entity\User: plaintext
+        Rapsys\AirBundle\Entity\User:
+            algorithm: 'bcrypt'
+
+    # Set providers
+    providers:
+        database:
+            entity:
+                class: 'Rapsys\AirBundle\Entity\User'
+                property: 'mail'
+
+    # Set firewall
+    firewalls:
+        # Disables authentication for assets and the profiler, adapt it according to your needs
+        dev:
+            pattern: '^/(_(profiler|wdt)|css|images|js)/'
+            security: false
+
+        main:
+            # Allow anonymous access
+            #anonymous: ~
+            #lazy: true
+
+            # Activate database provider
+            provider: 'database'
+
+            # XXX: https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate
+            #http_basic: ~
+
+            # Set form login
+            # XXX: see https://symfony.com/doc/current/security/form_login_setup.html
+            # XXX: see https://symfony.com/doc/current/security/custom_authenticator.html
+            form_login:
+                # Redirect to referer if different from login route
+                use_referer: true
+                # Login path
+                login_path: 'rapsysuser_login'
+                # Check path
+                check_path: 'rapsysuser_login'
+                # Username parameter
+                username_parameter: 'login[mail]'
+                # Password parameter
+                password_parameter: 'login[password]'
+
+            # Set logout route
+            logout:
+                # Logout route
+                path: 'rapsysuser_logout'
+                # Logout default target
+                target: 'rapsysair'
+
+            # Set custom access denied handler
+            access_denied_handler: 'Rapsys\AirBundle\Handler\AccessDeniedHandler'
+
+            # Remember me
+            # XXX: see https://symfony.com/doc/current/security/remember_me.html
+            remember_me:
+                # Use APP_SECRET
+                secret: '%kernel.secret%'
+                # Always remember me
+                always_remember_me: true
+
+    # Set role hierarchy
+    role_hierarchy:
+        ROLE_GUEST: [ 'ROLE_USER' ]
+        ROLE_REGULAR: [ 'ROLE_USER', 'ROLE_GUEST' ]
+        ROLE_SENIOR: [ 'ROLE_USER', 'ROLE_GUEST', 'ROLE_REGULAR' ]
+        ROLE_ADMIN: [ 'ROLE_USER', 'ROLE_GUEST', 'ROLE_REGULAR', 'ROLE_SENIOR' ]
+
+# Framework configuration
+framework:
+    # Cache
+    cache:
+        # Memcache server
+        default_memcached_provider: 'memcached://localhost:11211'
+        # Cache pool
+        pools:
+            # Air cache
+            air.cache:
+                adapter: 'cache.adapter.memcached'
+            # User cache
+            user.cache:
+                adapter: 'cache.adapter.memcached'
+    default_locale: '%rapsysair.locale%'
+    disallow_search_engine_index: false
+    #error_controller: 'Rapsys\AirBundle\Controller\ErrorController::show'
+    # Append ip to mail headers
+    mailer:
+        headers:
+            X-Originating-IP: '%env(string:REMOTE_ADDR)%'
+    secret: '%env(string:APP_SECRET)%'
+    # Native session
+    session:
+        enabled: true
+        handler_id: ~
+        storage_factory_id: 'session.storage.factory.native'
+        cookie_secure: 'auto'
+        cookie_samesite: 'lax'
+    translator:
+        fallbacks: '%rapsysair.locales%'
+    validation:
+        email_validation_mode: 'html5'
+
+# Service configuration
+services:
+    # Register google client service
+    Google\Client:
+        alias: 'google.client'
+    # Register google client service alias
+    google.client:
+        arguments: [
+            {
+                application_name: '%env(string:RAPSYSAIR_GOOGLE_PROJECT)%',
+                client_id: '%env(string:GOOGLE_CLIENT_ID)%',
+                client_secret: '%env(string:GOOGLE_CLIENT_SECRET)%',
+                #redirect_uri: '%env(string:RAPSYSAIR_SCHEME)%://%env(string:RAPSYSAIR_HOSTNAME)%/google/callback',
+                redirect_uri: 'rapsysair_google_callback',
+                scopes: [ !php/const '\Google\Service\Calendar::CALENDAR_EVENTS', !php/const '\Google\Service\Calendar::CALENDAR', !php/const '\Google\Service\Oauth2::USERINFO_EMAIL' ],
+                access_type: 'offline',
+                #//XXX: see https://stackoverflow.com/questions/10827920/not-receiving-google-oauth-refresh-token
+                #'approval_prompt' => 'force',
+                prompt: 'consent'
+            }
+        ]
+        class: 'Google\Client'
+        public: true
+    # Register application controller
+    Rapsys\AirBundle\Controller\ApplicationController:
+        arguments: [ '@security.authorization_checker', '@service_container', '@rapsysuser.access_decision_manager', '@doctrine', '@rapsysair.facebook_util', '@form.factory', '@rapsyspack.image_util', '@mailer.mailer', '@doctrine.orm.default_entity_manager', '@rapsyspack.map_util', '@rapsyspack.path_package', '@router', '@security.helper', '@rapsyspack.slugger_util', '@request_stack', '@translator', '@twig' ]
+        tags: [ 'controller.service_arguments' ]
+    # Register calendar controller
+    Rapsys\AirBundle\Controller\CalendarController:
+        arguments: [ '@security.authorization_checker', '@service_container', '@rapsysuser.access_decision_manager', '@doctrine', '@rapsysair.facebook_util', '@form.factory', '@rapsyspack.image_util', '@mailer.mailer', '@doctrine.orm.default_entity_manager', '@rapsyspack.map_util', '@rapsyspack.path_package', '@router', '@security.helper', '@rapsyspack.slugger_util', '@request_stack', '@translator', '@twig' ]
+        tags: [ 'controller.service_arguments' ]
+    # Register default controller
+    Rapsys\AirBundle\Controller\DefaultController:
+        arguments: [ '@security.authorization_checker', '@service_container', '@rapsysuser.access_decision_manager', '@doctrine', '@rapsysair.facebook_util', '@form.factory', '@rapsyspack.image_util', '@mailer.mailer', '@doctrine.orm.default_entity_manager', '@rapsyspack.map_util', '@rapsyspack.path_package', '@router', '@security.helper', '@rapsyspack.slugger_util', '@request_stack', '@translator', '@twig' ]
+        tags: [ 'controller.service_arguments' ]
+    # Register dance controller
+    Rapsys\AirBundle\Controller\DanceController:
+        arguments: [ '@security.authorization_checker', '@service_container', '@rapsysuser.access_decision_manager', '@doctrine', '@rapsysair.facebook_util', '@form.factory', '@rapsyspack.image_util', '@mailer.mailer', '@doctrine.orm.default_entity_manager', '@rapsyspack.map_util', '@rapsyspack.path_package', '@router', '@security.helper', '@rapsyspack.slugger_util', '@request_stack', '@translator', '@twig' ]
+        tags: [ 'controller.service_arguments' ]
+    # Register location controller
+    Rapsys\AirBundle\Controller\LocationController:
+        arguments: [ '@security.authorization_checker', '@service_container', '@rapsysuser.access_decision_manager', '@doctrine', '@rapsysair.facebook_util', '@form.factory', '@rapsyspack.image_util', '@mailer.mailer', '@doctrine.orm.default_entity_manager', '@rapsyspack.map_util', '@rapsyspack.path_package', '@router', '@security.helper', '@rapsyspack.slugger_util', '@request_stack', '@translator', '@twig' ]
+        tags: [ 'controller.service_arguments' ]
+    # Register user controller
+    Rapsys\AirBundle\Controller\UserController:
+        arguments: [ '@user.cache', '@security.authorization_checker', '@service_container', '@doctrine', '@form.factory', '@security.user_password_hasher', '@logger', '@mailer.mailer', '@doctrine.orm.default_entity_manager', '@router', '@security.helper', '@rapsyspack.slugger_util', '@request_stack', '@translator', '@twig', '@google.client' ]
+        tags: [ 'controller.service_arguments' ]
+    # Register session controller
+    Rapsys\AirBundle\Controller\SessionController:
+        arguments: [ '@security.authorization_checker', '@service_container', '@rapsysuser.access_decision_manager', '@doctrine', '@rapsysair.facebook_util', '@form.factory', '@rapsyspack.image_util', '@mailer.mailer', '@doctrine.orm.default_entity_manager', '@rapsyspack.map_util', '@rapsyspack.path_package', '@router', '@security.helper', '@rapsyspack.slugger_util', '@request_stack', '@translator', '@twig' ]
+        tags: [ 'controller.service_arguments' ]
+    # Register snippet controller
+    Rapsys\AirBundle\Controller\SnippetController:
+        arguments: [ '@security.authorization_checker', '@service_container', '@rapsysuser.access_decision_manager', '@doctrine', '@rapsysair.facebook_util', '@form.factory', '@rapsyspack.image_util', '@mailer.mailer', '@doctrine.orm.default_entity_manager', '@rapsyspack.map_util', '@rapsyspack.path_package', '@router', '@security.helper', '@rapsyspack.slugger_util', '@request_stack', '@translator', '@twig' ]
+        tags: [ 'controller.service_arguments' ]
+    # Register error controller
+    Rapsys\AirBundle\Controller\ErrorController:
+        arguments: [ '@security.authorization_checker', '@service_container', '@rapsysuser.access_decision_manager', '@doctrine', '@rapsysair.facebook_util', '@form.factory', '@rapsyspack.image_util', '@mailer.mailer', '@doctrine.orm.default_entity_manager', '@rapsyspack.map_util', '@rapsyspack.path_package', '@router', '@security.helper', '@rapsyspack.slugger_util', '@request_stack', '@translator', '@twig' ]
+        #arguments: [ '@service_container', '@router', '@translator' ]
+        tags: [ 'controller.service_arguments' ]
+    # Register access denied handler
+    Rapsys\AirBundle\Handler\AccessDeniedHandler:
+        arguments: [ '@security.authorization_checker', '@service_container', '@rapsysuser.access_decision_manager', '@doctrine', '@rapsysair.facebook_util', '@form.factory', '@rapsyspack.image_util', '@mailer.mailer', '@doctrine.orm.default_entity_manager', '@rapsyspack.map_util', '@rapsyspack.path_package', '@router', '@security.helper', '@rapsyspack.slugger_util', '@request_stack', '@translator', '@twig' ]
+    # Register air fixtures
+    Rapsys\AirBundle\DataFixtures\AirFixtures:
+        arguments: [ '@service_container', '@security.user_password_hasher' ]
+        tags: [ 'doctrine.fixture.orm' ]
+    # Register application form
+    Rapsys\AirBundle\Form\ApplicationType:
+        arguments: [ '@doctrine', '@translator' ]
+        tags: [ 'form.type' ]
+    # Register location form
+    Rapsys\AirBundle\Form\LocationType:
+        tags: [ 'form.type' ]
+    # Register session form
+    Rapsys\AirBundle\Form\SessionType:
+        arguments: [ '@doctrine' ]
+        tags: [ 'form.type' ]
+    # Register hidden entity form type
+    Rapsys\AirBundle\Form\Extension\Type\HiddenEntityType:
+        arguments: [ '@doctrine' ]
+        tags: [ 'form.type' ]
+    # Register contact form
+    Rapsys\AirBundle\Form\ContactType:
+        arguments: [ '@rapsyspack.image_util', '@rapsyspack.slugger_util', '@translator' ]
+        tags: [ 'form.type' ]
+    # Register register form
+    Rapsys\AirBundle\Form\RegisterType:
+        arguments: [ '@doctrine.orm.entity_manager', '@rapsyspack.image_util', '@rapsyspack.slugger_util', '@translator' ]
+        tags: [ 'form.type' ]
+    # Register attribute command
+    Rapsys\AirBundle\Command\AttributeCommand:
+        arguments: [ '@doctrine' ]
+        tags: [ 'console.command' ]
+    # Register calendar command
+    Rapsys\AirBundle\Command\CalendarCommand:
+        arguments: [ '@doctrine', '%kernel.default_locale%', '@router', '@rapsyspack.slugger_util', '@translator', '@user.cache', '@google.client', '@twig.markdown.default' ]
+        tags: [ 'console.command' ]
+    # Register rekey command
+    Rapsys\AirBundle\Command\RekeyCommand:
+        arguments: [ '@doctrine' ]
+        tags: [ 'console.command' ]
+    # Register weather command
+    Rapsys\AirBundle\Command\WeatherCommand:
+        arguments: [ '@doctrine', '@filesystem' ]
+        tags: [ 'console.command' ]
+    # Register repository factory
+    Rapsys\AirBundle\Factory:
+        arguments: [ '@request_stack', '@router', '@rapsyspack.slugger_util', '@translator', '%kernel.default_locale%', '%rapsysair.languages%' ]
+    # Register facebook event subscriber
+    Rapsys\PackBundle\Subscriber\FacebookSubscriber:
+        arguments: [ '@router', '%rapsysair.locales%' ]
+        tags: [ 'kernel.event_subscriber' ]
+    # Register dotenv:dump command
+    Symfony\Component\Dotenv\Command\DotenvDumpCommand: ~
+    # Register facebook util service
+    rapsysair.facebook_util:
+        class: 'Rapsys\PackBundle\Util\FacebookUtil'
+        arguments: [ '@router',  '%kernel.project_dir%/var/cache', '%rapsyspack.path%', 'facebook', '%rapsysair.path%/png/facebook.png', { irishgrover: '%rapsysair.path%/ttf/irishgrover.v10.ttf', labelleaurore: '%rapsysair.path%/ttf/labelleaurore.v10.ttf', dejavusans: '%rapsysair.path%/ttf/dejavusans.2.37.ttf', droidsans: '%rapsysair.path%/ttf/droidsans.regular.ttf' } ]
+        public: true
+    # Register security password_hasher_factory as public
+    # XXX: required for command `php bin/console doctrine:`
+    security.password_hasher_factory:
+        class: 'Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory'
+        arguments: [ { 'Rapsys\AirBundle\Entity\User': { class: 'Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher', arguments: [ ~, ~, ~, '2y'] } } ]
+        public: true
+
+# Twig Configuration
+twig:
+    # Enforce debug
+    #auto_reload: ~
+    debug: '%kernel.debug%'
+    #strict_variables: '%kernel.debug%'
+
+    # Disable cache
+    # XXX: enable forced regeneration of css and js at each page load
+    cache: false
+
+    # Fix form layout for css
+    # XXX: @RapsysAir is a shortcut to vendor/rapsys/airbundle/Resources/views directory here
+    form_theme: [ '@RapsysAir/form/form_div_layout.html.twig' ]
+
+    # Set twig paths
+    paths:
+        # Required by email.image(site_logo) directive
+        # XXX: Allow twig to resolve @RapsysAir/png/logo.png in vendor/rapsys/airbundle/Resources/public/png/logo.png
+        '%kernel.project_dir%/vendor/rapsys/airbundle/Resources/public': 'RapsysAir'
+    # Override default exception controller
+    #exception_controller: Rapsys\AirBundle\Controller\ErrorController::preview
similarity index 55%
rename from Resources/config/routes/rapsys_air.yaml
rename to Resources/config/routes/rapsysair.yaml
index 3fd3afe88b1aacc3db081bcc9b8cf5bbb6edcfc3..59aad9d10a070ddcd5836d648c731bd65e61d41a 100644 (file)
+#https://symfony.com/doc/current/controller.html#controller-request-argument
+#CRUD: edit, index, new, show, _delete_form, _form
 #https://symfony.com/doc/current/routing.html#localized-routes-i18n
 #SCRUD: index, add, edit, delete, view
-rapsys_air:
+
+#TODO: rename view in show ???
+rapsysair:
     path:
         en_gb: '/en'
         fr_fr: '/'
     controller: Rapsys\AirBundle\Controller\DefaultController::index
     methods: GET
 
-rapsys_air_about:
+rapsysair_about:
     path:
         en_gb: '/en/about'
         fr_fr: '/a-propos'
     controller: Rapsys\AirBundle\Controller\DefaultController::about
     methods: GET
 
-rapsys_air_contact:
+#TODO: drop it or should we keep it to be able to add an application from multiple places ???
+rapsysair_application_add:
     path:
-        en_gb: '/en/contact'
-        fr_fr: '/contacter'
-    controller: Rapsys\AirBundle\Controller\DefaultController::contact
+        en_gb: '/en/application'
+        fr_fr: '/reservation'
+    controller: Rapsys\AirBundle\Controller\ApplicationController::add
     methods: GET|POST
 
-rapsys_air_calendar:
+rapsysair_contact:
     path:
-        en_gb: '/en/calendar'
-        fr_fr: '/calendrier'
-    controller: Rapsys\AirBundle\Controller\CalendarController::index
+        en_gb: '/en/contact/{id<\d*>?}/{user<[\w-]*>?}'
+        fr_fr: '/contacter/{id<\d*>?}/{user<[\w-]*>?}'
+    controller: Rapsys\AirBundle\Controller\DefaultController::contact
     methods: GET|POST
 
-rapsys_air_calendar_callback:
-    path: '/calendar/callback'
-    controller: Rapsys\AirBundle\Controller\CalendarController::callback
-    methods: GET
+#rapsysair_calendar:
+#    path:
+#        en_gb: '/en/calendar'
+#        fr_fr: '/calendrier'
+#    controller: Rapsys\AirBundle\Controller\CalendarController::index
+#    methods: GET|POST
 
-rapsys_air_frequently_asked_questions:
+rapsysair_city:
     path:
-        en_gb: '/en/frequently-asked-questions'
-        fr_fr: '/foire-aux-questions'
-    controller: Rapsys\AirBundle\Controller\DefaultController::frequentlyAskedQuestions
-    methods: GET
+        'en_gb': '/en/city'
+        'fr_fr': '/ville'
+    controller: Rapsys\AirBundle\Controller\LocationController::cities
+    methods: GET|POST
 
-rapsys_air_organizer_regulation:
+rapsysair_city_view:
     path:
-        en_gb: '/en/organizer-regulation'
-        fr_fr: '/reglement-organisateur'
-    controller: Rapsys\AirBundle\Controller\DefaultController::organizerRegulation
-    methods: GET
+        'en_gb': '/en/city/{latitude<-?(?:\d*\.)?\d+>}/{longitude<-?(?:\d*\.)?\d+>}/{city<[\w-]+>}'
+        'fr_fr': '/ville/{latitude<-?(?:\d*\.)?\d+>}/{longitude<-?(?:\d*\.)?\d+>}/{city<[\w-]+>}'
+    controller: Rapsys\AirBundle\Controller\LocationController::city
+    methods: GET|POST
 
-rapsys_air_terms_of_service:
+rapsysair_dance:
     path:
-        en_gb: '/en/terms-of-service'
-        fr_fr: '/conditions-generales-d-utilisation'
-    controller: Rapsys\AirBundle\Controller\DefaultController::termsOfService
-    methods: GET
+        'en_gb': '/en/dance'
+        'fr_fr': '/danse'
+    controller: Rapsys\AirBundle\Controller\DanceController::index
+    methods: GET|POST
 
-rapsys_air_dispute:
+rapsysair_dance_view:
     path:
-        en_gb: '/en/dispute'
-        fr_fr: '/contestation'
-    controller: Rapsys\AirBundle\Controller\DefaultController::dispute
+        'en_gb': '/en/dance/{id<[0-9]+>}/{name<[\w-]+>}/{type<[\w-]+>}'
+        'fr_fr': '/danse/{id<[0-9]+>}/{name<[\w-]+>}/{type<[\w-]+>}'
+    controller: Rapsys\AirBundle\Controller\DanceController::view
     methods: GET|POST
 
-rapsys_air_location:
+rapsysair_dance_name:
     path:
-        en_gb: '/en/location'
-        fr_fr: '/emplacement'
-    controller: Rapsys\AirBundle\Controller\LocationController::index
+        'en_gb': '/en/dance/{name<[a-zA-Z0-9=_-]+>}/{dance<[\w-]+>}'
+        'fr_fr': '/danse/{name<[a-zA-Z0-9=_-]+>}/{dance<[\w-]+>}'
+    controller: Rapsys\AirBundle\Controller\DanceController::name
+    methods: GET|POST
+
+rapsysair_frequently_asked_questions:
+    path:
+        en_gb: '/en/frequently-asked-questions'
+        fr_fr: '/foire-aux-questions'
+    controller: Rapsys\AirBundle\Controller\DefaultController::frequentlyAskedQuestions
+    methods: GET
+
+rapsysair_google_callback:
+    path: '/google/callback'
+    controller: Rapsys\AirBundle\Controller\UserController::googleCallback
     methods: GET
 
-rapsys_air_location_add:
+rapsysair_location:
     path:
         en_gb: '/en/location'
         fr_fr: '/emplacement'
-    controller: Rapsys\AirBundle\Controller\LocationController::add
-    methods: POST
-
-rapsys_air_location_edit:
-    path:
-        en_gb: '/en/location/{id<\d+>}'
-        fr_fr: '/emplacement/{id<\d+>}'
-    controller: Rapsys\AirBundle\Controller\LocationController::edit
-    methods: POST
+    controller: Rapsys\AirBundle\Controller\LocationController::index
+    methods: GET|POST
 
-rapsys_air_location_view:
+rapsysair_location_view:
     path:
-        en_gb: '/en/location/{id<\d+>}'
-        fr_fr: '/emplacement/{id<\d+>}'
+        en_gb: '/en/location/{id<\d+>}/{location<[\w-]+>?}'
+        fr_fr: '/emplacement/{id<\d+>}/{location<[\w-]+>?}'
     controller: Rapsys\AirBundle\Controller\LocationController::view
     methods: GET
 
-rapsys_air_application_add:
+rapsysair_organizer_regulation:
     path:
-        en_gb: '/en/application'
-        fr_fr: '/reservation'
-    controller: Rapsys\AirBundle\Controller\ApplicationController::add
-    methods: GET|POST
+        en_gb: '/en/organizer-regulation'
+        fr_fr: '/reglement-organisateur'
+    controller: Rapsys\AirBundle\Controller\DefaultController::organizerRegulation
+    methods: GET
 
-rapsys_air_session:
+rapsysair_session:
     path:
         en_gb: '/en/session'
         fr_fr: '/seance'
     controller: Rapsys\AirBundle\Controller\SessionController::index
     methods: GET
 
-rapsys_air_session_edit:
-    path:
-        en_gb: '/en/session/{id<\d+>}'
-        fr_fr: '/seance/{id<\d+>}'
-    controller: Rapsys\AirBundle\Controller\SessionController::edit
-    methods: POST
-
-rapsys_air_session_tangoargentin:
+rapsysair_session_tangoargentin:
     path:
-        en_gb: '/en/session/tangoargentin'
-        fr_fr: '/seance/tangoargentin'
-    format: json
+        en_gb: '/en/session/tangoargentin.{!_format?json}'
+        fr_fr: '/seance/tangoargentin.{!_format?json}'
     controller: Rapsys\AirBundle\Controller\SessionController::tangoargentin
     methods: GET
 
-rapsys_air_session_view:
+rapsysair_session_view:
     path:
-        en_gb: '/en/session/{id<\d+>}'
-        fr_fr: '/seance/{id<\d+>}'
+        en_gb: '/en/session/{id<\d+>}/{location<[\w-]+>?}/{dance<[\w-]*>?}/{user<[\w-]*>?}'
+        fr_fr: '/seance/{id<\d+>}/{location<[\w-]+>?}/{dance<[\w-]*>?}/{user<[\w-]*>?}'
     controller: Rapsys\AirBundle\Controller\SessionController::view
-    methods: GET
-
-rapsys_air_session_view2:
-    path:
-        en_gb: '/en/session2/{id<\d+>}'
-        fr_fr: '/seance2/{id<\d+>}'
-    controller: Rapsys\AirBundle\Controller\SessionController::view2
-    methods: GET
+    methods: GET|POST
 
-rapsys_air_snippet_add:
+rapsysair_snippet_add:
     path:
         en_gb: '/en/snippet'
         fr_fr: '/extrait'
     controller: Rapsys\AirBundle\Controller\SnippetController::add
     methods: POST
 
-rapsys_air_snippet_edit:
+rapsysair_snippet_edit:
     path:
         en_gb: '/en/snippet/{id<\d+>}'
         fr_fr: '/extrait/{id<\d+>}'
     controller: Rapsys\AirBundle\Controller\SnippetController::edit
     methods: POST
 
-rapsys_air_user:
+rapsysair_terms_of_service:
+    path:
+        en_gb: '/en/terms-of-service'
+        fr_fr: '/conditions-generales-d-utilisation'
+    controller: Rapsys\AirBundle\Controller\DefaultController::termsOfService
+    methods: GET
+
+rapsysair_user:
     path:
         en_gb: '/en/user'
         fr_fr: '/utilisateur'
     controller: Rapsys\AirBundle\Controller\DefaultController::userIndex
     methods: GET
 
-rapsys_air_user_view:
+rapsysair_user_milongaraphael:
+    path:
+        en_gb: '/en/milonga-raphael'
+        fr_fr: '/milonga-raphael'
+    controller: Rapsys\AirBundle\Controller\DefaultController::userView
+    defaults:
+        # default parameters
+        id: 1
+        user: 'milonga-raphael'
+
+rapsysair_user_view:
     path:
-        en_gb: '/en/user/{id<\d+>}'
-        fr_fr: '/utilisateur/{id<\d+>}'
+        en_gb: '/en/user/{id<\d+>}/{user<[\w-]+>?}'
+        fr_fr: '/utilisateur/{id<\d+>}/{user<[\w-]+>?}'
     controller: Rapsys\AirBundle\Controller\DefaultController::userView
     methods: GET|POST
 
-rapsys_user_confirm:
+rapsysuser_confirm:
     path:
         en_gb: '/en/confirm/{hash}/{mail}'
         fr_fr: '/confirmer/{hash}/{mail}'
-    controller: Rapsys\UserBundle\Controller\DefaultController::confirm
+    controller: Rapsys\UserBundle\Controller\UserController::confirm
     requirements:
         mail: '[a-zA-Z0-9=_-]+'
         hash: '[a-zA-Z0-9=_-]+'
     methods: GET|POST
 
-rapsys_user_edit:
+rapsysuser_edit:
     path:
         en_gb: '/en/user/{hash}/{mail}'
         fr_fr: '/utilisateur/{hash}/{mail}'
@@ -180,11 +194,11 @@ rapsys_user_edit:
         hash: '[a-zA-Z0-9=_-]+'
     methods: GET|POST
 
-rapsys_user_login:
+rapsysuser_login:
     path:
         en_gb: '/en/login/{hash}/{mail}'
         fr_fr: '/connecter/{hash}/{mail}'
-    controller: Rapsys\UserBundle\Controller\DefaultController::login
+    controller: Rapsys\UserBundle\Controller\UserController::login
     defaults:
         mail: ~
         hash: ~
@@ -193,17 +207,17 @@ rapsys_user_login:
         hash: '[a-zA-Z0-9=_-]+'
     methods: GET|POST
 
-rapsys_user_logout:
+rapsysuser_logout:
     path:
         en_gb: '/en/logout'
         fr_fr: '/deconnecter'
     methods: GET
 
-rapsys_user_recover:
+rapsysuser_recover:
     path:
         en_gb: '/en/recover/{hash}/{pass}/{mail}'
         fr_fr: '/recuperer/{hash}/{pass}/{mail}'
-    controller: Rapsys\UserBundle\Controller\DefaultController::recover
+    controller: Rapsys\UserBundle\Controller\UserController::recover
     defaults:
         mail: ~
         pass: ~
@@ -214,11 +228,11 @@ rapsys_user_recover:
         hash: '[a-zA-Z0-9=_-]+'
     methods: GET|POST
 
-rapsys_user_register:
+rapsysuser_register:
     path:
         en_gb: '/en/register/{hash}/{field}/{mail}'
         fr_fr: '/enregistrer/{hash}/{field}/{mail}'
-    controller: Rapsys\UserBundle\Controller\DefaultController::register
+    controller: Rapsys\UserBundle\Controller\UserController::register
     defaults:
         mail: ~
         field: ~
index 26cea93975b1554844485dc7e1cb9913bea71f4d..ffc1dcfba5a05604c46cd7b9b8d4c7d75adacd72 100644 (file)
@@ -4,6 +4,7 @@
        font-weight: 400;
        src: local('Droid Sans Regular'), local('DroidSans-Regular'), url(/bundles/rapsysair/woff2/droidsans.regular.woff2) format('woff2');
        unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+       font-display: swap;
 }
 @font-face {
        font-family: 'Droid Sans';
@@ -11,6 +12,7 @@
        font-weight: 700;
        src: local('Droid Sans Bold'), local('DroidSans-Bold'), url(/bundles/rapsysair/woff2/droidsans.bold.woff2) format('woff2');
        unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+       font-display: swap;
 }
 @font-face {
        font-family: 'Droid Sans Mono';
@@ -18,6 +20,7 @@
        font-weight: 400;
        src: local('Droid Sans Mono Regular'), local('DroidSansMono-Regular'), url(/bundles/rapsysair/woff2/droidsansmono.regular.woff2) format('woff2');
        unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+       font-display: swap;
 }
 @font-face {
        font-family: 'Droid Serif';
@@ -25,6 +28,7 @@
        font-weight: 400;
        src: local('Droid Serif Italic'), local('DroidSerif-Italic'), url(/bundles/rapsysair/woff2/droidserif.italic.woff2) format('woff2');
        unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+       font-display: swap;
 }
 @font-face {
        font-family: 'Droid Serif';
@@ -32,6 +36,7 @@
        font-weight: 700;
        src: local('Droid Serif Bold Italic'), local('DroidSerif-BoldItalic'), url(/bundles/rapsysair/woff2/droidserif.bolditalic.woff2) format('woff2');
        unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+       font-display: swap;
 }
 @font-face {
        font-family: 'Droid Serif';
@@ -39,6 +44,7 @@
        font-weight: 400;
        src: local('Droid Serif Regular'), local('DroidSerif-Regular'), url(/bundles/rapsysair/woff2/droidserif.regular.woff2) format('woff2');
        unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+       font-display: swap;
 }
 @font-face {
        font-family: 'Droid Serif';
@@ -46,4 +52,5 @@
        font-weight: 700;
        src: local('Droid Serif Bold'), local('DroidSerif-Bold'), url(/bundles/rapsysair/woff2/droidserif.bold.woff2) format('woff2');
        unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+       font-display: swap;
 }
diff --git a/Resources/public/css/lemon.css b/Resources/public/css/lemon.css
new file mode 100644 (file)
index 0000000..761732d
--- /dev/null
@@ -0,0 +1,7 @@
+@font-face {
+       font-family: 'Lemon';
+       font-style: normal;
+       font-weight: 400;
+       src: local('Lemon'), url(/bundles/rapsysair/woff2/lemon.woff2) format('woff2');
+       font-display: swap;
+}
diff --git a/Resources/public/css/notoemoji.css b/Resources/public/css/notoemoji.css
new file mode 100644 (file)
index 0000000..0bdc43b
--- /dev/null
@@ -0,0 +1,7 @@
+@font-face {
+       font-family: 'Noto Emoji';
+       font-style: normal;
+       font-weight: 400;
+       src: local('Noto Emoji'), local('Noto-Emoji'), url(/bundles/rapsysair/woff2/notoemoji.woff2) format('woff2');
+       font-display: swap;
+}
index 6d7d54c7a649553dab74d56133f4cae3dc6c2cf9..6a53959a209a13c2f5b9c2ca2fe89939de715509 100644 (file)
 /* Reset link */
 a {
        /*text-decoration: none;*/
-       color: #066;
+       color: #136;
 }
 
 a:hover {
        text-decoration: underline solid #00c3f9;
 }
 
-h1::first-letter,
-h2::first-letter,
-h3::first-letter,
-h4::first-letter,
-h5::first-letter,
-h6::first-letter,
-a::first-letter {
-       color: #00c3f9;
-}
-
 /* Font styling */
 html, body, dd, li, p, td {
        /* DejaVu Sans/FreeSans/FreeSerif/Linux Libertine/Symbola/Unifont*/
        font-family: 'Droid Sans', 'Symbola', 'DejaVu Sans', 'FreeSans', sans-serif;
-       font-display: swap;
 }
 
 button, code, input, option, optgroup, pre, select, textarea {
        font-family: 'Droid Sans Mono', monospace;
-       font-display: swap;
 }
 
 dt, h1, h2, h3, h4, h5, h6, label, legend, th, details {
        font-family: 'Droid Serif', serif;
-       font-display: swap;
 }
 
 /* Default styling */
 h1 {
-       font-size: 2rem;
+       font-size: 1.5rem;
+       margin: 0;
+       padding: 0 .6rem .6rem;
 }
 
 h2 {
-       font-size: 1.5rem;
-       margin: 0 0 .5rem;
-       padding: .5rem;
+       font-size: 1.17rem;
+       margin: 0;
+       padding: 0 .5rem .5rem;
 }
 
 h3 {
-       font-size: 1.17rem;
-       margin: 0 0 .5rem;
-       padding: .4rem;
+       font-size: 1rem;
+       margin: 0;
+       padding: 0 .4rem .4rem;
 }
 
 h4 {
-       font-size: 1rem;
-       margin: 0 0 .5rem;
-       padding: .3rem;
+       font-size: .85rem;
+       margin: 0;
+       padding: 0 .3rem .3rem;
 }
 
 h5 {
-       font-size: .85rem;
-       margin: 0 0 .5rem;
-       padding: .2rem;
+       font-size: .67rem;
+       margin: 0;
+       padding: 0 .2rem .2rem;
 }
 
 h6 {
-       font-size: .67rem;
-       margin: 0 0 .5rem;
-       padding: .1rem;
+       font-size: .5rem;
+       margin: 0;
+       padding: 0 .1rem .1rem;
 }
 
-header {
-       margin: 0 0 .5rem;
+ul {
+       list-style: ' - ' inside none;
+       margin: 0;
+       padding: 0 .5rem .5rem;
 }
 
-header h2 {
+li {
+       font-size: .9rem;
        margin: 0;
-       padding: .5rem .5rem .3rem;
-       border-bottom: 0 none;
+       padding: 0 .5rem .5rem;
+}
+
+li:only-child,
+li:last-child,
+li:last-of-type {
+       padding-bottom: 0;
+}
+
+p {
+       font-size: .9rem;
+       margin: 0;
+       padding: 0 .4rem .4rem;
+}
+
+article,
+section,
+nav {
+       margin: 0 .5rem .5rem;
+       border: .1rem solid #00c3f9;
+       border-top: 0;
+       border-left: 0;
+       border-radius: .5rem;
 }
 
-nav strong {
+article article {
+       margin: 0 .3rem .3rem;
+       border-radius: .4rem;
+}
+
+article article article {
+       margin: 0 .2rem .2rem;
+       border-radius: .3rem;
+}
+
+article section a.link {
        display: block;
-       font-size: 1.17rem;
-       margin: 0 0 .5rem;
-       padding: .4rem;
+       text-align: center;
+       margin: 0 auto;
+       padding: .5rem;
+       /*display: flex;
+       align-items: center;
+       gap: .5rem;*/
+}
+
+article section a.link img {
+       /*width: 12rem;*/
+       padding-right: .5rem;
+       text-align: right;
+       vertical-align: bottom;
+}
+
+article section a.link span {
+       width: 24rem;
+       line-height: 50px;
+       /*flex: 1 1;*/
+}
+
+section {
+       border-radius: .4rem;
+}
+
+section .center {
+       display: flex;
+       flex-direction: row;
+       justify-content: center;
+       align-items: center;
+       margin-bottom: .5rem;
+       /*display: block;
+       width: 24rem;*/
+       /*margin: 0 auto;*/
+       gap: .5rem;
 }
 
-h2, h3, h4, h5, header, nav strong {
+/*article {
        background-color: #cff;
-       border-bottom: .1rem solid #00c3f9;
 }
 
-strong {
-       font-weight: bold;
+article article {
+       background-color: white;
 }
 
-p {
-       margin: 0 .5rem .3rem;
-       font-size: .9rem;
+article article article {
+       border-radius: .3rem;
+       background-color: #cff;
+}*/
+
+header {
+       margin: 0 0 .5rem;
+       padding: .5rem;
+       border-radius: .4rem .4rem 0 0;
+       /*background-color: #369;
+       border-radius: inherit;*/
+       background-color: #00c3f9;
+       color: #136;
+}
+
+header h2,
+header h3,
+header h4,
+header h5,
+header h6,
+header p {
+       padding-left: 0;
+       padding-right: 0;
+}
+
+header h2:last-child,
+header h3:last-child,
+header h4:last-child,
+header h5:last-child,
+header h6:last-child,
+header p:last-child {
+       padding-bottom: 0;
+}
+
+/*p:only-child,
+p:last-child,
+p:last-of-type {
+       padding-bottom: 0;
+}*/
+
+/*header h2:first-child {
+       padding-top: .5rem;
+}
+
+header h3:first-child {
+       padding-top: .4rem;
+}
+
+header h4:first-child {
+       padding-top: .3rem;
+}
+
+header h5:first-child {
+       padding-top: .2rem;
+}
+
+header h6:first-child {
+       padding-top: .1rem;
+}
+
+header p:last-child {
+       padding-top: 0;
+}*/
+
+/*article header {
+       border-radius: .4rem .4rem 0 0;
+}*/
+
+/*article article header {
+       border-radius: .3rem .3rem 0 0;
+       margin-bottom: .3rem;
+}
+
+article article article header {
+       border-radius: .2rem .2rem 0 0;
+       margin-bottom: .2rem;
+}*/
+
+/*header h1,
+header h2,
+header h3,
+header h4,
+header h5,
+header h6,
+header p {
+       margin-bottom: 0;
+       padding-bottom: 0;
+}*/
+
+.ellipsis {
+       /*Required for ellipsis on h2/h3/h4 in header*/
+       display: grid;
+       margin-bottom: 0;
+}
+
+.ellipsis > * {
+       overflow-x: hidden;
+       text-overflow: ellipsis;
+       white-space: nowrap;
+}
+
+/*nav strong {
+       display: block;
+       font-size: 1.17rem;
+       margin: 0 0 .5rem;
+       padding: .4rem;
+}*/
+
+strong {
+       font-weight: bold;
 }
 
 pre {
@@ -117,6 +278,7 @@ dl:first-of-type {
 }
 
 dt {
+       color: #369;
        font-size: .9rem;
        font-weight: bold;
 }
@@ -126,38 +288,67 @@ dd {
        margin-left: 1rem;
 }
 
-body {
-       display: flex;
-       flex-flow: column wrap;
-       color: #066;
+.map figure,
+.multimap figure,
+.thumb figure {
+       text-align: center;
 }
 
-nav,
-section,
-article {
-       margin: 0 .5rem .5rem;
+.map img,
+.multimap img,
+.thumb img {
+       border-radius: .2rem;
        border: .1rem solid #00c3f9;
-       border-radius: .3rem;
+       aspect-ratio: 1;
 }
 
-ul {
-       display: grid;
-       margin: 0 .5rem .3rem;
-       font-size: .9rem;
-       list-style: ' - ' inside none;
-       gap: .3rem;
+.map dd {
+       margin: 0;
+}
+
+.map dd img {
+       width: 100%;
+       width: calc(100% - .2rem);
+       height: auto;
+}
+
+.thumb img {
+       width: 100%;
+       width: calc(100% - 1.2rem);
+}
+
+.four .multimap {
+       grid-column: span 4;
+}
+
+.three .multimap {
+       grid-column: span 3;
+}
+
+.two .multimap {
+       grid-column: span 2;
+}
+
+body {
+       display: flex;
+       flex-flow: column wrap;
+       color: #036;
 }
 
 nav ul {
        list-style: none inside none;
-       margin-bottom: .5rem;
        gap: .5rem;
+       /*margin: .5rem;*/
 }
 
 nav ul ul {
+       padding-top: .5rem;
+}
+
+/*nav ul ul {
        margin-top: .5rem;
        margin-bottom: 0;
-}
+}*/
 
 /* Form */
 label {
@@ -179,6 +370,17 @@ textarea {
        box-sizing: border-box;
 }
 
+optgroup {
+       border: none;
+}
+
+/*option[selected='selected'],*/
+/*option:not(:checked),*/
+option:checked {
+       background-color: #cff;
+}
+
+
 form {
        display: flex;
        flex-direction: column;
@@ -187,10 +389,6 @@ form {
        text-align: center;
 }
 
-form .row {
-       flex-direction: row;
-}
-
 form div {
        display: flex;
        align-content: space-around;
@@ -205,10 +403,27 @@ form div:last-of-type {
        margin-bottom: 0;
 }
 
+form div.row {
+       flex-direction: row;
+       gap: .5rem;
+       flex-wrap: wrap;
+}
+
 form label {
        width: 12rem;
-       padding-right: 1rem;
+       line-height: 1.3rem;
+       /*padding-right: 1rem;*/
        text-align: right;
+       overflow-x: hidden;
+       text-overflow: ellipsis;
+}
+
+form label.captcha {
+       line-height: normal;
+}
+
+form label.captcha img {
+       height: 1.325rem;
 }
 
 form input,
@@ -222,6 +437,24 @@ form section section {
        margin: 0 auto;
 }
 
+form optgroup,
+form option {
+       width: 100%;
+}
+
+/*form optgroup {
+       width: 23.8rem;
+}
+
+form option {
+       width: 23.6rem;
+}*/
+
+/*Checkbox input*/
+form input[type='checkbox'] {
+       width: 1.5rem;
+}
+
 form select {
        padding: 0 .1rem .1rem .1rem;
 }
@@ -229,10 +462,15 @@ form select {
 form button {
        width: 10rem;
        /*margin: .5rem auto 0 auto;*/
-       margin: 0 auto;
+       /*margin: 0 auto;*/
        padding: 0 .1rem .2rem .1rem;
 }
 
+/*form button img {
+       float: left;
+       padding: .5rem;
+}*/
+
 form .message {
        margin: 0 0 .5rem 0;
 }
@@ -242,6 +480,7 @@ form div .message {
 }
 
 /* Vertical form */
+.location .col label,
 .col label {
        width: 8rem;
        padding-right: 0;
@@ -250,6 +489,8 @@ form div .message {
        margin: 0 auto;
 }
 
+.location .col input,
+.location .col textarea,
 .col input,
 .col option,
 .col optgroup,
@@ -273,61 +514,84 @@ form div .message {
 
 /* Header */
 #header {
-       background-color: transparent;
-       border: .1rem solid #00c3f9;
+       background-color: #cff;
+       border: .1rem solid #136;
        border-top: 0;
+       border-left: 0;
        border-radius: 0 0 .5rem .5rem;
        margin: 0 .5rem .5rem;
        display: flex;
-       flex-direction: row;
-       flex-wrap: nowrap;
+       flex-direction: column;
        justify-content: space-between;
-       line-height: 32px;
-       font-size: 32px;
+       font-weight: bold;
+       line-height: 45px;
        padding: .5rem;
        gap: .5rem;
 }
 
-#header h1 {
+#header div {
+       display: flex;
+       flex-direction: row;
+       justify-content: space-between;
+       gap: .5rem;
+}
+
+#logo {
        padding: 0;
-       margin: auto;
        white-space: nowrap;
-       width: 171px;
-       height: 32px;
+       color: #09c;
+       text-shadow: 1.5px 1.5px 3px #136;
+       text-decoration: none;
+       display: flex;
+       font-family: 'Lemon', sans-serif;
+       font-size: 2rem;
+       line-height: 45px;
+       gap: .5rem;
 }
 
-#header h2 {
-       display: none;
+#logo img,
+#logo span {
+       margin: 0 auto;
 }
 
-#header nav {
-       display: flex;
+#title {
+       margin: .25rem 0 0;
+       padding: 0;
+       border-bottom: 0 none;
+       text-align: right;
        flex: 1 1 auto;
-       border-style: none;
-       margin: 0;
+       font-family: 'Lemon', sans-serif;
+       font-weight: normal;
+       /*white-space: nowrap;*/
+}
+
+#title a {
+       text-decoration: none;
+       color: #09c;
+       text-shadow: 1.5px 1.5px 3px #136;
+       /*overflow-x: hidden;
+       text-overflow: ellipsis;*/
 }
 
-#header ul {
+#nav {
        display: flex;
-       flex: 1 1 auto;
-       flex-direction: row;
        flex-wrap: wrap;
-       margin: 0;
        gap: .5rem;
+       line-height: 2.5rem;
+       margin: 0;
+       border: 0 none;
 }
 
-#header li {
-       display: flex;
+#nav a {
        flex: 1 1 auto;
+       border: .1rem solid #136;
+       border-top: 0;
+       border-left: 0;
        border-radius: .2rem;
-       border: .1rem solid #00c3f9;
-       background-color: #cff;
-       justify-content: center;
-}
-
-#header ul a {
+       background-color: #00c3f9;
+       color: #136;
        text-align: center;
-       font-weight: bold;
+       padding: 0 .25rem;
 }
 
 /* Message */
@@ -351,10 +615,15 @@ form div .message {
 
 .message ul {
        margin: 0;
+       padding: 0;
        list-style: none inside none;
        gap: .1rem;
 }
 
+.message li {
+       padding: .25rem;
+}
+
 .mortal,
 .mortal button,
 .mortal select {
@@ -430,7 +699,7 @@ form div .message {
 #form,
 #recover,
 #regulation {
-       border-radius: .5rem;
+       /*border-radius: .5rem;*/
        /*border: .1rem solid #00c3f9;
        margin: .5rem;
        margin-top: 0;
@@ -445,6 +714,9 @@ form div .message {
        gap: .5rem;
 }
 
+.cell {
+}
+
 .grid {
        display: grid;
        border-style: solid;
@@ -452,17 +724,19 @@ form div .message {
        border-radius: .2rem;
        box-sizing: border-box;
        border-collapse: collapse;
-       grid-gap: .1rem;
        flex: 1 1 auto;
+       margin: 0 0 auto 0;
+       padding: 0;
+       gap: .1rem;
 }
 
 .grid article,
 .grid section {
        border-collapse: inherit;
-       border-color: inherit;
-       border-radius: inherit;
+       /*border-radius: inherit;*/
        border-style: inherit;
-       border-width: .1rem;
+       /*border-width: .1rem;
+       border-width: inherit;*/
        box-sizing: inherit;
        flex-grow: inherit;
        margin: 0;
@@ -470,19 +744,27 @@ form div .message {
        overflow: hidden;
 }
 
-.grid h3 {
+/*.grid header {
+       border-radius: .2rem .2rem 0 0;
+       margin-bottom: 0;
+}*/
+
+/*.grid ul {
+       margin: .1rem;
+       padding: .1rem;
        margin: 0;
-       overflow: hidden;
-       text-overflow: ellipsis;
-       white-space: nowrap;
-}
+}*/
+
+/*.grid section {
+       border-color: inherit;
+}*/
 
-.grid ul {
+/*.grid ul {
        display: block;
        margin: .1rem;
        font-size: .8rem;
        list-style: none inside none;
-}
+}*/
 
 .grid li {
        border-width: .1rem;
@@ -494,10 +776,8 @@ form div .message {
        margin: 0 0 .1rem;
        flex-direction: row;
        justify-content: space-between;
-}
-
-.grid li a:first-letter {
-       color: inherit;
+       overflow-x: hidden;
+       text-overflow: ellipsis;
 }
 
 /*XXX: required by ul display:block for overflow:hidden*/
@@ -508,32 +788,38 @@ form div .message {
 }
 
 .grid a {
+       /*TODO: voir pourquoi on a besoin d'un overflow-y de merde, sans une putain de scrollbar s'affiche dans certaines conditions sur chrome, depuis le passage de deux lignes de grid à 3 !!!*/
+       /*Est-ce parce que le city est pas expanded ???*/
+       overflow-y: clip;
        overflow-x: hidden;
        text-overflow: ellipsis;
 }
 
 .grid p {
-       margin: 0 0 .3rem;
+       padding: 0 0 .3rem;
+       margin: 0;
 }
 
 .grid p:only-child,
 .grid p:last-child,
 .grid p:last-of-type {
-       margin: 0;
+       padding: 0;
 }
 
-.current,
-.current h3 {
-       background-color: #cfc;
-       border-color: #008000;
-       color: #008000;
+.session header {
+       margin-bottom: .5rem;
 }
 
-.current h3:first-letter {
-       color: #00b000;
+.current {
+       filter: hue-rotate(-90deg) saturate(2);
 }
 
-.granted,
+.granted {
+       background-color: #cff;
+       border-color: #00c3f9;
+}
+
+/*.granted,
 .granted a {
        background-color: #cff;
        border-color: #00c3f9;
@@ -544,9 +830,29 @@ form div .message {
        border-color: #930;
        background-color: #fc9;
        color: #930;
+}*/
+
+.highlight {
+       filter: hue-rotate(60deg);
+}
+
+.canceled {
+       filter: hue-rotate(180deg) grayscale(33%);
+}
+
+.locked {
+       filter: hue-rotate(180deg);
+}
+
+.pending {
+       filter: grayscale(33%);
+}
+
+.disabled {
+       filter: grayscale(66%);
 }
 
-.canceled,
+/*.canceled,
 .canceled a,
 .canceled h2,
 .canceled header {
@@ -569,22 +875,49 @@ form div .message {
 }
 
 .highlight,
-.highlight a {
+.highlight a,
+.highlight h3,
+.highlight h3 a {
        border-color: #3333c3;
        background-color: #c3c3f9;
-       color: #3333c3;
+       color: #606;
+}*/
+
+.calendar header {
+       margin-bottom: .1rem;
+       display: grid;
 }
 
-.disabled {
-       filter: grayscale(66%);
+.calendar h3 {
+       overflow-x: hidden;
+       text-overflow: ellipsis;
+       white-space: nowrap;
+}
+
+.calendar ul {
+       padding: 0 .1rem .1rem;
 }
 
+/*.ellipsis {
+       / *Required for ellipsis on h2/h3/h4 in header* /
+       display: grid;
+       margin-bottom: 0;
+}
+
+.ellipsis > * {
+       overflow-x: hidden;
+       text-overflow: ellipsis;
+       white-space: nowrap;
+}*/
+
 .calendar a {
        display: grid;
        /*grid-template-columns: 1fr auto fit-content(1fr);*/
        grid-template-columns: max-content 1fr max-content;
        flex: 1 1 auto;
        grid-gap: .1rem;
+       /*XXX: reset to visible to prevent scroll*/
+       overflow-x: visible;
 }
 
 .calendar .reducible {
@@ -593,6 +926,21 @@ form div .message {
        text-align: center;
 }
 
+.calendar .glyph {
+       font-family: 'Noto Emoji', 'Droid Sans', 'Symbola', 'DejaVu Sans', 'FreeSans', sans-serif;
+}
+
+.calendar .temperature,
+.calendar .rain,
+.calendar .rate {
+       line-height: 1rem;
+       text-align: right;
+}
+
+.calendar .rate {
+       text-align: center;
+}
+
 .calendar .info {
        line-height: 1rem;
        text-align: right;
@@ -618,23 +966,28 @@ form div .message {
        grid-template-columns: repeat(2, 1fr);
 }
 
-.location h3 {
+/*.city .grid h3,
+.location .grid h3,
+.city .grid h4,
+.location .grid h4 {
        border-style: none;
+       margin: 0;
        flex: 1 1 auto;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
 }
 
-.location p {
-       margin: 0 .5rem .5rem;
-}
-
-.location .grid article,
-.location .grid section {
+.city .grid article,
+.location .grid article {
        min-height: auto;
+       height: fit-content;
 }
 
+.city .grid {
+       gap: .5rem;
+}*/
+
 .location form {
        margin: .5rem auto;
 }
@@ -679,7 +1032,7 @@ form div .message {
 .form section {
        margin: 0;
        border: .05rem solid #00c3f9;
-       border-radius: .2rem;
+       /*border-radius: .2rem;*/
        width: 100%;
 }
 
@@ -696,9 +1049,11 @@ form div .message {
        padding: .5rem;
        text-align: center;
        font-size: .8rem;
+       line-height: 2.5rem;
        display: flex;
        justify-content: space-between;
        background-color: #cff;
+       gap: .5rem;
 }
 
 #footer summary::after {
@@ -714,6 +1069,23 @@ form div .message {
        margin: 0;
 }
 
+#footer a,
+#footer details {
+       padding: 0 .5rem;
+       border: .1rem solid #136;
+    border-top: 0;
+    border-left: 0;
+    border-radius: .2rem;
+       background-color: #00c3f9;
+       color: #136;
+       justify-content: center;
+}
+
+#footer details a {
+       padding: 0;
+       border: 0 none;
+}
+
 /* viewport responsive hack */
 @media ( max-width: 1400px ) {
        .location label,
@@ -721,22 +1093,37 @@ form div .message {
        .location textarea {
                width: 12rem;
        }
+
+       .session .three {
+               grid-template-columns: repeat(2, 1fr);
+       }
 }
 
-@media ( max-width: 900px ) {
+@media ( max-width: 1000px ) {
        .panel {
+               /*flex-flow: column-reverse wrap;*/
                flex-flow: column wrap;
        }
 
+       .location .two {
+               grid-template-columns: repeat(2, 1fr);
+       }
+}
+
+@media ( max-width: 900px ) {
        .form {
                flex-direction: row;
        }
 
        form label {
-               width: 6rem;
                line-height: 2rem;
        }
 
+       form label.captcha img {
+               height: auto;
+               padding: .25rem 0;
+       }
+
        form input,
        form option,
        form optgroup,
@@ -747,6 +1134,10 @@ form div .message {
                line-height: 2rem;
        }
 
+       form select {
+               height: 2.4rem;
+       }
+
        form button {
                width: 6rem;
                line-height: 2rem;
@@ -781,6 +1172,26 @@ form div .message {
                grid-template-columns: repeat(3, 1fr);
        }
 
+       .three {
+               grid-template-columns: repeat(2, 1fr);
+       }
+
+       .two {
+               grid-template-columns: repeat(1, 1fr);
+       }
+
+       .four .multimap {
+               grid-column: span 3;
+       }
+
+       .three .multimap {
+               grid-column: span 2;
+       }
+
+       .two .multimap {
+               grid-column: span 1;
+       }
+
        /*#dashboard .seventh:nth-child(7n+1),
        #dashboard .seventh:nth-child(7n+2),
        #dashboard .seventh:nth-child(7n+3),
@@ -795,6 +1206,18 @@ form div .message {
        }*/
 }
 
+@media ( max-width: 700px ) {
+       .session .three {
+               grid-template-columns: repeat(1, 1fr);
+       }
+
+       .multimap img {
+               width: 100%;
+               width: calc(100% - .2rem);
+               height: auto;
+       }
+}
+
 @media ( max-width: 600px ) {
        form label {
                margin: 0 auto;
@@ -806,11 +1229,11 @@ form div .message {
        form select,
        form textarea,
        form section section {
-               width: 14rem;
+               width: 12rem;
                margin: 0 auto;
        }
 
-       #header {
+       #logo {
                flex-direction: column;
        }
 
@@ -838,11 +1261,26 @@ form div .message {
                margin-bottom: 0;
        }
 
-       .four,
-       .three {
+       .four {
                grid-template-columns: repeat(2, 1fr);
        }
 
+       .three {
+               grid-template-columns: repeat(1, 1fr);
+       }
+
+       .four .multimap {
+               grid-column: span 2;
+       }
+
+       .three .multimap {
+               grid-column: span 1;
+       }
+
+       .location .two {
+               grid-template-columns: repeat(1, 1fr);
+       }
+
        /*#dashboard .seventh:nth-child(n) {
                width: calc(100% / 2 - .1rem);
        }
@@ -850,9 +1288,45 @@ form div .message {
        #dashboard .seventh:nth-child(7n) {
                width: calc(100%);
        }*/
+
+       /*.city .two,
+       .location .two {
+               grid-template-columns: 1fr;
+       }
+
+       .city .grid article,
+       .location .grid article {
+               overflow: hidden;
+       }*/
 }
 
 @media ( max-width: 450px ) {
+       #header div {
+               flex-direction: column;
+       }
+
+       #title {
+               text-align: center;
+       }
+
+       #logo {
+               flex-direction: row;
+       }
+
+       dd, p, #footer {
+               font-size: 110%;
+       }
+
+       a, dd, figcaption, #footer {
+               /*XXX: required to validate Tap targets are sized appropriately*/
+               line-height: 3rem;
+       }
+
+       figure,
+       .calendar a {
+               line-height: normal;
+       }
+
        form .row,
        .grid,
        .form,
@@ -860,6 +1334,10 @@ form div .message {
                flex-direction: column;
        }
 
+       form label {
+               text-align: center;
+       }
+
        .seven {
                grid-template-columns: repeat(1, 1fr);
        }
@@ -871,10 +1349,23 @@ form div .message {
                grid-column: auto;
        }
 
-       .four,
-       .three {
+       .four {
                grid-template-columns: repeat(1, 1fr);
        }
+
+       .four .multimap {
+               grid-column: span 1;
+       }
+}
+
+@media ( max-width: 320px ) {
+       #logo {
+               flex-direction: column;
+       }
+
+       #title {
+               white-space: normal;
+       }
 }
 
 @media ( max-width: 260px ) {
diff --git a/Resources/public/facebook/.keep b/Resources/public/facebook/.keep
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/Resources/public/jpeg/calendar.jpeg b/Resources/public/jpeg/calendar.jpeg
new file mode 100644 (file)
index 0000000..9620a29
Binary files /dev/null and b/Resources/public/jpeg/calendar.jpeg differ
diff --git a/Resources/public/jpeg/facebook.1200x1200.jpeg b/Resources/public/jpeg/facebook.1200x1200.jpeg
new file mode 100644 (file)
index 0000000..7667e3c
Binary files /dev/null and b/Resources/public/jpeg/facebook.1200x1200.jpeg differ
diff --git a/Resources/public/jpeg/logo.en.jpeg b/Resources/public/jpeg/logo.en.jpeg
new file mode 100644 (file)
index 0000000..b2ab92b
Binary files /dev/null and b/Resources/public/jpeg/logo.en.jpeg differ
diff --git a/Resources/public/jpeg/logo.fr.jpeg b/Resources/public/jpeg/logo.fr.jpeg
new file mode 100644 (file)
index 0000000..47f8cbc
Binary files /dev/null and b/Resources/public/jpeg/logo.fr.jpeg differ
diff --git a/Resources/public/png/calendar.png b/Resources/public/png/calendar.png
new file mode 100644 (file)
index 0000000..eb50b49
Binary files /dev/null and b/Resources/public/png/calendar.png differ
diff --git a/Resources/public/png/facebook.1200x1200.png b/Resources/public/png/facebook.1200x1200.png
new file mode 100644 (file)
index 0000000..8feb332
Binary files /dev/null and b/Resources/public/png/facebook.1200x1200.png differ
diff --git a/Resources/public/png/logo.171.png b/Resources/public/png/logo.171.png
new file mode 100644 (file)
index 0000000..1140bdf
Binary files /dev/null and b/Resources/public/png/logo.171.png differ
diff --git a/Resources/public/png/logo.orig.png b/Resources/public/png/logo.orig.png
new file mode 100644 (file)
index 0000000..9528d60
Binary files /dev/null and b/Resources/public/png/logo.orig.png differ
index 1140bdfa250ca2b75bccfd5cbfb8a1280e3c150c..40c6ba957d843d0f7f2350c48c74d500b426d86e 100644 (file)
Binary files a/Resources/public/png/logo.png and b/Resources/public/png/logo.png differ
diff --git a/Resources/public/svg/calendar.svg b/Resources/public/svg/calendar.svg
new file mode 100644 (file)
index 0000000..c32c0c7
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Livello_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+        viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
+<g>
+       <g transform="translate(3.75 3.75)">
+               <path fill="#FFFFFF" d="M148.882,43.618l-47.368-5.263l-57.895,5.263L38.355,96.25l5.263,52.632l52.632,6.579l52.632-6.579
+                       l5.263-53.947L148.882,43.618z"/>
+               <path fill="#1A73E8" d="M65.211,125.276c-3.934-2.658-6.658-6.539-8.145-11.671l9.132-3.763c0.829,3.158,2.276,5.605,4.342,7.342
+                       c2.053,1.737,4.553,2.592,7.474,2.592c2.987,0,5.553-0.908,7.697-2.724s3.224-4.132,3.224-6.934c0-2.868-1.132-5.211-3.395-7.026
+                       s-5.105-2.724-8.5-2.724h-5.276v-9.039H76.5c2.921,0,5.382-0.789,7.382-2.368c2-1.579,3-3.737,3-6.487
+                       c0-2.447-0.895-4.395-2.684-5.855s-4.053-2.197-6.803-2.197c-2.684,0-4.816,0.711-6.395,2.145s-2.724,3.197-3.447,5.276
+                       l-9.039-3.763c1.197-3.395,3.395-6.395,6.618-8.987c3.224-2.592,7.342-3.895,12.342-3.895c3.697,0,7.026,0.711,9.974,2.145
+                       c2.947,1.434,5.263,3.421,6.934,5.947c1.671,2.539,2.5,5.382,2.5,8.539c0,3.224-0.776,5.947-2.329,8.184
+                       c-1.553,2.237-3.461,3.947-5.724,5.145v0.539c2.987,1.25,5.421,3.158,7.342,5.724c1.908,2.566,2.868,5.632,2.868,9.211
+                       s-0.908,6.776-2.724,9.579c-1.816,2.803-4.329,5.013-7.513,6.618c-3.197,1.605-6.789,2.421-10.776,2.421
+                       C73.408,129.263,69.145,127.934,65.211,125.276z"/>
+               <path fill="#1A73E8" d="M121.25,79.961l-9.974,7.25l-5.013-7.605l17.987-12.974h6.895v61.197h-9.895L121.25,79.961z"/>
+               <path fill="#EA4335" d="M148.882,196.25l47.368-47.368l-23.684-10.526l-23.684,10.526l-10.526,23.684L148.882,196.25z"/>
+               <path fill="#34A853" d="M33.092,172.566l10.526,23.684h105.263v-47.368H43.618L33.092,172.566z"/>
+               <path fill="#4285F4" d="M12.039-3.75C3.316-3.75-3.75,3.316-3.75,12.039v136.842l23.684,10.526l23.684-10.526V43.618h105.263
+                       l10.526-23.684L148.882-3.75H12.039z"/>
+               <path fill="#188038" d="M-3.75,148.882v31.579c0,8.724,7.066,15.789,15.789,15.789h31.579v-47.368H-3.75z"/>
+               <path fill="#FBBC04" d="M148.882,43.618v105.263h47.368V43.618l-23.684-10.526L148.882,43.618z"/>
+               <path fill="#1967D2" d="M196.25,43.618V12.039c0-8.724-7.066-15.789-15.789-15.789h-31.579v47.368H196.25z"/>
+       </g>
+</g>
+</svg>
diff --git a/Resources/public/svg/logo.orig.svg b/Resources/public/svg/logo.orig.svg
new file mode 100644 (file)
index 0000000..2af678e
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- License http://creativecommons.org/licenses/by-nc-sa/4.0/ -->
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" preserveAspectRatio="xMidYMid slice" viewBox="0 0 684 128" width="684" height="128">
+       <defs>
+               <radialGradient id="radial" cx=".5" cy=".55" fx=".5" fy=".5" r=".5">
+                       <stop offset="0%" stop-color="#00c3f9" stop-opacity="0" />
+                       <stop offset="40%" stop-color="#00c3f9" stop-opacity="0.33" />
+                       <stop offset="100%" stop-color="#00c3f9" stop-opacity="1" />
+               </radialGradient>
+               <radialGradient id="radial2" cx=".55" cy=".5" fx=".5" fy=".5" r=".5">
+                       <stop offset="0%" stop-color="#00c3f9" stop-opacity="0" />
+                       <stop offset="40%" stop-color="#00c3f9" stop-opacity="0.33" />
+                       <stop offset="100%" stop-color="#00c3f9" stop-opacity="0.66" />
+               </radialGradient>
+       </defs>
+       <text x="0" y="126" textLength="128px" fill="url('#radial')" font-family="Arial, Helvetica, Regular, sans-serif" font-size="184px" transform="scale(1.02425 1)" transform-origin="0 0">A</text>
+       <text x="140" y="126" textLength="100px" fill="#000" font-family="Arial, Helvetica, Regular, sans-serif" font-size="150px">ir</text>
+       <text x="280" y="126" textLength="108px" fill="url('#radial2')" font-family="Arial, Helvetica, Regular, sans-serif" font-size="184px" transform="scale(1.02425 1)" transform-origin="0 0">L</text>
+       <text x="400" y="126" textLength="290px" fill="#000" font-family="Arial, Helvetica, Regular, sans-serif" font-size="150px">ibre</text>
+</svg>
diff --git a/Resources/public/svg/logo.svg b/Resources/public/svg/logo.svg
new file mode 100644 (file)
index 0000000..fe5f178
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- License http://creativecommons.org/licenses/by-nc-sa/4.0/ -->
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" preserveAspectRatio="xMidYMid slice" viewBox="0 0 200 90" width="200" height="90">
+       <defs>
+               <filter id="shadow">
+                       <!--<feDropShadow dx="3" dy="3" stdDeviation="2" style="flood-color: #136" />-->
+                       <!--XXX: see https://www.alexgeorgiou.gr/render-svg-fedropshadow-filter/ -->
+                       <feGaussianBlur in="SourceAlpha" stdDeviation="2" />
+                       <feOffset dx="3" dy="3" result="offsetblur" />
+                       <feFlood flood-color="#136" flood-opacity="1" />
+                       <feComposite in2="offsetblur" operator="in" />
+                       <feMerge>
+                               <feMergeNode/>
+                               <feMergeNode in="SourceGraphic"/>
+                       </feMerge>
+               </filter>
+       </defs>
+       <path style="fill:#0099cc;fill-opacity:1;filter:url(#shadow)" d="m 80.980603,81.878807 c -1.220301,-0.617241 -2.561639,-1.616229 -2.980749,-2.21997 L 77.237857,78.56111 h 1.635814 1.635816 l 1.650639,-0.793246 c 0.907852,-0.436304 2.051637,-1.26878 2.541757,-1.849975 l 0.891122,-1.056731 0.01219,-1.62782 0.01218,-1.627803 -0.729653,-1.332156 C 83.825329,68.333753 81.00396,65.898874 78.80001,65.019627 l -1.91754,-0.764988 h -2.847545 c -1.566149,0 -6.157473,0.363199 -10.202959,0.807132 L 56.476534,65.868886 52.732,65.314206 48.98745,64.759523 46.413852,63.512128 c -2.809958,-1.361972 -2.919284,-1.323902 -1.848987,0.643672 0.968423,1.780309 2.589884,3.134839 5.86257,4.897469 l 2.718926,1.464402 -4.90702,0.308309 -4.907018,0.308311 -3.768556,1.210012 -3.768532,1.210007 -4.35872,2.486251 -4.358723,2.486249 -3.218288,0.784367 -3.218287,0.784392 -2.425303,-0.15053 -2.425307,-0.150513 2.084246,-1.314344 2.084254,-1.31433 1.631833,-2.356428 c 1.968406,-2.842417 2.779155,-5.451512 2.819907,-9.074705 L 24.441293,63.021426 22.791711,59.948169 21.142129,56.87492 19.748603,56.001554 18.355097,55.128192 12.504729,55.086438 6.6543695,55.044664 5.8089173,54.235995 4.9634647,53.427332 5.0802228,51.270504 5.1969809,49.113676 4.3920438,48.474719 3.587081,47.835729 3.8814135,46.948766 4.175726,46.061812 3.1357439,45.529446 2.095762,44.997081 2.5951833,43.74065 C 3.3455031,41.852999 3.2323184,41.556379 1.5473023,40.994532 L 0,40.478599 V 39.559437 38.640258 L 0.972242,37.757709 C 2.1439476,36.694125 4.1215298,33.634201 4.1242102,32.880612 4.1252847,32.582928 3.6039144,32.09846 2.9656627,31.804018 L 1.8051858,31.268699 2.9308991,31.235291 4.0566325,31.201884 4.3865813,30.207478 4.7165508,29.213064 4.1600704,27.600233 C 3.558186,25.855912 3.5071574,25.316731 3.4505855,20.102056 l -0.037565,-3.453285 1.133673,-2.083527 C 5.1702036,13.41929 6.4955227,11.701988 7.4918674,10.748994 L 9.3033883,9.0162928 10.711922,8.7219669 C 11.486609,8.5599851 13.513026,7.782821 15.215059,6.9947225 16.91709,6.2066043 19.809061,5.2011116 21.641663,4.7602645 l 3.332,-0.8015198 6.467579,0.2167918 6.467582,0.2167919 4.899789,1.5925313 4.899789,1.5925512 8.952466,4.4537671 c 11.171117,5.557535 15.700997,7.419532 21.528885,8.849401 l 4.590838,1.126337 5.673441,-0.0016 5.673442,-0.0016 3.094588,-0.613028 c 1.702048,-0.337153 5.415568,-1.313376 8.252278,-2.169372 l 5.15768,-1.556349 15.0851,-7.233253 15.08511,-7.2332535 4.35997,-1.3278833 4.35994,-1.32790291 3.76449,-0.27146552 L 157.05108,0 l 3.30958,0.81187158 3.30957,0.81189102 2.87089,1.5710898 c 3.54754,1.9413948 6.83279,5.0298125 8.35745,7.8567266 l 1.15611,2.143601 0.20783,3.946612 0.20785,3.946611 -1.01905,2.95996 -1.01901,2.959959 -0.0516,-2.502081 -0.0516,-2.502073 -0.78805,-2.212144 c -0.43345,-1.21669 -1.45413,-2.985072 -2.26823,-3.929771 l -1.48022,-1.717595 -2.98581,-1.398405 -2.9858,-1.398386 -4.1587,-0.247323 -4.15872,-0.247343 -3.21048,0.787593 c -1.76574,0.433175 -4.6004,1.458693 -6.29922,2.278956 l -3.08875,1.49134 -3.75825,3.126467 c -10.60845,8.8252 -20.8191,13.765827 -34.50795,16.697389 l -5.35388,1.146868 -10.573205,-0.02506 -10.573234,-0.02507 -4.679249,-1.0665 c -2.573577,-0.586585 -6.578349,-1.709309 -8.899506,-2.49496 l -4.220278,-1.428455 0.517939,0.596926 c 0.873657,1.006893 7.262883,3.590467 11.444314,4.627676 l 3.962799,0.98298 6.151076,0.589194 6.151072,0.589196 6.597173,-0.562022 c 12.305219,-1.048267 24.225689,-4.12954 36.173519,-9.350351 l 5.71667,-2.498 4.06707,-1.051212 4.06708,-1.051231 5.67346,-0.256006 5.67344,-0.256024 4.96684,0.984807 4.96685,0.984809 4.49413,2.014402 c 5.72726,2.567084 10.06375,5.533725 14.0562,9.61598 l 3.16262,3.233755 1.79918,3.102597 c 2.18336,3.764897 3.019,6.618442 3.019,10.309068 v 2.788031 l -1.00048,2.155971 -1.00047,2.155974 -2.1472,1.849973 c -1.18095,1.017497 -2.39424,1.849973 -2.69623,1.849973 h -0.54901 l 0.35578,-1.072245 c 0.19576,-0.58972 0.36457,-2.976196 0.37529,-5.303259 l 0.0203,-4.231014 -1.14185,-2.46663 c -1.38263,-2.986821 -3.23454,-4.931324 -5.98633,-6.285677 l -2.06304,-1.015379 -3.35251,-0.221396 -3.35249,-0.221376 -3.48605,0.782545 c -5.79514,1.300906 -11.15405,4.265952 -16.62889,9.200712 -1.70203,1.534149 -4.82754,4.166957 -6.94556,5.850716 l -3.85101,3.061383 -3.62761,1.713821 c -1.99523,0.942606 -4.90419,2.038333 -6.4644,2.434937 l -2.83671,0.721111 -5.15768,0.002 -5.15767,0.002 -4.12613,-0.948698 c -2.26939,-0.521804 -4.59034,-1.081224 -5.15768,-1.243187 -0.56734,-0.161992 -0.006,0.162198 1.24679,0.720337 3.22984,1.438607 8.96226,2.795391 13.54677,3.206309 l 3.92525,0.351817 3.79267,-0.600558 3.79265,-0.600577 3.92603,-1.435521 3.92603,-1.435522 3.03682,-2.018889 c 1.67029,-1.110392 4.77756,-3.611635 6.90509,-5.558312 6.4152,-5.869849 10.13303,-8.235736 14.04506,-8.937717 l 1.87651,-0.336703 1.70987,0.552425 c 0.94042,0.303844 2.29007,0.999125 2.99927,1.545048 l 1.28941,0.992633 -2.06307,-0.361645 c -1.13468,-0.198967 -3.01995,-0.335313 -4.18948,-0.303052 l -2.12643,0.05866 -2.5155,1.233626 -2.51546,1.233625 -7.48872,7.316346 -7.48871,7.316362 -3.34239,1.8658 -3.3424,1.865806 -3.61041,1.093843 -3.61034,1.093865 -5.67347,-0.03069 -5.67344,-0.03069 -4.12614,-1.356727 c -5.09668,-1.675804 -9.7056,-4.06432 -18.296203,-9.481665 -6.82564,-4.30435 -16.046484,-9.084082 -17.503404,-9.073111 l -0.819909,0.0058 2.930553,1.597136 c 1.611794,0.878432 4.164844,2.623194 5.673443,3.877254 l 2.742886,2.280102 1.805186,2.879997 1.805189,2.880016 v 3.976657 3.976679 L 92.309001,78.32371 91.00565,80.780085 88.718286,81.890069 86.430927,83 H 84.816149 83.20137 l -2.218733,-1.122243 z m 50.751337,-29.21691 3.56523,-0.722432 3.34269,-1.496159 c 4.37707,-1.959166 8.96127,-5.15058 12.47853,-8.687226 l 2.83672,-2.852377 -2.32095,1.736062 c -6.44842,4.823356 -13.3267,8.484826 -19.19067,10.215624 l -3.45977,1.021187 -3.76097,0.323787 -3.76101,0.323789 -4.12612,-0.587191 c -2.26939,-0.322973 -7.72363,-1.439911 -12.12054,-2.482114 -7.898424,-1.872153 -15.859209,-3.248321 -16.307925,-2.819128 -0.127113,0.12157 0.763012,0.366772 1.978133,0.544836 1.215096,0.178103 6.154889,1.345576 10.977332,2.594489 4.82241,1.248932 10.39269,2.55576 12.37842,2.904081 l 3.61033,0.633281 5.15769,0.03594 5.15768,0.03594 3.5652,-0.722414 z M 5.1051834,20.101925 5.630698,18.128619 l -0.7530003,0.957619 -0.7530001,0.957594 0.040613,1.509036 c 0.044671,1.656336 0.1593994,1.479164 0.9397728,-1.450943 z" />
+</svg>
diff --git a/Resources/public/ttf/default.ttf b/Resources/public/ttf/default.ttf
new file mode 120000 (symlink)
index 0000000..a768014
--- /dev/null
@@ -0,0 +1 @@
+irishgrover.v10.ttf
\ No newline at end of file
diff --git a/Resources/public/ttf/lemon.ttf b/Resources/public/ttf/lemon.ttf
new file mode 100644 (file)
index 0000000..fa42b31
Binary files /dev/null and b/Resources/public/ttf/lemon.ttf differ
diff --git a/Resources/public/ttf/notoemoji.ttf b/Resources/public/ttf/notoemoji.ttf
new file mode 100644 (file)
index 0000000..19b7bad
Binary files /dev/null and b/Resources/public/ttf/notoemoji.ttf differ
diff --git a/Resources/public/woff2/lemon.woff2 b/Resources/public/woff2/lemon.woff2
new file mode 100644 (file)
index 0000000..4803498
Binary files /dev/null and b/Resources/public/woff2/lemon.woff2 differ
diff --git a/Resources/public/woff2/notoemoji.woff2 b/Resources/public/woff2/notoemoji.woff2
new file mode 100644 (file)
index 0000000..fb9f6c6
Binary files /dev/null and b/Resources/public/woff2/notoemoji.woff2 differ
index 65f757b0303380434a073d63153c049c6db30b22..e6c2f81533bb55a0248709f8013a719bc5de0566 100644 (file)
@@ -1,20 +1,19 @@
-'%pseudonym% outdoor Argentine Tango session calendar': '%pseudonym% outdoor Argentine Tango session calendar'
-'%title%''s mission is simple: provide dancers with the information they need to easily find organizers dance sessions.': '%title%''s mission is simple: provide dancers with the information they need to easily find organizers dance sessions.'
-'%title%, an information site for outdoor dancers and organizers.': '%title%, an information site for outdoor dancers and organizers.'
 ':': ':'
-'About Libre Air': 'About Libre Air'
+'about': 'about'
 'About': 'About'
+'About Libre Air': 'About Libre Air'
 'Absence of cancellation reporting': 'Absence of cancellation reporting'
 'Absence the first hour in cancellation case': 'Absence the first hour in cancellation case'
 'Abstract': 'Abstract'
 'Access denied': 'Access denied'
+'Access map': 'Access map'
 'Account %mail% already exists': 'Account %mail% already exists'
 'Account %mail% created but unable to contact': 'Account %mail% created but unable to contact'
 'Account %mail% do not exists': 'Account %mail% do not exists'
-'Account %mail% or with slug %slug% already exists': 'Account %mail% or with slug %slug% already exists'
 'Account %mail% password updated': 'Account %mail% password updated'
-'Account %mail% updated but unable to contact': 'Account %mail% updated but unable to contact'
 'Account %mail% updated': 'Account %mail% updated'
+'Account %mail% updated but unable to contact': 'Account %mail% updated but unable to contact'
+'Activity': 'Activity'
 'Add': 'Add'
 'Address': 'Address'
 'Admin': 'Admin'
 'All slot canceled and account disabled from one month up to permanent suspension': 'All slot canceled and account disabled from one month up to permanent suspension'
 'All slot canceled and account disabled from two month up to permanent suspension': 'All slot canceled and account disabled from two month up to permanent suspension'
 'All slot canceled and account disabled from two weeks to one month': 'All slot canceled and account disabled from two weeks to one month'
-'Anyone can create an account on Air Libre, dancer or organizer.': 'Anyone can create an account on Air Libre, dancer or organizer.'
+' and ': ' and '
 'Anyone': 'Anyone'
+'Anyone can create an account on Air Libre, dancer or organizer.': 'Anyone can create an account on Air Libre, dancer or organizer.'
 'Anyplace': 'Anyplace'
-'Application %id% updated': 'Application %id% updated'
-'Application %id%': 'Application %id%'
 'Application add': 'Application add'
 'Application': 'Application'
+'Application %id%': 'Application %id%'
+'Application %id% updated': 'Application %id% updated'
+'Argentine Tango': 'Argentine Tango'
+'Argentine Tango at Auber metro station': 'Argentine Tango in Auber metro station'
 'Argentine Tango at Bastille place': 'Argentine Tango in Bastille place'
 'Argentine Tango at Colette place': 'Argentine Tango in Colette place'
 'Argentine Tango at Drawings'' garden': 'Argentine Tango in Drawings'' garden'
 'Argentine Tango at Igor Stravinsky place': 'Argentine Tango on Igor Stravinsky place'
 'Argentine Tango at Jussieu esplanade': 'Argentine Tango on Jussieu esplanade'
 'Argentine Tango at Louvre palace': 'Argentine Tango in Louvre palace'
+'Argentine Tango at Lyon rail station': 'Argentine Tango in Lyon rail station'
 'Argentine Tango at Madeleine place': 'Argentine Tango in Madeleine place'
 'Argentine Tango at Monde garden': 'Argentine Tango in Monde garden'
 'Argentine Tango at Orleans gallery': 'Argentine Tango in Orleans gallery'
 'Argentine Tango at Orsay museum': 'Argentine Tango in front of Orsay museum'
 'Argentine Tango at Saint-Honore market': 'Argentine Tango in Saint-Honore market'
+'Argentine Tango at Saint-Sulpice place': 'Argentine Tango in Saint-Sulpice place'
 'Argentine Tango at Swan island': 'Argentine Tango on Swan island'
 'Argentine Tango at Tino-Rossi garden': 'Argentine Tango in Tino-Rossi garden'
 'Argentine Tango at Tokyo palace': 'Argentine Tango in Tokyo palace'
 'Argentine Tango at Trocadero esplanade': 'Argentine Tango on Trocadero esplanade'
 'Argentine Tango at Vendome place': 'Argentine Tango in Vendome place'
 'Argentine Tango at Vivian Maier garden': 'Argentine Tango in Vivian Maier garden'
+'Argentine Tango ball and class': 'Argentine Tango ball and class'
+'Argentine Tango ball and concert': 'Argentine Tango ball and concert'
+'Argentine Tango ball': 'Argentine Tango ball'
 'Argentine Tango in Paris': 'Argentine Tango in Paris'
 'Argentine Tango organizers': 'Argentine Tango organizers'
-'Argentine Tango': 'Argentine Tango'
+'Argentine Tango private class': 'Argentine Tango private class'
+'Argentine Tango public class': 'Argentine Tango public class'
+'around Auber metro station': 'around Auber metro station'
+'around Bastille place': 'around Bastille place'
+'around Champs-Elysées kiosk': 'around Champs-Elysées kiosk'
+'around Colette place': 'around Colette place'
+'around Docks': 'around Docks'
+'around Drawings'' garden': 'around Drawings'' garden'
+'around Earth theater': 'around Earth theater'
+'around Garnier': 'around Garnier'
+'around Garnier opera': 'around Garnier opera'
+'around Honore': 'around St-Honore'
+'around Igor Stravinsky place': 'around Igor Stravinsky place'
+'around Jussieu esplanade': 'around Jussieu esplanade'
+'around Louvre palace': 'around Louvre palace'
+'around Lyon rail station': 'around Lyon rail station'
+'around Madeleine place': 'around Madeleine place'
+'around Monde': 'around Monde'
+'around Monde garden': 'around Monde garden'
+'around Odeon theater': 'around Odeon theater'
+'around Orleans gallery': 'around Orleans gallery'
+'around Orsay museum': 'around front of Orsay museum'
+'around Saint-Honore market': 'around Saint-Honore market'
+'around Saint-Sulpice place': 'around Saint-Sulpice place'
+'Around %start% until %stop%': 'Around %start% until %stop%'
+'around Swan island': 'around Swan island'
+'around Tino-Rossi garden': 'around Tino-Rossi garden'
+'around Tokyo palace': 'around Tokyo palace'
+'around Trocadero': 'around Trocadero'
+'around Trocadero esplanade': 'around Trocadero esplanade'
+'around Vendome place': 'around Vendome place'
+'around Vivian Maier garden': 'around Vivian Maier garden'
 'Assit at location an organizer': 'Assit at location an organizer'
 'Assited or organized once at location': 'Assited or organized once at location'
+'at Auber metro station': 'in Auber metro station'
+'at Bastille place': 'in Bastille place'
+'at Champs-Elysées kiosk': 'at Champs-Elysées kiosk'
+'at Colette place': 'in Colette place'
+'at Drawings'' garden': 'in Drawings'' garden'
+'at Earth theater': 'at Earth theater'
+'at Garnier opera': 'at Garnier opera'
+'at Igor Stravinsky place': 'on Igor Stravinsky place'
+'at Jussieu esplanade': 'on Jussieu esplanade'
+'at Louvre palace': 'in Louvre palace'
+'at Lyon rail station': 'in Lyon rail station'
+'at Madeleine place': 'in Madeleine place'
+'at Monde garden': 'in Monde garden'
+'at Odeon theater': 'at Odeon theater'
+'at Orleans gallery': 'in Orleans gallery'
+'at Orsay museum': 'in front of Orsay museum'
+'at Saint-Honore market': 'at Saint-Honore market'
+'at Saint-Sulpice place': 'at Saint-Sulpice place'
+'at Swan island': 'on Swan island'
 'Attended once since start until end': 'Attended once since start until end'
+'at Tino-Rossi garden': 'at Tino-Rossi garden'
+'at Tokyo palace': 'in Tokyo palace'
 'Attribute': 'Attribute'
 'Attributed to': 'Attributed to'
+'at Trocadero esplanade': 'at Trocadero esplanade'
+'at Varenne ballroom': 'at Varenne ballroom'
+'at Vendome place': 'in Vendome place'
+'at Vivian Maier garden': 'in Vivian Maier garden'
+'Auber metro station access map': 'Auber metro station access map'
+'Auber metro station': 'Auber metro station'
+'Auber metro station miniature': 'Auber metro station miniature'
+'Auber metro station sector map': 'Auber metro station sector map'
 'Auto attribute': 'Auto attribute'
 'Autonomous enclosure 200W RMS (Ibiza Sound Port 8 or superior)': 'Autonomous enclosure 200W RMS (Ibiza Sound Port 8 or superior)'
 'Autonomous enclosure 250W RMS (Ibiza Sound Port 10 or superior)': 'Autonomous enclosure 250W RMS (Ibiza Sound Port 10 or superior)'
 'Autonomous music player (with Bluetooth, Jack, MiniJack or RCA connectivity)': 'Autonomous music player (with Bluetooth, Jack, MiniJack or RCA connectivity)'
 'Backup battery': 'Backup battery'
+'Ball and class': 'Ball and class'
+'Ball and concert': 'Ball and concert'
+'Ball': 'Ball'
+'Bastille place access map': 'Bastille place access map'
 'Bastille place': 'Bastille place'
+'Bastille place miniature': 'Bastille place miniature'
+'Bastille place sector map': 'Bastille place sector map'
 'Begin': 'Begin'
 'Broom and/or squeegee depending on location': 'Broom and/or squeegee depending on location'
+'by %pseudonym%': 'by %pseudonym%'
+'calendar': 'calendar'
+'Calendar': 'Calendar'
 'Cancel': 'Cancel'
 'Canceled': 'Canceled'
 'Candidates': 'Candidates'
-'Change for the next season': 'Change for the next season'
+'Captcha: %captcha%': 'Captcha: %captcha%'
+'Champs-Elysées kiosk': 'Champs-Elysées kiosk'
+'Champs-Elysées kiosk miniature': 'Champs-Elysées kiosk miniature'
+'Champs-Elysées kiosk sector map': 'Champs-Elysées kiosk sector map'
 'Change': 'Change'
+'Change for the next season': 'Change for the next season'
+'Cities': 'Cities'
 'City': 'City'
+'City list': 'City list'
 'Civility': 'Civility'
 'Class': 'Class'
-'Colette place': 'Colette place'
 'Colette': 'Colette'
+'Colette place': 'Colette place'
+'Colette place access map': 'Colette place access map'
+'Colette place miniature': 'Colette place miniature'
+'Colette place sector map': 'Colette place sector map'
 'Completely offline (neither deezer nor spotify)': 'Completely offline (neither deezer nor spotify)'
 'Confirm password': 'Confirm password'
 'Consult %pseudonym% profile': 'Consult %pseudonym% profile'
 'Consult %title% website': 'Consult %title% website'
-'Contact Libre Air': 'Contact Libre Air'
+'contact': 'contact'
 'Contact': 'Contact'
-'Contribute to %title%': 'Contribute to %title%'
-'Contribution': 'Contribution'
+'Contact Libre Air': 'Contact Libre Air'
+'Contribution to costs': 'Contribution to costs'
 'Copyright 2019-2021': 'Copyright 2019-2021'
 'Cortina every 2 to 4 songs': 'Cortina every 2 to 4 songs'
 'Cortina every 3 to 4 songs': 'Cortina every 3 to 4 songs'
+'Country': 'Country'
 'Court city': 'Court city'
 'Created': 'Created'
 'Dance': 'Dance'
+'Dances': 'Dances'
+'Dance floor around the kiosk in the garden between Concorde place and the Petit-Palais.': 'Dance floor around the kiosk in the garden between Concorde place and the Petit-Palais.'
+'Dance floor at the northern end of the Passage des Jacobins in the Saint-Honoré market.': 'Dance floor at the northern end of the Passage des Jacobins in the Saint-Honoré market.'
+'Dance floor either between the fountain and the Saint-Sulpice church or on the Palatine street square to the right of the Saint-Sulpice church front.': 'Dance floor either between the fountain and the Saint-Sulpice church or on the Palatine street square to the right of the Saint-Sulpice church front.'
+'Dance floor in George and Rosy Varenne ballroom.': 'Dance floor in George and Rosy Varenne ballroom.'
+'Dance floor in the mini-garden of the Monde headquarter, pass under the arch then go along to the left, the dance floor is at the balcony of the Austerlitz rail station tracks.': 'Dance floor in the mini-garden of the Monde headquarter, pass under the arch then go along to the left, the dance floor is at the balcony of the Austerlitz rail station tracks.'
+'Dance floor in the lower level or upstairs depending on availability.': 'Dance floor in the lower level or upstairs depending on availability.'
+'Dance floor of the Earth Theater in the basement after the garden.': 'Dance floor of the Earth Theater in the basement after the garden.'
+'Dance floor on the sublevel between the two entrances.': 'Dance floor on the sublevel between the two entrances.'
+'%dance% %id% by %pseudonym%': '%dance% %id% by %pseudonym%'
+'%dance% %id% by %pseudonym% %location% %city%': '%dance% %id% by %pseudonym% %location% %city%'
+'Dance in %city%': 'Dance in %city%'
+'%dance% %location% %city% %slot% on %date% at %time%': '%dance% %location% %city% %slot% on %date% at %time%'
+'%dances% %cities%': '%dances% %cities%'
+'%dances% %cities% sessions': '%dances% %cities% sessions'
+'%dances% %city%': '%dances% %city%'
+'Dance session calendar': 'Dance session calendar'
+'%dances% indoor and outdoor calendar %cities%': '%dances% indoor and outdoor calendar %cities%'
+'%dances% indoor and outdoor calendar %city%': '%dances% indoor and outdoor calendar %city%'
+'%dances% indoor and outdoor calendar %location%': '%dances% indoor and outdoor calendar %location%'
+'%dances% indoor and outdoor session calendar %cities%': '%dances% indoor and outdoor session calendar %cities%'
+'%dances% %location%': '%dances% %location%'
+'%dances% %types% %indoors% calendar %ats% %ins% %pseudonym%': '%dances% %types% %indoors% calendar %ats% %ins% %pseudonym%'
 'Dashboard': 'Dashboard'
+'Date and schedule': 'Date and schedule'
 'Date': 'Date'
+'Delete': 'Delete'
 'Description': 'Description'
-'Dispute': 'Dispute'
-'Do not obey land owner': 'Do not obey land owner'
-'Do not obey police': 'Do not obey police'
 'Docks': 'Docks'
+'Donate': 'Donate'
 'Donate on %title% to help us fund our mission.': 'Donate on %title% to help us fund our mission.'
 'Donate to %pseudonym%': 'Donate to %pseudonym%'
-'Donate': 'Donate'
+'Do not obey land owner': 'Do not obey land owner'
+'Do not obey police': 'Do not obey police'
 'Drawings'' garden': 'Drawings'' garden'
 'Duration of 4h (or superior)': 'Duration of 4h (or superior)'
 'Duration of 6h (or superior)': 'Duration of 6h (or superior)'
 'Duration of 8h (or superior)': 'Duration of 8h (or superior)'
 'Early account reactivation possible with a new backup battery proof': 'Early account reactivation possible with a new backup battery proof'
+'Earth theater': 'Earth theater'
+'Earth theater access map': 'Earth theater access map'
+'Earth theater miniature': 'Earth theater miniature'
+'Earth theater sector map': 'Earth theater sector map'
 'Elementary french communication': 'Elementary french communication'
 'Elementary tango ballroom organisation': 'Elementary tango ballroom organisation'
 'Elementary tango music programming': 'Elementary tango music programming'
 'End': 'End'
 'English': 'English'
 'Evening': 'Evening'
+'faq': 'faq'
 'First time': 'First time'
 'Forbidden gathering': 'Forbidden gathering'
 'Force cancel': 'Force cancel'
 'Forename': 'Forename'
+'France': 'France'
+'Free': 'Free'
 'French': 'French'
+'frequently asked questions': 'frequently asked questions'
 'Frequently asked questions': 'Frequently asked questions'
 'Friday': 'Friday'
-'From %start% to %stop%': 'From %start% to %stop%'
+'from %start% to %stop%': 'from %start% to %stop%'
 'Fund our mission': 'Fund our mission'
-'Garnier opera': 'Garnier opera'
 'Garnier': 'Garnier'
+'Garnier opera': 'Garnier opera'
+'Garnier opera miniature': 'Garnier opera miniature'
+'Google Calendar logo': 'Google Calendar logo'
+'GPS coordinates': 'GPS coordinates'
 'Guest': 'Guest'
 'Hardware': 'Hardware'
 'Hat': 'Hat'
-'Hi %recipient_name%,': 'Hi %recipient_name%,'
 'Hi,': 'Hi,'
+'Hi %recipient_name%,': 'Hi %recipient_name%,'
 'History of Libre Air': 'History of Libre Air'
 'Home': 'Home'
 'Honore': 'St-Honore'
 'Hotspot': 'Hotspot'
-'How you can help': 'How you can help'
 'However, you don''t need to be a professional dancer or organizer to help %title%!': 'However, you don''t need to be a professional dancer or organizer to help %title%!'
+'How you can help': 'How you can help'
 'If it''s an outdoor place we want to document it.': 'If it''s an outdoor place we want to document it.'
 'If you did not receive a verification mail, check your Spam or Junk mail folders': 'If you did not receive a verification mail, check your Spam or Junk mail folders'
+'Igor Stravinsky place access map': 'Igor Stravinsky place access map'
 'Igor Stravinsky place': 'Igor Stravinsky place'
+'Igor Stravinsky place miniature': 'Igor Stravinsky place miniature'
+'Igor Stravinsky place sector map': 'Igor Stravinsky place sector map'
+'Image for %user% %location% deleted': 'Image for %user% %location% deleted'
+'Image for %user% %location% updated': 'Image for %user% %location% updated'
 'Image': 'Image'
 'In case of bug or glitch you may contact us throught the contact form: %title%': 'In case of bug or glitch you may contact us throught the contact form: %title%'
-'In the future, %title% hopes to become a daily visited resource for participants and organizers of outdoor dance sessions.': 'In the future, %title% hopes to become a daily visited resource for participants and organizers of outdoor dance sessions.'
+'Indoor and outdoor dance calendar in %city%': 'Indoor and outdoor dance calendar in %city%'
+'indoor': 'indoor'
+'Indoor': 'Indoor'
+'in Paris': 'in Paris'
+'In Paris': 'In Paris'
+'inside': 'inside'
+'Interiority': 'Interiority'
 'Intermediate french communication': 'Intermediate french communication'
 'Intermediate tango ballroom organisation': 'Intermediate tango ballroom organisation'
 'Intermediate tango music programming': 'Intermediate tango music programming'
-'Invalid %field% field: %value%': 'Invalid %field% field: %value%'
+'In the future, %title% hopes to become a daily visited resource for participants and organizers of outdoor dance sessions.': 'In the future, %title% hopes to become a daily visited resource for participants and organizers of outdoor dance sessions.'
 'Invalid credentials.': 'Invalid credentials'
+'Invalid %field% field: %value%': 'Invalid %field% field: %value%'
+'Jussieu esplanade access map': 'Jussieu esplanade access map'
 'Jussieu esplanade': 'Jussieu esplanade'
+'Jussieu esplanade miniature': 'Jussieu esplanade miniature'
+'Jussieu esplanade sector map': 'Jussieu esplanade sector map'
 'Jussieu': 'Jussieu'
 'Kicked out for music too strong': 'Kicked out for music too strong'
 'Latitude': 'Latitude'
 'Length': 'Length'
 'Libre Air about': 'Libre Air about'
-'Libre Air dispute': 'Libre Air dispute'
+'Libre Air cities access map': 'Libre Air cities access map'
+'Libre Air cities': 'Libre Air cities'
+'Libre Air cities miniature': 'Libre Air cities miniature'
+'Libre Air cities sector map': 'Libre Air cities sector map'
+'Libre Air city list': 'Libre Air city list'
 'Libre Air frequently asked questions': 'Libre Air frequently asked questions'
 'Libre Air is an initiative to have a powerful and shared calendar for outdoor Argentine Tango sessions.': 'Libre Air is an initiative to have a powerful and shared calendar for outdoor Argentine Tango sessions.'
+'Libre Air': 'Libre Air'
+'Libre Air location list %city%': 'Libre Air location list %city%'
 'Libre Air location list': 'Libre Air location list'
+'Libre Air location list %location% %city%': 'Libre Air location list %location% %city%'
 'Libre Air locations': 'Libre Air locations'
+'Libre Air locations sector map': 'Libre Air locations sector map'
+'Libre Air logo': 'Libre Air logo'
 'Libre Air organizer regulation': 'Libre Air organizer regulation'
-'Libre Air organizers list': 'Libre Air organizers list'
 'Libre Air organizers': 'Libre Air organizers'
+'Libre Air organizers list': 'Libre Air organizers list'
+'Libre Air %pseudonym% location list': 'Libre Air %pseudonym% location list'
+'Libre Air session list': 'Libre Air session list'
 'Libre Air terms of service': 'Libre Air terms of service'
 'Libre Air user list': 'Libre Air user list'
 'Libre Air users': 'Libre Air users'
-'Libre Air': 'Libre Air'
 'Light change': 'Light change'
 'Limited to low volume': 'Limited to low volume'
 'Limited to once per season': 'Limited to once per season'
 'Limited to saturday and sunday': 'Limited to saturday and sunday'
 'Limited to week day': 'Limited to week day'
-'Link to %pseudonym%': 'Link to %pseudonym%'
 'Link': 'Link'
+'Link a Google Calendar account': 'Link a Google Calendar account'
+'Link to %pseudonym%': 'Link to %pseudonym%'
+'Lists Libre air users': 'Lists Libre air users'
+'Lists Libre air organizers': 'Lists Libre air organizers'
+'listing': 'listing'
+'Listing': 'Listing'
+'%location% %city% %slot% on %date% at %time%': '%location% %city% %slot% on %date% at %time%'
+'location list': 'location list'
 'Location': 'Location'
+'locations': 'locations'
 'Locations': 'Locations'
-'Lock': 'Lock'
 'Locked': 'Locked'
+'Lock': 'Lock'
 'Login': 'Login'
 'Logout': 'Logout'
 'Longitude': 'Longitude'
-'Louvre palace': 'Louvre palace'
 'Louvre': 'Louvre'
+'Louvre palace access map': 'Louvre palace access map'
+'Louvre palace': 'Louvre palace'
+'Louvre palace miniature': 'Louvre palace miniature'
+'Louvre palace sector map': 'Louvre palace sector map'
+'Lyon rail station access map': 'Lyon rail station access map'
+'Lyon rail station': 'Lyon rail station'
+'Lyon rail station miniature': 'Lyon rail station miniature'
+'Lyon rail station sector map': 'Lyon rail station sector map'
 'Madam': 'Madam'
-'Madeleine place': 'Madeleine place'
 'Madeleine': 'Madeleine'
+'Madeleine place': 'Madeleine place'
+'Madeleine place miniature': 'Madeleine place miniature'
 'Maier': 'Maier'
-'Mail recover': 'Mail recover'
 'Mail': 'Mail'
+'Mail recover': 'Mail recover'
 'Majority of danceable songs': 'Majority of danceable songs'
 'Make a donation to %title%': 'Make a donation to %title%'
-'Managment veto possible': 'Managment veto possible'
 'Managment': 'Managment'
+'Managment veto possible': 'Managment veto possible'
 'Maps': 'Maps'
 'Medium change': 'Medium change'
 'Meeting': 'Meeting'
 'Message': 'Message'
-'Minimap': 'Minimap'
-'Miss': 'Miss'
 'Missing broom (except Opera/Orsay/Tokyo)': 'Missing broom (except Opera/Orsay/Tokyo)'
 'Missing in action': 'Missing in action'
 'Missing talcum powder': 'Missing talcum powder'
+'Miss': 'Miss'
 'Mister': 'Mister'
 'Modify account': 'Modify account'
-'Modify password': 'Modify password'
 'Modify': 'Modify'
+'Modify password': 'Modify password'
 'Monday': 'Monday'
+'Monde garden access map': 'Monde garden access map'
+'Monde garden miniature': 'Monde garden miniature'
 'Monde garden': 'Monde garden'
-'Monde': 'Monde'
+'Monde garden sector map': 'Monde garden sector map'
 'Monthly application %location% already exists': 'Monthly application %location% already exists'
 'More than one hour delay': 'More than one hour delay'
 'Morning': 'Morning'
 'My account': 'My account'
 'Name': 'Name'
 'Navigation': 'Navigation'
-'No more battery': 'No more battery'
+'Newsletter': 'Newsletter'
 'Nobody': 'Nobody'
+'No more battery': 'No more battery'
 'None': 'None'
+'No': 'No'
 'Not Found': 'Not Found'
 'Notice number': 'Notice number'
+'Odeon theater': 'Odeon theater'
+'Odeon theater access map': 'Odeon theater access map'
+'Odeon theater miniature': 'Odeon theater miniature'
+'Odeon theater sector map': 'Odeon theater sector map'
 'Offense': 'Offense'
-'On this page you will find the most recurring questions of Libre Air users.': 'On this page you will find the most recurring questions of Libre Air users.'
 'Only danceable songs': 'Only danceable songs'
+'On this page you will find the most recurring questions of Libre Air users.': 'On this page you will find the most recurring questions of Libre Air users.'
+'organizer': 'organizer'
+'Organizer': 'Organizer'
+'organizer regulation': 'organizer regulation'
 'Organizer regulation': 'Organizer regulation'
+'Organizers': 'Organizers'
 'Organizer''s snippet by dance space': 'Organizer''s snippet by dance space'
-'Organizer': 'Organizer'
 'Organizer: %title%': 'Organizer: %title%'
-'Organizers': 'Organizers'
+'Orleans gallery miniature': 'Orleans gallery miniature'
 'Orleans gallery': 'Orleans gallery'
+'Orleans gallery sector map': 'Orleans gallery sector map'
 'Orleans': 'Orleans'
+'Orsay museum miniature': 'Orsay museum miniature'
 'Orsay museum': 'Orsay museum'
+'Orsay museum sector map': 'Orsay museum sector map'
 'Orsay': 'Orsay'
 'Our community is composed of amazing dancers all around the world who participate, review dance session and help us promote them.': 'Our community is composed of amazing dancers all around the world who participate, review dance session and help us promote them.'
+'our donation page': 'our donation page'
 'Our mission': 'Our mission'
 'Outdoor Argentine Tango organizer list': 'Outdoor Argentine Tango organizer list'
-'Outdoor Argentine Tango session calendar %location%': 'Outdoor Argentine Tango session calendar %location%'
 'Outdoor Argentine Tango session calendar in Paris': 'Outdoor Argentine Tango session calendar in Paris'
+'Outdoor Argentine Tango session calendar %location%': 'Outdoor Argentine Tango session calendar %location%'
 'Outdoor Argentine Tango session the %date%': 'Outdoor Argentine Tango session the %date%'
+'outdoor': 'outdoor'
+'Outdoor': 'Outdoor'
 'Outdoor space reservation system': 'Outdoor space reservation system'
-'Paris': 'Paris'
+'outside': 'outside'
+'Paris access map': 'Paris access map'
 'Parisian region': 'Parisian region'
+'Paris miniature': 'Paris miniature'
+'Paris': 'Paris'
+'Paris sector map': 'Paris sector map'
 'Password': 'Password'
 'Penalization': 'Penalization'
 'Personnal rehearsal at home': 'Personnal rehearsal at home'
 'Personnal rehearsal outdoor': 'Personnal rehearsal outdoor'
 'Phone': 'Phone'
+'Place': 'Place'
 'Playlist': 'Playlist'
 'Preamble': 'Preamble'
 'Preparation': 'Preparation'
 'Prerequisite': 'Prerequisite'
 'Present qualified majority': 'Present qualified majority'
+'Primary': 'Primary'
+'Private class': 'Private class'
 'Profile': 'Profile'
+'Program': 'Program'
+'%pseudonym% calendar': '%pseudonym% calendar'
+'%pseudonym% organizer': '%pseudonym% organizer'
 'Pseudonym': 'Pseudonym'
+'%pseudonym% sector map': '%pseudonym% sector map'
+'Public class': 'Public class'
 'Qualified double majority': 'Qualified double majority'
 'Qualified majority': 'Qualified majority'
 'Questions asked via the contact form will be added to this page as they arise.': 'Questions asked via the contact form will be added to this page as they arise.'
 'Rainfall': 'Rainfall'
 'Rainrisk': 'Rainrisk'
 'Rapsys': 'Rapsys'
+'%rate%€ to the hat': '%rate%€ to the hat'
 'Rate': 'Rate'
 'Realfeel max': 'Realfeel max'
 'Realfeel min': 'Realfeel min'
 'Realfeel': 'Realfeel'
 'Recover': 'Recover'
+'Refresh': 'Refresh'
+'register: mail=%mail% locale=%locale% confirm=%confirm%': 'register: mail=%mail% locale=%locale% confirm=%confirm%'
 'Register': 'Register'
 'Regular': 'Regular'
 'Regulation': 'Regulation'
 'Required cable to connect both': 'Required cable to connect both'
 'Residence': 'Residence'
 'Rope with strings': 'Rope with strings'
+'Saint-Honore market access map': 'Saint-Honore market access map'
+'Saint-Honore market miniature': 'Saint-Honore market miniature'
 'Saint-Honore market': 'Saint-Honore market'
+'Saint-Honore market sector map': 'Saint-Honore market sector map'
+'Saint-Sulpice place access map': 'Saint-Sulpice place access map'
+'Saint-Sulpice place miniature': 'Saint-Sulpice place miniature'
+'Saint-Sulpice place': 'Saint-Sulpice place'
+'Saint-Sulpice place sector map': 'Saint-Sulpice place sector map'
 'Saturday': 'Saturday'
 'Schedule': 'Schedule'
 'Score': 'Score'
 'Service code': 'Service code'
 'Session %id% auto attributed': 'Session %id% auto attributed'
 'Session %id% by %pseudonym%': 'Session %id% by %pseudonym%'
-'Session %id% updated': 'Session %id% updated'
 'Session %id%': 'Session %id%'
+'Session %id% updated': 'Session %id% updated'
 'Session in the past on %date% %location% %slot% not yet supported': 'Session in the past on %date% %location% %slot% not yet supported'
+'session list': 'session list'
 'Session on %date% %location% %slot% not yet supported': 'Session on %date% %location% %slot% not yet supported'
+'sessions': 'sessions'
 'Sessions': 'Sessions'
 'Sexual assault complaint': 'Sexual assault complaint'
 'Short': 'Short'
 'Skill': 'Skill'
 'Slot': 'Slot'
 'Slug': 'Slug'
+'Snippet for %user% %location% updated': 'Snippet for %user% %location% updated'
 'Social network': 'Social network'
 'Start': 'Start'
 'Stop': 'Stop'
 'Subject': 'Subject'
 'Subject: %subject%': 'Subject: %subject%'
 'Subscribe': 'Subscribe'
+'Subscriptions': 'Subscriptions'
 'Sunday': 'Sunday'
 'Surname': 'Surname'
+'Swan island sector map': 'Swan island sector map'
 'Swan island': 'Swan island'
 'Table of contents': 'Table of contents'
 'Talcum powder bottle': 'Talcum powder bottle'
 'Temperature max': 'Temperature max'
 'Temperature min': 'Temperature min'
 'Temperature': 'Temperature'
+'terms of service': 'terms of service'
 'Terms of service': 'Terms of service'
-'Thanks so much for joining %site.title%, the space reservation program.': 'Thanks so much for joining %site.title%, the space reservation program.'
-'Thanks so much for rejoining %site.title%, the space reservation program.': 'Thanks so much for rejoining %site.title%, the space reservation program.'
-'The %title% project started in 2019 when the need for a new dispatch platform for outdoor dance events arose.': 'The %title% project started in 2019 when the need for a new dispatch platform for outdoor dance events arose.'
+'Thanks so much for joining %title.site%, the space reservation program.': 'Thanks so much for joining %title.site%, the space reservation program.'
+'Thanks so much for rejoining %title.site%, the space reservation program.': 'Thanks so much for rejoining %title.site%, the space reservation program.'
+'the afternoon': 'the afternoon'
+'the after': 'the after'
 'The dancer can follow an organizer by email.': 'The dancer can follow an organizer by email.'
+'The %date% around %start% until %stop%': 'The %date% around %start% until %stop%'
 'The development and hosting services are graciously provided by: %title%': 'The development and hosting services are graciously provided by: %title%'
+'the evening': 'the evening'
+'the morning': 'the morning'
 'The organizer will have a convenient way to automatically notify subscribers with minimum effort.': 'The organizer will have a convenient way to automatically notify subscribers with minimum effort.'
+'The %title% project started in 2019 when the need for a new dispatch platform for outdoor dance events arose.': 'The %title% project started in 2019 when the need for a new dispatch platform for outdoor dance events arose.'
 'Third time': 'Third time'
 'Thursday': 'Thursday'
+'Tino-Rossi garden access map': 'Tino-Rossi garden access map'
+'Tino-Rossi garden miniature': 'Tino-Rossi garden miniature'
+'Tino-Rossi garden sector map': 'Tino-Rossi garden sector map'
 'Tino-Rossi garden': 'Tino-Rossi garden'
+'%title%, an information site for outdoor dancers and organizers.': '%title%, an information site for outdoor dancers and organizers.'
+'%title%''s mission is simple: provide dancers with the information they need to easily find organizers dance sessions.': '%title%''s mission is simple: provide dancers with the information they need to easily find organizers dance sessions.'
 'Title': 'Title'
 'To change your password login with your mail and any password then follow the procedure': 'To change your password login with your mail and any password then follow the procedure'
 'To change your password relogin with your mail %mail% and any password then follow the procedure': 'To change your password relogin with your mail %mail% and any password then follow the procedure'
 'To create your account you must follow this link: %confirm_url%': 'To create your account you can follow this link: %confirm_url%'
 'To create your account you must follow this link:': 'To create your account you can follow this link:'
-'To recover your account follow this link: %recover_url%': 'To recover your account follow this link: %recover_url%'
-'To recover your account follow this link:': 'To recover your account follow this link:'
-'To the hat, ideally %rate% €, to cover the costs: talc, electricity, bicycle, server': 'To the hat, ideally %rate% €, to cover the costs: talc, electricity, bicycle, server'
-'To the hat, to cover the costs: talc, electricity, bicycle, server': 'To the hat, to cover the costs: talc, electricity, bicycle, server'
 'Tokyo palace': 'Tokyo palace'
 'Tokyo': 'Tokyo'
+'To recover your account follow this link: %recover_url%': 'To recover your account follow this link: %recover_url%'
+'To recover your account follow this link:': 'To recover your account follow this link:'
+'To the hat, ideally %rate% €, to cover: talc, electricity, bicycle, website, ...': 'To the hat, ideally %rate% €, to cover: talc, electricity, bicycle, website, ...'
+'To the hat, to cover: talc, electricity, bicycle, website, ...': 'To the hat, to cover: talc, electricity, bicycle, website, ...'
+'To the hat': 'To the hat'
 'Traffic at prohibited time': 'Traffic at prohibited time'
 'Trocadero esplanade': 'Trocadero esplanade'
 'Trocadero': 'Trocadero'
 'Unable to find account %mail%': 'Unable to find account %mail%'
 'Unable to find account': 'Unable to find account'
 'Unable to find user: %id%': 'Unable to find user: %id%'
+'Unlink': 'Unlink'
 'Until 00h00': 'Until 00h00'
 'Until 00h30 maximum': 'Until 00h30 maximum'
 'Until 00h30 minimum': 'Until 00h30 minimum'
 'Until 01h30 minimum on friday and saturday': 'Until 01h30 minimum on friday and saturday'
 'Until 19h00': 'Until 19h00'
 'Updated': 'Updated'
-'Use this form to recover your account': 'Use this form to recover your account'
-'User': 'User'
+'user list': 'user list'
+'users': 'users'
 'Users': 'Users'
+'User': 'User'
+'Use this form to recover your account': 'Use this form to recover your account'
+'Varenne ballroom access map': 'Varenne ballroom access map'
+'Varenne ballroom miniature': 'Varenne ballroom miniature'
+'Varenne ballroom sector map': 'Varenne ballroom sector map'
+'Varenne ballroom': 'Varenne ballroom'
+'Vendome place miniature': 'Vendome place miniature'
 'Vendome place': 'Vendome place'
 'Villette': 'Villette'
+'Vivian Maier garden access map': 'Vivian Maier garden access map'
+'Vivian Maier garden miniature': 'Vivian Maier garden miniature'
+'Vivian Maier garden sector map': 'Vivian Maier garden sector map'
 'Vivian Maier garden': 'Vivian Maier garden'
 'Weather': 'Weather'
 'Website of %title': 'Website of %title%'
 'Website': 'Website'
 'Wednesday': 'Wednesday'
-'Welcome %recipient_name% to %site.title%': 'Welcome %recipient_name% to %site.title%'
-'Welcome back %recipient_name% to %site.title%': 'Welcome back %recipient_name% to %site.title%'
+'Welcome back %recipient_name% to %title.site%': 'Welcome back %recipient_name% to %title.site%'
+'Welcome %recipient_name% to %title.site%': 'Welcome %recipient_name% to %title.site%'
+'welcome to %title.site%': 'welcome to %title.site%'
 'What is it ?': 'What is it ?'
 'When the feature will be available on Libre Air.': 'When the feature will be available on Libre Air.'
 'Who are we ?': 'Who are we ?'
 'Who can create an account ?': 'Who can create an account ?'
 'Who': 'Who'
+'Yes': 'Yes'
 'Your abstract': 'Your abstract'
 'Your account has been activated': 'Your account has been activated'
 'Your account has been created': 'Your account has been created'
 'Your address': 'Your address'
 'Your agent number': 'Your agent number'
 'Your begin': 'Your begin'
+'Your calendar': 'Your calendar'
 'Your city': 'Your city'
 'Your civility': 'Your civility'
 'Your class': 'Your class'
 'Your contact': 'Your contact'
+'Your country': 'Your country'
 'Your court city': 'Your court city'
 'Your dance': 'Your dance'
 'Your date': 'Your date'
 'Your hat': 'Your hat'
 'Your hotspot': 'Your hotspot'
 'Your image': 'Your image'
+'Your indoor': 'Your indoor'
 'Your latitude': 'Your latitude'
 'Your length': 'Your length'
 'Your link': 'Your link'
 'Your slot': 'Your slot'
 'Your slug': 'Your slug'
 'Your surname': 'Your surname'
+'Your subscription': 'Your subscription'
 'Your title': 'Your title'
 'Your user': 'Your user'
 'Your verification mail has been sent, to activate your account you must follow the confirmation link inside': 'Your verification mail has been sent, to activate your account you must follow the confirmation link inside'
 'Your website': 'Your website'
 'Your zipcode': 'Your zipcode'
 'Zipcode': 'Zipcode'
-'about': 'about'
-'at Bastille place': 'in Bastille place'
-'at Colette place': 'in Colette place'
-'at Docks': 'on Docks'
-'at Drawings'' garden': 'in Drawings'' garden'
-'at Garnier opera': 'at Garnier opera'
-'at Garnier': 'at Garnier'
-'at Honore': 'at St-Honore'
-'at Igor Stravinsky place': 'on Igor Stravinsky place'
-'at Jussieu esplanade': 'on Jussieu esplanade'
-'at Louvre palace': 'in Louvre palace'
-'at Madeleine place': 'in Madeleine place'
-'at Monde garden': 'in Monde garden'
-'at Monde': 'in Monde'
-'at Orleans gallery': 'in Orleans gallery'
-'at Orsay museum': 'in front of Orsay museum'
-'at Saint-Honore market': 'at Saint-Honore market'
-'at Swan island': 'on Swan island'
-'at Tino-Rossi garden': 'at Tino-Rossi garden'
-'at Tokyo palace': 'in Tokyo palace'
-'at Trocadero esplanade': 'at Trocadero esplanade'
-'at Trocadero': 'at Trocadero'
-'at Vendome place': 'in Vendome place'
-'at Vivian Maier garden': 'in Vivian Maier garden'
-'by %pseudonym%': 'by %pseudonym%'
-'calendar': 'calendar'
-'contact': 'contact'
-'dispute': 'dispute'
-'faq': 'faq'
-'frequently asked questions': 'frequently asked questions'
-'listing': 'listing'
-'location list': 'location list'
-'locations': 'locations'
-'organizer regulation': 'organizer regulation'
-'organizer': 'organizer'
-'our donation page': 'our donation page'
-'outdoor': 'outdoor'
-'register: mail=%mail% locale=%locale% confirm=%confirm%': 'register: mail=%mail% locale=%locale% confirm=%confirm%'
-'terms of service': 'terms of service'
-'the after': 'the after'
-'the afternoon': 'the afternoon'
-'the evening': 'the evening'
-'the morning': 'the morning'
-'user list': 'user list'
-'users': 'users'
-'welcome to %site.title%': 'welcome to %site.title%'
index 4c08268f98810ecc155ffcf3c2d40ddfca07b00f..a82368f09897d2ad5e309084c8388cdfe956232d 100644 (file)
@@ -1,20 +1,19 @@
-'%pseudonym% outdoor Argentine Tango session calendar': 'Calendrier des sessions de Tango Argentin en plein air de %pseudonym%'
-'%title%''s mission is simple: provide dancers with the information they need to easily find organizers dance sessions.': 'L''activité d''%title% est simple : fournir aux danseurs les informations dont ils ont besoin pour trouver facilement les organisateurs de séances de danse.'
-'%title%, an information site for outdoor dancers and organizers.': '%title%, un site d''information pour danseurs et organisateurs extérieurs.'
 ':': ' :'
-'About Libre Air': 'À propos d''Air Libre'
+'about': 'à propos'
 'About': 'À propos'
+'About Libre Air': 'À propos d''Air Libre'
 'Absence of cancellation reporting': 'Absence de déclaration d''annulation'
 'Absence the first hour in cancellation case': 'Absence la première heure en cas d''annulation'
 'Abstract': 'Résumé'
 'Access denied': 'Accès refusé'
+'Access map': 'Plan d''accès'
 'Account %mail% already exists': 'Compte %mail% déjà existant'
 'Account %mail% created but unable to contact': 'Compte %mail% créé mais impossible à contacter'
 'Account %mail% do not exists': 'Compte %mail% n''existe pas'
-'Account %mail% or with slug %slug% already exists': 'Compte %mail% ou avec limace %slug% déjà existant'
 'Account %mail% password updated': 'Mot de passe du compte %mail% mis à jour'
 'Account %mail% updated but unable to contact': 'Compte %mail% mis à jour mais impossible à contacter'
 'Account %mail% updated': 'Compte %mail% mis à jour'
+'Activity': 'Activité'
 'Add': 'Ajouter'
 'Address': 'Adresse'
 'Admin': 'Administrateur'
 'All slot canceled and account disabled from one month up to permanent suspension': 'Annulation de tous les créneaux et compte désactivé d''un mois à suspension définitive'
 'All slot canceled and account disabled from two month up to permanent suspension': 'Annulation de tous les créneaux et compte désactivé de deux mois à suspension définitive'
 'All slot canceled and account disabled from two weeks to one month': 'Annulation de tous les créneaux et compte désactivé de deux semaines à un mois'
+' and ': ' et '
 'Anyone can create an account on Air Libre, dancer or organizer.': 'Tout le monde peut créer un compte sur Air Libre, danseur ou organisateur.'
 'Anyone': 'N''importe qui'
 'Anyplace': 'N''importe où'
-'Application %id% updated': 'Réservation %id% mise à jour'
-'Application %id%': 'Réservation %id%'
 'Application add': 'Ajout de réservation'
 'Application already exists': 'Réservation déjà existante'
+'Application %id%': 'Réservation %id%'
+'Application %id% updated': 'Réservation %id% mise à jour'
 'Application on %date% %location% %slot% already exists': 'Réservation le %date% %location% %slot% déjà existante'
 'Application on %date% %location% %slot% created': 'Réservation le %date% %location% %slot% créée'
 'Application request the %date% for %location% on the slot %slot% saved': 'Réservation le %date% pour %location% sur le créneau %slot% enregistrée'
 'Application': 'Réservation'
+'Argentine Tango at Auber metro station': 'Tango Argentin à la station de métro Auber'
 'Argentine Tango at Bastille place': 'Tango Argentin sur la place de la Bastille'
-'Argentine Tango at Colette place': 'Tango Argentin à la place Colette'
+'Argentine Tango at Colette place': 'Tango Argentin sur la place Colette'
 'Argentine Tango at Drawings'' garden': 'Tango Argentin au jardin des Dessins'
 'Argentine Tango at Garnier opera': 'Tango Argentin devant l''opéra Garnier'
 'Argentine Tango at Igor Stravinsky place': 'Tango Argentin à place Igor Stravinsky'
 'Argentine Tango at Jussieu esplanade': 'Tango Argentin à l''esplanade de Jussieu'
 'Argentine Tango at Louvre palace': 'Tango Argentin au palais du Louvre'
-'Argentine Tango at Madeleine place': 'Tango Argentin à la place de la Madeleine'
+'Argentine Tango at Lyon rail station': 'Tango Argentin à la gare de Lyon'
+'Argentine Tango at Madeleine place': 'Tango Argentin sur la place de la Madeleine'
 'Argentine Tango at Monde garden': 'Tango Argentin au jardin du Monde'
 'Argentine Tango at Orleans gallery': 'Tango Argentin à la galerie d''Orleans'
 'Argentine Tango at Orsay museum': 'Tango Argentin devant le musée d''Orsay'
 'Argentine Tango at Saint-Honore market': 'Tango Argentin au marché Saint-Honoré'
+'Argentine Tango at Saint-Sulpice place': 'Tango Argentin sur la place Saint-Sulpice'
 'Argentine Tango at Swan island': 'Tango Argentin sur l''Île-aux-Cygnes'
 'Argentine Tango at Tino-Rossi garden': 'Tango Argentin au jardin Tino-Rossi'
 'Argentine Tango at Tokyo palace': 'Tango Argentin au palais de Tokyo'
 'Argentine Tango at Trocadero esplanade': 'Tango Argentin sur l''esplanade du Trocadero'
 'Argentine Tango at Vendome place': 'Tango Argentin sur la place Vendôme'
 'Argentine Tango at Vivian Maier garden': 'Tango Argentin au jardin Vivian Maier'
+'Argentine Tango ball and class': 'Bal et cours de Tango Argentin'
+'Argentine Tango ball and concert': 'Bal et concert de Tango Argentin'
+'Argentine Tango ball': 'Bal de Tango Argentin'
 'Argentine Tango in Paris': 'Tango Argentin à Paris'
 'Argentine Tango organizers': 'Organisateurs de Tango Argentin'
+'Argentine Tango private class': 'Cours privé de Tango Argentin'
+'Argentine Tango public class': 'Cours collectif de Tango Argentin'
 'Argentine Tango': 'Tango Argentin'
+'around Auber metro station': 'autour de la station de métro Auber'
+'around Bastille place': 'autour de la place de la Bastille'
+'around Champs-Elysées kiosk': 'autour du kiosque des Champs-Elysées'
+'around Colette place': 'autour de la place Colette'
+'around Docks': 'autour des Quais'
+'around Drawings'' garden': 'autour du jardin des Dessins'
+'around Earth theater': 'autour du théâtre de la Terre'
+'around Garnier': 'autour de Garnier'
+'around Garnier opera': 'autour de l''Opéra Garnier'
+'around Honore': 'autour de St-Honoré'
+'around Igor Stravinsky place': 'autour de la place Igor Stravinsky'
+'around Jussieu esplanade': 'autour de l''esplanade de Jussieu'
+'around Louvre palace': 'autour du palais du Louvre'
+'around Lyon rail station': 'autour de la gare de Lyon'
+'around Madeleine place': 'autour de la place de la Madeleine'
+'around Monde': 'autour du Monde'
+'around Monde garden': 'autour du jardin du Monde'
+'around Odeon theater': 'autour du théâtre de l''Odéon'
+'around Orleans gallery': 'autour de la galerie d''Orleans'
+'around Orsay museum': 'devant le musée d''Orsay'
+'around Saint-Honore market': 'autour du Marché Saint-Honoré'
+'around Saint-Sulpice place': 'autour de la place Saint-Sulpice'
+'Around %start% until %stop%': 'Vers %start% jusqu''à %stop%'
+'around Swan island': 'autour de l''Île-aux-Cygnes'
+'around Tino-Rossi garden': 'autour du jardin Tino-Rossi'
+'around Tokyo palace': 'autour du palais de Tokyo'
+'around Trocadero': 'autour de Trocadéro'
+'around Trocadero esplanade': 'autour de l''Esplanade du Trocadéro'
+'around Vendome place': 'autour de la place Vendôme'
+'around Vivian Maier garden': 'autour du jardin Vivian Maier'
 'Assit at location an organizer': 'Assister sur l''emplacement un organisateur'
 'Assited or organized once at location': 'Assister ou organiser une fois sur l''emplacement'
+'at Auber metro station': 'à la station de métro Auber'
+'at Bastille place': 'sur la place de la Bastille'
+'at Champs-Elysées kiosk': 'au kiosque des Champs-Elysées'
+'at Colette place': 'sur la place Colette'
+'at Drawings'' garden': 'au jardin des Dessins'
+'at Earth theater': 'au théâtre de la Terre'
+'at Garnier opera': 'à l''Opéra Garnier'
+'at Igor Stravinsky place': 'à la place Igor Stravinsky'
+'at Jussieu esplanade': 'à l''esplanade de Jussieu'
+'at Louvre palace': 'au palais du Louvre'
+'at Lyon rail station': 'à la gare de Lyon'
+'at Madeleine place': 'sur la place de la Madeleine'
+'at Monde garden': 'au jardin du Monde'
+'at Odeon theater': 'au théâtre de l''Odéon'
+'at Orleans gallery': 'à la galerie d''Orleans'
+'at Orsay museum': 'devant le musée d''Orsay'
+'at Saint-Honore market': 'au Marché Saint-Honoré'
+'at Saint-Sulpice place': 'sur la place Saint-Sulpice'
+'at Swan island': 'sur l''Île-aux-Cygnes'
 'Attended once since start until end': 'Être présent une fois du début à la fin'
+'at Tino-Rossi garden': 'au jardin Tino-Rossi'
+'at Tokyo palace': 'au palais de Tokyo'
 'Attribute': 'Attribuer'
 'Attributed to': 'Attribué à'
+'at Trocadero esplanade': 'à l''Esplanade du Trocadéro'
+'at Varenne ballroom': 'à la salle Varenne'
+'at Vendome place': 'sur la place Vendôme'
+'at Vivian Maier garden': 'au jardin Vivian Maier'
+'Auber metro station access map': 'Carte d''accès de la station de métro Auber'
+'Auber metro station miniature': 'Miniature de la station de métro Auber'
+'Auber metro station sector map': 'Carte du secteur de la station de métro Auber'
+'Auber metro station': 'Station de métro Auber'
 'Authentication request could not be processed due to a system problem.': 'Authentification impossible suite à un problème système'
 'Auto attribute': 'Auto attribuer'
 'Autonomous enclosure 200W RMS (Ibiza Sound Port 8 or superior)': 'Enceinte autonome 200W RMS (Ibiza Sound Port 8 ou supérieure)'
 'Autonomous enclosure 250W RMS (Ibiza Sound Port 10 or superior)': 'Enceinte autonome 250W RMS (Ibiza Sound Port 10 ou supérieure)'
 'Autonomous music player (with Bluetooth, Jack, MiniJack or RCA connectivity)': 'Lecteur de musique autonome (avec connectivité Bluetooth, Jack, MiniJack ou RCA)'
 'Backup battery': 'Batterie de secours'
+'Ball and class': 'Bal et cours'
+'Ball and concert': 'Bal et concert'
+'Ball': 'Bal'
+'Bastille place access map': 'Carte d''accès à la place de la Bastille'
+'Bastille place miniature': 'Miniature la place de la Bastille'
 'Bastille place': 'Place de la Bastille'
+'Bastille place sector map': 'Carte du secteur de la place de la Bastille'
 'Begin': 'Début'
 'Broom and/or squeegee depending on location': 'Balais et/ou raclette selon l''emplacement'
+'by %pseudonym%': 'par %pseudonym%'
+'calendar': 'calendrier'
+'Calendar': 'Calendrier'
 'Cancel': 'Annuler'
 'Canceled': 'Annulation'
 'Candidates': 'Candidats'
+'Captcha: %captcha%': 'Captcha : %captcha%'
+'Champs-Elysées kiosk': 'Kiosque des Champs-Elysées'
+'Champs-Elysées kiosk miniature': 'au kiosque des Champs-Elysées'
+'Champs-Elysées kiosk sector map': 'Carte du secteur du kiosque des Champs-Elysées'
 'Change for the next season': 'Modification pour la saison suivante'
 'Change': 'Modification'
+'Cities': 'Villes'
+'City list': 'Liste des villes'
 'City': 'Ville'
 'Civility': 'Civilité'
 'Class': 'Cours'
-'Colette place': 'Place Colette'
 'Colette': 'Colette'
+'Colette place access map': 'Carte d''accès de la place Colette'
+'Colette place miniature': 'Miniature de la place Colette'
+'Colette place': 'Place Colette'
+'Colette place sector map': 'Carte du secteur de la place Colette'
 'Completely offline (neither deezer nor spotify)': 'Entière hors ligne (ni deezer ni spotify)'
 'Confirm password': 'Confirmation'
 'Consult %pseudonym% profile': 'Consulter le profil de %pseudonym%'
 'Consult %title% website': 'Consulter le site de %title%'
-'Contact Libre Air': 'Contacter Air Libre'
+'contact': 'contacter'
 'Contact': 'Contacter'
-'Contribute to %title%': 'Contribuer à %title%'
-'Contribution': 'Contribution'
+'Contact Libre Air': 'Contacter Air Libre'
+'Contribution to costs': 'Contribution aux frais'
 'Copyright 2019-2021': 'Droit d''auteur 2019-2021'
 'Cortina every 2 to 4 songs': 'Cortina touts les 2 à 4 morceaux'
 'Cortina every 3 to 4 songs': 'Cortina touts les 3 à 4 morceaux'
+'Country': 'Pays'
 'Court city': 'Ville du tribunal'
 'Created': 'Création'
 'Dance': 'Danse'
+'Dances': 'Danses'
+'Dance floor around the kiosk in the garden between Concorde place and the Petit-Palais.': 'Piste de danse autour du kiosque dans le jardin entre place Concorde et le Petit-Palais.'
+'Dance floor at the northern end of the Passage des Jacobins in the Saint-Honoré market.': 'Piste de danse à l''extrémité Nord du passage des jacobins dans le marché Saint-Honoré.'
+'Dance floor either between the fountain and the Saint-Sulpice church or on the Palatine street square to the right of the Saint-Sulpice church front.': 'Piste de danse soit entre la fontaine et l''église Saint-Sulpice soit sur la place de la rue Palatine à droite de la façade de l''église Saint-Sulpice.'
+'Dance floor in George and Rosy Varenne ballroom.': 'Piste de danse dans la salle de danse chez George et Rosy à Varenne.'
+'Dance floor in the mini-garden of the Monde headquarter, pass under the arch then go along to the left, the dance floor is at the balcony of the Austerlitz rail station tracks.': 'Piste de danse dans le mini-jardin du siège du Monde, passer sous l''arche puis longer à gauche, la piste est au balcon des voies de la gare d''Austerlitz.'
+'Dance floor in the lower level or upstairs depending on availability.': 'Piste de danse au niveau inférieur ou à l''étage selon disponibilité.'
+'Dance floor of the Earth Theater in the basement after the garden.': 'Piste de danse du théâtre de la Terre au sous-sol après le jardin.'
+'Dance floor on the sublevel between the two entrances.': 'Piste de danse au sous-sol entre les deux entrées.'
+'%dance% %id% by %pseudonym%': '%dance% %id% par %pseudonym%'
+'%dance% %id% by %pseudonym% %location% %city%': '%dance% %id% par %pseudonym% %location% %city%'
+'Dance in %city%': 'Danse à %city%'
+'%dance% %location% %city% %slot% on %date% at %time%': '%dance% %location% %city% %slot% le %date% à %time%'
+'%dances% %cities%': '%dances% %cities%'
+'%dances% %cities% sessions': 'Séances de %dances% %cities%'
+'%dances% %city%': '%dances% %city%'
+'Dance session calendar': 'Calendrier des séances de danse'
+'%dances% indoor and outdoor calendar %cities%': 'Calendrier de %dances% en plein air et en salle %cities%'
+'%dances% indoor and outdoor calendar %city%': 'Calendrier de %dances% en plein air et en salle %city%'
+'%dances% indoor and outdoor calendar %location%': 'Calendrier de %dances% en plein air et en salle %location%'
+'%dances% indoor and outdoor session calendar %cities%': 'Calendrier des séances de %dances% en plein air et en salle %cities%'
+'%dances% %location%': '%dances% %location%'
+'%dances% %types% %indoors% calendar %ats% %ins% %pseudonym%': 'Calendrier de %types% de %dances% %indoors% %ats% %ins% %pseudonym%'
 'Dashboard': 'Tableau de bord'
+'Date and schedule': 'Date et horaire'
 'Date': 'Date'
+'Delete': 'Supprimer'
 'Description': 'Description'
-'Dispute': 'Contestation'
-'Do not obey land owner': 'Ne pas respecter les consignes du propriétaire'
-'Do not obey police': 'Ne pas respecter les consignes de police'
 'Docks': 'Quais'
+'Donate': 'Contribuer'
 'Donate on %title% to help us fund our mission.': 'Faites un don sur %title% pour nous aider à financer notre activité.'
 'Donate to %pseudonym%': 'Faire un don à %pseudonym%'
-'Donate': 'Contribuer'
+'Do not obey land owner': 'Ne pas respecter les consignes du propriétaire'
+'Do not obey police': 'Ne pas respecter les consignes de police'
 'Drawings'' garden': 'Jardin des dessins'
 'Duration of 4h (or superior)': 'Durée de 4h (ou supérieure)'
 'Duration of 6h (or superior)': 'Durée de 6h (ou supérieure)'
 'Duration of 8h (or superior)': 'Durée de 8h (ou supérieure)'
 'Early account reactivation possible with a new backup battery proof': 'Réactivation anticipée du compte possibl sur présentation d''une nouvelle batterie de secours'
+'Earth theater': 'Théâtre de la Terre'
+'Earth theater access map': 'Carte d''accès du théâtre de la Terre'
+'Earth theater miniature': 'Miniature du théâtre de la Terre'
+'Earth theater sector map': 'Plan de secteur du théâtre de la Terre'
 'Elementary french communication': 'Communication en français élémentaire'
 'Elementary tango ballroom organisation': 'Organisation de bal élémentaire'
 'Elementary tango music programming': 'Programmation musicale de tango élémentaire'
 'End': 'Fin'
 'English': 'Anglais'
 'Evening': 'Soirée'
+'faq': 'faq'
 'First time': 'Première fois'
 'Forbidden gathering': 'Rassemblement interdit'
 'Force cancel': 'Annulation forcée'
 'Forename': 'Prénom'
+'France': 'France'
+'Free': 'Gratuit'
 'French': 'Français'
+'frequently asked questions': 'foire aux questions'
 'Frequently asked questions': 'Foire aux questions'
 'Friday': 'Vendredi'
-'From %start% to %stop%': 'A partir de %start% à %stop%'
+'from %start% to %stop%': 'de %start% à %stop%'
 'Fund our mission': 'financer notre activité'
-'Garnier opera': 'Opéra Garnier'
 'Garnier': 'Garnier'
+'Garnier opera miniature': 'Miniature de l''Opéra Garnier'
+'Garnier opera': 'Opéra Garnier'
+'Google Calendar logo': 'Logo Google Calendrier'
+'GPS coordinates': 'Coordonnées GPS'
 'Guest': 'Invité'
 'Hardware': 'Matériel'
 'Hat': 'Chapeau'
 'Home': 'Accueil'
 'Honore': 'St-Honoré'
 'Hotspot': 'Hotspot'
-'How you can help': 'Comment pouvez-vous aider'
 'However, you don''t need to be a professional dancer or organizer to help %title%!': 'Vous n''avez cependant pas besoin d''être un danseur ou un organisateur professionnel pour aider %title% !'
+'How you can help': 'Comment pouvez-vous aider'
 'If it''s an outdoor place we want to document it.': 'S''il s''agit d''un endroit extérieur, nous voulons le répertorier.'
 'If you did not receive a verification mail, check your Spam or Junk mail folders': 'Si vous n''avez pas reçu le courriel de vérification, regardez les dossiers Spam ou Indésirable dans votre messagerie'
+'Igor Stravinsky place access map': 'Carte d''accès à la place Igor Stravinsky'
+'Igor Stravinsky place miniature': 'Miniature de la place Igor Stravinsky'
 'Igor Stravinsky place': 'Place Igor Stravinsky'
+'Igor Stravinsky place sector map': 'Carte du secteur de la place Igor Stravinsky'
+'Image for %user% %location% deleted': 'Image pour %user% %location% supprimée'
+'Image for %user% %location% updated': 'Image pour %user% %location% mise à jour'
 'Image': 'Image'
 'In case of bug or glitch you may contact us throught the contact form: %title%': 'En cas de bug ou de problème, vous pouvez nous contacter via le formulaire de contact : %title%'
-'In the future, %title% hopes to become a daily visited resource for participants and organizers of outdoor dance sessions.': 'À l''avenir, %title% espère devenir une ressource visitée quotidiennement par les participants et organisateurs de séances de danse en extérieur.'
+'Indoor and outdoor dance calendar in %city%': 'Calendrier de danse en plein air et en salle à %city%'
+'indoor': 'en salle'
+'Indoor': 'En salle'
+'in Paris': 'à Paris'
+'In Paris': 'À Paris'
+'inside': 'à l''intérieur'
+'Interiority': 'Intériorité'
 'Intermediate french communication': 'Communication en français intermédiaire'
 'Intermediate tango ballroom organisation': 'Organisation de bal intermédiaire'
 'Intermediate tango music programming': 'Programmation musicale de tango intermédiaire'
-'Invalid %field% field: %value%': 'Champ %field% invalide: %value%'
+'In the future, %title% hopes to become a daily visited resource for participants and organizers of outdoor dance sessions.': 'À l''avenir, %title% espère devenir une ressource visitée quotidiennement par les participants et organisateurs de séances de danse en plein air.'
 'Invalid credentials.': 'Identifiants invalides'
+'Invalid %field% field: %value%': 'Champ %field% invalide: %value%'
+'Jussieu esplanade access map': 'Carte d''accès à l''esplanade de Jussieu'
 'Jussieu esplanade': 'Esplanade de Jussieu'
+'Jussieu esplanade miniature': 'Miniature de l''esplanade de Jussieu'
+'Jussieu esplanade sector map': 'Carte du secteur de l''esplanade de Jussieu'
 'Jussieu': 'Jussieu'
 'Kicked out for music too strong': 'Expulsé pour musique trop forte'
 'Latitude': 'Latitude'
 'Length': 'Durée'
 'Libre Air about': 'À propos d''Air Libre'
-'Libre Air dispute': 'Contestation Air Libre'
+'Libre Air': 'Air Libre'
+'Libre Air cities access map': 'Carte d''accès des villes d''Air Libre'
+'Libre Air cities sector map': 'Carte du secteur des villes d''Air Libre'
+'Libre Air cities': 'Villes d''Air Libre'
+'Libre Air city list': 'Liste des villes d''Air Libre'
 'Libre Air frequently asked questions': 'Foire aux questions d''Air Libre'
 'Libre Air is an initiative to have a powerful and shared calendar for outdoor Argentine Tango sessions.': 'Air Libre est une initiative pour avoir un calendrier puissant et partagé pour les sessions de Tango Argentin en plein air.'
+'Libre Air location list %city%': 'Liste des emplacements d''Air Libre %city%'
 'Libre Air location list': 'Liste des emplacements d''Air Libre'
+'Libre Air location list %location% %city%': 'Liste des emplacements d''Air Libre %location% %city%'
 'Libre Air locations': 'Emplacements d''Air Libre'
+'Libre Air locations sector map': 'Carte du secteur des emplacements d''Air Libre'
+'Libre Air logo': 'Logo d''Air Libre'
 'Libre Air organizer regulation': 'Règlement organisateur d''Air Libre'
 'Libre Air organizers list': 'Liste des organisateurs d''Air Libre'
 'Libre Air organizers': 'Organisateurs d''Air Libre'
+'Libre Air %pseudonym% location list': 'Liste des emplacements d''Air Libre %pseudonym%'
+'Libre Air session list': 'Séances d''Air Libre'
 'Libre Air terms of service': 'Conditions générales d''utilisation d''Air Libre'
 'Libre Air user list': 'Liste des utilisateurs d''Air Libre'
 'Libre Air users': 'Utilisateurs d''Air Libre'
-'Libre Air': 'Air Libre'
 'Light change': 'Changement léger'
 'Limited to low volume': 'Limité au volume sonore doux'
 'Limited to once per season': 'Limité à une fois par saison'
 'Limited to saturday and sunday': 'Limité au samedi et dimanche'
 'Limited to week day': 'Limité aux jours de semaine'
-'Link to %pseudonym%': 'Lien vers %pseudonym%'
 'Link': 'Lien'
+'Link a Google Calendar account': 'Lier un compte Google Calendrier'
+'Link to %pseudonym%': 'Lien vers %pseudonym%'
+'Lists Libre air users': 'Répertorie les utilisateurs d''Air Libre'
+'Lists Libre air organizers': 'Répertorie les organisateurs d''Air Libre'
+'listing': 'liste'
+'Listing': 'Liste'
+'%location% %city% %slot% on %date% at %time%': '%location% %city% %slot% le %date% à %time%'
 'Location': 'Emplacement'
+'location list': 'liste des emplacements'
+'locations': 'emplacements'
 'Locations': 'Emplacements'
-'Lock': 'Vérrouiller'
 'Locked': 'Vérrouillage'
+'Lock': 'Vérrouiller'
 'Login': 'Se connecter'
 'Logout': 'Déconnecter'
 'Longitude': 'Longitude'
-'Louvre palace': 'Palais du Louvre'
 'Louvre': 'Louvre'
+'Louvre palace access map': 'Carte d''accès du palais du Louvre'
+'Louvre palace miniature': 'Miniature du palais du Louvre'
+'Louvre palace': 'Palais du Louvre'
+'Louvre palace sector map': 'Carte du secteur du palais du Louvre'
+'Lyon rail station access map': 'Carte d''accès de la gare de Lyon'
+'Lyon rail station': 'Gare de Lyon'
+'Lyon rail station miniature': 'Miniature de la gare de Lyon'
+'Lyon rail station sector map': 'Carte du secteur de la gare de Lyon'
 'Madam': 'Madame'
-'Madeleine place': 'Place de la Madeleine'
 'Madeleine': 'Madeleine'
+'Madeleine place miniature': 'Miniature de la place de la Madeleine'
+'Madeleine place': 'Place de la Madeleine'
 'Maier': 'Maier'
-'Mail recover': 'Récupération de courriel'
 'Mail': 'Courriel'
+'Mail recover': 'Récupération de courriel'
 'Majority of danceable songs': 'Musique dançable en majorité'
 'Make a donation to %title%': 'Faire un don à %title%'
-'Managment veto possible': 'Possibilité de véto encadrement'
 'Managment': 'Encadrement'
+'Managment veto possible': 'Possibilité de véto encadrement'
 'Maps': 'Cartes'
 'Medium change': 'Changement intermédiaire'
 'Meeting': 'Réunion'
 'Message': 'Message'
-'Minimap': 'Minicarte'
-'Miss': 'Mademoiselle'
 'Missing broom (except Opera/Orsay/Tokyo)': 'Balais absent (sauf Opera/Orsay/Tokyo)'
 'Missing in action': 'Porté disparu'
 'Missing talcum powder': 'Talc manquant'
+'Miss': 'Mademoiselle'
 'Mister': 'Monsieur'
 'Modify account': 'Modifier le compte'
-'Modify password': 'Modifier le mot de passe'
 'Modify': 'Modifier'
+'Modify password': 'Modifier le mot de passe'
 'Monday': 'Lundi'
+'Monde garden access map': 'Carte d''accès du jardin du Monde'
 'Monde garden': 'Jardin du Monde'
-'Monde': 'Monde'
+'Monde garden miniature': 'Miniature du jardin du Monde'
+'Monde garden sector map': 'Carte du secteur du jardin du Monde'
 'Monthly application %location% already exists': 'La réservation mensuelle %location% existe déjà'
 'More than one hour delay': 'Retard de plus d''une heure'
-'Morning': 'Matin'
+'Morning': 'Matinée'
 'Mostly danceable songs': 'Musique dançable principalement'
 'Move': 'Déplacer'
 'Mr.': 'M.'
 'My account': 'Mon compte'
 'Name': 'Nom'
 'Navigation': 'Navigation'
-'No more battery': 'Plus de batterie'
+'Newsletter': 'Liste de diffusion'
 'Nobody': 'Personne'
+'No more battery': 'Plus de batterie'
 'None': 'Aucun'
+'No': 'Non'
 'Not Found': 'Pas Trouvé'
 'Notice number': 'Numéro d''avis'
+'Odeon theater': 'Théâtre de l''Odéon'
+'Odeon theater access map': 'Carte d''accès du théâtre de l''Odéon'
+'Odeon theater miniature': 'Miniature du théâtre de l''Odéon'
+'Odeon theater sector map': 'Plan de secteur du théâtre de l''Odéon'
 'Offense': 'Infraction'
-'On this page you will find the most recurring questions of Libre Air users.': 'Sur cette page, vous trouverez les questions les plus récurrentes des utilisateurs d''Air Libre.'
 'Only danceable songs': 'Musique dançable uniquement'
+'On this page you will find the most recurring questions of Libre Air users.': 'Sur cette page, vous trouverez les questions les plus récurrentes des utilisateurs d''Air Libre.'
+'organizer': 'organisateur'
+'Organizer': 'Organisateur'
+'organizer regulation': 'règlement organisateur'
 'Organizer regulation': 'Règlement organisateur'
+'Organizers': 'Organisateurs'
 'Organizer''s snippet by dance space': 'Extrait organisateur par espace de danse'
-'Organizer': 'Organisateur'
 'Organizer: %title%': 'Organisateur : %title%'
-'Organizers': 'Organisateurs'
 'Orleans gallery': 'Galerie d''Orléans'
+'Orleans gallery miniature': 'Miniature de la galerie d''Orleans'
+'Orleans gallery sector map': 'Plan de secteur de la gallerie d''Orleans'
 'Orleans': 'Orléans'
+'Orsay museum miniature': 'Miniature du musée d''Orsay'
 'Orsay museum': 'Musée d''Orsay'
+'Orsay museum sector map': 'Plan de secteur du musée d''Orsay'
 'Orsay': 'Orsay'
 'Our community is composed of amazing dancers all around the world who participate, review dance session and help us promote them.': 'Notre communauté est composée de danseurs fantastiques du monde entier qui participent, considèrent les sessions de danse et nous aident à les promouvoir.'
+'our donation page': 'notre page de don'
 'Our mission': 'Notre activité'
 'Outdoor Argentine Tango organizer list': 'Liste des organisateurs de Tango Argentin en plein air'
-'Outdoor Argentine Tango session calendar %location%': 'Calendrier des sessions de Tango Argentin en plein air %location%'
 'Outdoor Argentine Tango session calendar in Paris': 'Calendrier des sessions de Tango Argentin en plein air à Paris'
-'Outdoor Argentine Tango session the %date%': 'Session de Tango Argentin extérieure le %date%'
+'Outdoor Argentine Tango session calendar %location%': 'Calendrier des sessions de Tango Argentin en plein air %location%'
+'Outdoor Argentine Tango session the %date%': 'Session de Tango Argentin en plein air le %date%'
+'outdoor': 'en extérieur'
+'Outdoor': 'En extérieur'
 'Outdoor space reservation system': 'Système de réservation d''espace en plein air'
-'Paris': 'Paris'
+'outside': 'en plein air'
+'Paris access map': 'Plan d''accès de Paris'
 'Parisian region': 'Région parisienne'
+'Paris miniature': 'Plan d''accès de Paris'
+'Paris': 'Paris'
+'Paris sector map': 'Plan de secteur de Paris'
 'Password': 'Mot de passe'
 'Penalization': 'Pénalisation'
 'Personnal rehearsal at home': 'Répétition à la maison'
 'Personnal rehearsal outdoor': 'Répétition en extérieur'
 'Phone': 'Téléphone'
+'Place': 'Lieu'
 'Playlist': 'Liste de lecture'
 'Preamble': 'Préambule'
 'Preparation': 'Préparation'
 'Prerequisite': 'Prérequis'
 'Present qualified majority': 'Majorité qualifiée des présents'
+'Primary': 'Primaire'
+'Private class': 'Cours privé'
 'Profile': 'Profil'
+'Program': 'Programme'
+'%pseudonym% calendar': 'Calendrier de %pseudonym%'
+'%pseudonym% organizer': 'Organisateur %pseudonym%'
 'Pseudonym': 'Pseudonyme'
+'%pseudonym% sector map': 'Carte du secteur de %pseudonym%'
+'Public class': 'Cours collectif'
 'Qualified double majority': 'Double majorité qualifiée'
 'Qualified majority': 'Majorité qualifiée'
 'Questions asked via the contact form will be added to this page as they arise.': 'Les questions posées via le formulaire de contact seront ajoutées au fur et à mesure sur cette page.'
 'Rainfall': 'Pluviométrie'
 'Rainrisk': 'Risque de pluie'
 'Rapsys': 'Rapsys'
+'%rate%€ to the hat': '%rate%€ au chapeau'
 'Rate': 'Tarif'
 'Realfeel max': 'Ressenti max'
 'Realfeel min': 'Ressenti min'
 'Realfeel': 'Ressenti'
 'Recover account on %title%': 'Récupération de compte sur %title%'
 'Recover': 'Récupérer'
+'Refresh': 'Rafraîchir'
+'register: mail=%mail% locale=%locale% confirm=%confirm%': 'register: mail=%mail% locale=%locale% confirm=%confirm%'
 'Register': 'S''enregistrer'
 'Regular': 'Régulier'
 'Regulation': 'Règlement'
 'Required cable to connect both': 'Câble requis pour les connecter'
 'Residence': 'Résidence'
 'Rope with strings': 'Corde avec ficelles'
+'Saint-Honore market access map': 'Carte d''accès du marché Saint-Honoré'
 'Saint-Honore market': 'Marché Saint-Honoré'
+'Saint-Honore market miniature': 'Miniature du marché Saint-Honoré'
+'Saint-Honore market sector map': 'Carte du secteur du marché Saint-Honoré'
+'Saint-Sulpice place access map': 'Carte d''accès à la place Saint-Sulpice'
+'Saint-Sulpice place miniature': 'Miniature de la place Saint-Sulpice'
+'Saint-Sulpice place': 'Place Saint-Sulpice'
+'Saint-Sulpice place sector map': 'Carte du secteur de la place Saint-Sulpice'
 'Saturday': 'Samedi'
-'Schedule': 'Programme'
+'Schedule': 'Horaire'
 'Score': 'Score'
 'Second time': 'Seconde fois'
 'Send a message to %pseudonym%': 'Envoyer un message à %pseudonym%'
 'Service code': 'Code service'
 'Session %id% auto attributed': 'Séance %id% attribuée automatiquement'
 'Session %id% by %pseudonym%': 'Séance %id% par %pseudonym%'
+'Session %id%': 'Séance %id%'
 'Session %id% the %date% for %location% on the slot %slot% saved': 'Séance %id% Réservation le %date% pour %location% sur le créneau %slot% enregistrée'
 'Session %id% updated': 'Séance %id% mise à jour'
-'Session %id%': 'Séance %id%'
 'Session in the past on %date% %location% %slot% not yet supported': 'Séance dans le passé le %date% %location% %slot% pas encore supporté'
+'session list': 'liste des séances'
 'Session on %date% %location% %slot% created': 'Séance le %date% %location% %slot% créée'
 'Session on %date% %location% %slot% not yet supported': 'Séance le %date% %location% %slot% pas encore supporté'
 'Session': 'Séance'
+'sessions': 'séances'
 'Sessions': 'Séances'
 'Sexual assault complaint': 'Plainte pour agression sexuelle'
 'Short': 'Titre court'
 'Skill': 'Compétence'
 'Slot': 'Créneau'
 'Slug': 'Limace'
+'Snippet for %user% %location% updated': 'Fragment pour %user% %location% mis à jour'
 'Social network of %pseudonym%': 'Réseau social de %pseudonym%'
 'Social network': 'Réseau social'
 'Start': 'Début'
 'Stop': 'Fin'
-'Subject': 'Sujet'
 'Subject: %subject%': 'Sujet : %subject%'
+'Subject': 'Sujet'
 'Subscribe': 'S''abonner'
+'Subscriptions': 'Souscriptions'
 'Sunday': 'Dimanche'
 'Surname': 'Nom'
 'Swan island': 'Île aux Cygnes'
+'Swan island sector map': 'Carte du secteur de l''île aux Cygnes'
 'Table of contents': 'Table des matières'
 'Talcum powder bottle': 'Flacon de talc en poudre'
 'Teach class the first hour': 'Enseigner la première heure'
 'Temperature max': 'Température max'
 'Temperature min': 'Température min'
 'Temperature': 'Température'
+'terms of service': 'conditions générales d''utilisation'
 'Terms of service': 'Conditions générales d''utilisation'
-'Thanks so much for joining %site.title%, the space reservation program.': 'Merci d''avoir joint %site.title%, le système de réservation d''espace extérieur'
-'Thanks so much for rejoining %site.title%, the space reservation program.': 'Merci d''avoir rejoint %site.title%, le système de réservation d''espace extérieur'
-'The %title% project started in 2019 when the need for a new dispatch platform for outdoor dance events arose.': 'Le projet %title% a commencé en 2019 lorsque la nécessité d''une nouvelle plateforme de répartition pour les événements de danse en extérieur s''est faite sentir.'
+'Thanks so much for joining %title.site%, the space reservation program.': 'Merci d''avoir joint %title.site%, le système de réservation d''espace en plein air'
+'Thanks so much for rejoining %title.site%, the space reservation program.': 'Merci d''avoir rejoint %title.site%, le système de réservation d''espace en plein air'
+'the after': 'en after'
+'the afternoon': 'en après-midi'
 'The dancer can follow an organizer by email.': 'Le danseur pourra suivre un organisateur par courriel.'
+'The %date% around %start% until %stop%': 'Le %date% vers %start% jusqu''à %stop%'
 'The development and hosting services are graciously provided by: %title%': 'Les services de développment et d''hébergement sont gracieusement fournis par : %title%'
+'the evening': 'en soirée'
+'the morning': 'en matinée'
 'The organizer will have a convenient way to automatically notify subscribers with minimum effort.': 'L''organisateur disposera d''un moyen pratique d''avertir automatiquement ses abonnés avec un minimum d''effort.'
+'The %title% project started in 2019 when the need for a new dispatch platform for outdoor dance events arose.': 'Le projet %title% a commencé en 2019 lorsque la nécessité d''une nouvelle plateforme de répartition pour les événements de danse en plein air s''est faite sentir.'
 'Third time': 'Troisième fois'
 'Thursday': 'Jeudi'
+'Tino-Rossi garden access map': 'Carte d''accès du jardin Tino-Rossi'
 'Tino-Rossi garden': 'Jardin Tino-Rossi'
+'Tino-Rossi garden miniature': 'Miniature du jardin Tino-Rossi'
+'Tino-Rossi garden sector map': 'Carte du secteur du jardin Tino-Rossi'
+'%title%, an information site for outdoor dancers and organizers.': '%title%, un site d''information pour danseurs et organisateurs de plein air.'
+'%title%''s mission is simple: provide dancers with the information they need to easily find organizers dance sessions.': 'L''activité d''%title% est simple : fournir aux danseurs les informations dont ils ont besoin pour trouver facilement les organisateurs de séances de danse.'
 'Title': 'Titre'
 'To change your password login with your mail and any password then follow the procedure': 'Pour changer votre mot de passe connectez-vous avec votre courriel et n''importe quel mot de passe puis suivez la procédure'
 'To change your password relogin with your mail %mail% and any password then follow the procedure': 'Pour changer votre mot de passe reconnectez-vous avec votre courriel %mail% et n''importe quel mot de passe puis suivez la procédure'
 'To create your account you must follow this link: %confirm_url%': 'Pour créer votre compte vous devez suivre ce lien : %confirm_url%'
 'To create your account you must follow this link:': 'Pour créer votre compte vous devez suivre ce lien :'
-'To recover your account follow this link: %recover_url%': 'Pour récupérer votre compte suivez ce lien : %recover_url%'
-'To recover your account follow this link:': 'Pour récupérer votre compte suivez ce lien :'
-'To the hat, ideally %rate% €, to cover the costs: talc, electricity, bicycle, server': 'Au chapeau, idéalement %rate% €, pour couvrir les frais : talc, électricité, vélo, server'
-'To the hat, to cover the costs: talc, electricity, bicycle, server': 'Au chapeau, pour couvrir les frais : talc, électricité, vélo, server'
 'Tokyo palace': 'Palais de Tokyo'
 'Tokyo': 'Tokyo'
+'To recover your account follow this link:': 'Pour récupérer votre compte suivez ce lien :'
+'To recover your account follow this link: %recover_url%': 'Pour récupérer votre compte suivez ce lien : %recover_url%'
+'To the hat': 'Au chapeau'
+'To the hat, ideally %rate% €, to cover: talc, electricity, bicycle, website, ...': 'Au chapeau, idéalement %rate% €, pour couvrir : talc, électricité, vélo, site internet, ...'
+'To the hat, to cover: talc, electricity, bicycle, website, ...': 'Au chapeau, pour couvrir : talc, électricité, vélo, site internet, ...'
 'Traffic at prohibited time': 'Circulation à une heure interdite'
 'Trocadero esplanade': 'Esplanade du Trocadéro'
 'Trocadero': 'Trocadéro'
 'Unable to access this page without role %role%!': 'Impossible d''accéder à cette page sans rôle %role% !'
 'Unable to access user: %id%': 'Impossible d''accéder à l''utilisateur : %id%'
 'Unable to access user: %mail%': 'Impossible d''accéder à l''utilisateur : %mail%'
-'Unable to find account %mail%': 'Impossible de trouver le compte %mail%'
 'Unable to find account': 'Impossible de trouver le compte'
+'Unable to find account %mail%': 'Impossible de trouver le compte %mail%'
 'Unable to find user: %id%': 'Impossible de trouver l''utilisateur : %id%'
+'Unlink': 'Délier'
 'Until 00h00': 'Jusqu''à 00h00'
 'Until 00h30 maximum': 'Jusqu''à 00h30 maximum'
 'Until 00h30 minimum': 'Jusqu''à 00h30 minimum'
 'Until 01h30 minimum on friday and saturday': 'Jusqu''à 01h30 minimum vendredi et samedi'
 'Until 19h00': 'Jusqu''à 19h00'
 'Updated': 'Mise à jour'
-'Use this form to recover your account': 'Utilisez ce formulaire pour récupérer votre compte'
-'User': 'Utilisateur'
+'user list': 'liste des utilisateurs'
+'users': 'utilisateurs'
 'Users': 'Utilisateurs'
+'User': 'Utilisateur'
+'Use this form to recover your account': 'Utilisez ce formulaire pour récupérer votre compte'
+'Varenne ballroom access map': 'Carte d''accès à la salle Varenne'
+'Varenne ballroom miniature': 'Miniature la salle Varenne'
+'Varenne ballroom': 'Salle Varenne'
+'Varenne ballroom sector map': 'Carte du secteur de la salle Varenne'
+'Vendome place miniature': 'Miniature de la place Vendôme'
 'Vendome place': 'Place Vendôme'
 'Villette': 'Villette'
+'Vivian Maier garden access map': 'Carte d''accès du jardin Vivian Maier'
 'Vivian Maier garden': 'Jardin Vivian Maier'
+'Vivian Maier garden miniature': 'Miniature du jardin Vivian Maier'
+'Vivian Maier garden sector map': 'Carte secteur du jardin Vivian Maier'
 'Weather': 'Météo'
 'Website of %title%': 'Site de %title%'
 'Website': 'Site internet'
 'Wednesday': 'Mercredi'
-'Welcome %recipient_name% to %site.title%': 'Bienvenue %recipient_name% sur %site.title%'
-'Welcome back %recipient_name% to %site.title%': 'Bienvenue à nouveau %recipient_name% sur %site.title%'
+'Welcome back %recipient_name% to %title.site%': 'Bienvenue à nouveau %recipient_name% sur %title.site%'
+'Welcome %recipient_name% to %title.site%': 'Bienvenue %recipient_name% sur %title.site%'
+'welcome to %title.site%': 'bienvenue sur %title.site%'
 'What is it ?': 'Qu''est-ce que c''est ?'
 'When the feature will be available on Libre Air.': 'Lorsque la fonctionnalité sera disponible sur Air Libre.'
 'Who are we ?': 'Qui sommes nous ?'
 'Who can create an account ?': 'Qui peut créer un compte ?'
 'Who': 'Qui'
+'Yes': 'Oui'
 'Your abstract': 'Votre résumé'
 'Your account has been activated': 'Votre compte a été activé'
 'Your account has been created': 'Votre compte a été créé'
 'Your address': 'Votre adresse'
 'Your agent number': 'Votre numéro d''agent'
 'Your begin': 'Votre début'
+'Your calendar': 'Votre calendrier'
 'Your city': 'Votre ville'
 'Your civility': 'Votre civilité'
 'Your class': 'Votre classe'
 'Your contact': 'Votre contact'
+'Your country': 'Votre pays'
 'Your court city': 'Votre ville du tribunal'
 'Your dance': 'Votre danse'
 'Your date': 'Votre date'
 'Your hat': 'Votre chapeau'
 'Your hotspot': 'Votre hotspot'
 'Your image': 'Votre image'
+'Your indoor': 'Votre à l''intérieur'
 'Your latitude': 'Votre latitude'
 'Your length': 'Votre durée'
 'Your link': 'Votre lien'
 'Your slot': 'Votre créneau'
 'Your slug': 'Votre limace'
 'Your surname': 'Votre nom'
+'Your subscription': 'Votre souscription'
 'Your title': 'Votre titre'
 'Your user': 'Votre utilisateur'
 'Your verification mail has been sent, to activate your account you must follow the confirmation link inside': 'Votre courriel de vérification a été envoyé, pour activer votre compte vous devez suivre le lien de confirmation à l''intérieur'
 'Your zipcode': 'Votre code postal'
 'Zipcode': 'Code postal'
-'about': 'à propos'
-'at Bastille place': 'à la place de la Bastille'
-'at Colette place': 'à la place Colette'
-'at Docks': 'aux Quais'
-'at Drawings'' garden': 'au jardin des Dessins'
-'at Garnier opera': 'à l''Opéra Garnier'
-'at Garnier': 'à Garnier'
-'at Honore': 'à St-Honoré'
-'at Igor Stravinsky place': 'à place Igor Stravinsky'
-'at Jussieu esplanade': 'à l''esplanade de Jussieu'
-'at Louvre palace': 'au palais du Louvre'
-'at Madeleine place': 'à la place de la Madeleine'
-'at Monde garden': 'au jardin du Monde'
-'at Monde': 'au Monde'
-'at Orleans gallery': 'à la galerie d''Orleans'
-'at Orsay museum': 'devant le musée d''Orsay'
-'at Saint-Honore market': 'au Marché Saint-Honoré'
-'at Swan island': 'sur l''Île-aux-Cygnes'
-'at Tino-Rossi garden': 'au jardin Tino-Rossi'
-'at Tokyo palace': 'au palais de Tokyo'
-'at Trocadero esplanade': 'à l''Esplanade du Trocadéro'
-'at Trocadero': 'à Trocadéro'
-'at Vendome place': 'à la place Vendôme'
-'at Vivian Maier garden': 'au jardin Vivian Maier'
-'by %pseudonym%': 'par %pseudonym%'
-'calendar': 'calendrier'
-'contact': 'contacter'
-'dispute': 'contestation'
-'faq': 'faq'
-'frequently asked questions': 'foire aux questions'
-'listing': 'liste'
-'location list': 'liste des emplacements'
-'locations': 'emplacements'
-'organizer regulation': 'règlement organisateur'
-'organizer': 'organisateur'
-'our donation page': 'notre page de don'
-'outdoor': 'extérieur'
-'register: mail=%mail% locale=%locale% confirm=%confirm%': 'register: mail=%mail% locale=%locale% confirm=%confirm%'
-'terms of service': 'conditions générales d''utilisation'
-'the after': 'en after'
-'the afternoon': 'en après-midi'
-'the evening': 'en soirée'
-'the morning': 'en matinée'
-'user list': 'liste des utilisateurs'
-'users': 'utilisateurs'
-'welcome to %site.title%': 'bienvenue sur %site.title%'
index 687e21729e98a3554c4a07cf5fb2fa5766e11b6a..46b6c9b2b0e158b3219f6fe1b8def42945563204 100644 (file)
@@ -14,7 +14,7 @@
                                                                        <ul>
                                                                                {% for session in day.sessions %}
                                                                                        <li class="{{ ['session']|merge(session.class)|join(' ') }}">
-                                                                                               <a href="{{ path('rapsys_air_session', {'id': session.id}) }}" title="{{ session.title }}">{{ session.title }}</a>
+                                                                                               <a href="{{ path('rapsysair_session', {'id': session.id}) }}" title="{{ session.title }}">{{ session.title }}</a>
                                                                                        </li>
                                                                                {% endfor %}
                                                                        </ul>
index b6239deadde46211e234de94d45a42d6e49a265b..120eb94541ada407a665c3a44ec57244b068c331 100644 (file)
@@ -1,7 +1,10 @@
-{% extends '@RapsysAir/body.html.twig' %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
        <section id="form">
-               <h2>{{ title }}</h2>
+               <header>
+                       <h2>{{ title.page }}</h2>
+                       <p>{{ description }}</p>
+               </header>
                {{ form_start(form) }}
                        <div>
                                {% if form.user is defined %}
index e4fb0a2b703b8696dbbe2de0691aaf8a57033008..77819c9036784d208c4c489f0c793d01f0ed2b55 100644 (file)
 <!DOCTYPE html>
 <html{% if locale is defined and locale %} lang="{{ locale }}"{% endif %}>
-       <head{% if facebook is defined and facebook and facebook['prefixes'] is defined and facebook['prefixes'] %} prefix="{{ facebook['prefixes']|map((value, key) => "#{key}: #{value}")|join(' ') }}"{% endif %}>
-               {% block metas %}<meta charset="UTF-8">{% endblock %}
-               <title>{% block title %}Welcome!{% endblock %}</title>
-               {% block stylesheets %}{% endblock %}
+       <head{% if facebook is defined and facebook %} prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb#"{% endif %}>
+               {% block meta %}<meta charset="UTF-8">{% endblock %}
+               <title>{% block title %}{{ [title.page, title.section, title.site]|filter(v => v)|join(' - ') }}{% endblock %}</title>
+               {% block stylesheet %}
+                       <meta name="viewport" content="width=device-width, initial-scale=1" />
+                       {% if description is defined and description %}
+                               <meta name="description" content="{{ description }}" />
+                       {% endif %}
+                       {% if keywords is defined and keywords %}
+                               <meta name="keywords" content="{{ keywords|join(', ') }}" />
+                       {% endif %}
+                       {% if icon is defined and icon %}
+                               <link rel="shortcut icon" type="image/x-icon" href="{{ asset(icon.ico) }}" />
+                               <link rel="icon" type="image/svg+xml" href="{{ asset(icon.svg) }}" />
+                               {% for size, icon in icon.png %}
+                                       {# Apple #}
+                                       {% if size in [120, 152, 167, 180] %}
+                                               {% if size == 180 %}
+                                                       <link rel="apple-touch-icon" href="{{ asset(icon) }}" />
+                                               {% endif %}
+                                               <link rel="apple-touch-icon" sizes="{{ size }}x{{ size }}" href="{{ asset(icon) }}" />
+                                       {# Windows #}
+                                       {% elseif size in [70, 150, 310] %}
+                                               <meta name="msapplication-square{{ size }}x{{ size }}logo" content="{{ asset(icon) }}" />
+                                       {# Others #}
+                                       {% else %}
+                                               <link rel="icon" type="image/png" sizes="{{ size }}x{{ size }}" href="{{ asset(icon) }}" />
+                                       {% endif %}
+                               {% endfor %}
+                       {% endif %}
+                       {# stylesheet '//fonts.googleapis.com/css?family=Irish+Grover' '//fonts.googleapis.com/css?family=La+Belle+Aurore' '@RapsysAirBundle/Resources/public/css/{reset,screen}.css' #}
+                       {% stopwatch 'stylesheet' %}
+                               {% stylesheet '@RapsysAir/css/{reset,droidsans,lemon,notoemoji,screen}.css' %}
+                                       <link rel="stylesheet" type="text/css" href="{{ asset_url }}?20221024100144" />
+                               {% endstylesheet %}
+                       {% endstopwatch %}
+                       {% if canonical is defined and canonical %}
+                               <link rel="canonical" href="{{ canonical }}"{% if locale is defined and locale %} hreflang="{{ locale }}"{% endif %} />
+                       {% endif %}
+                       {% if alternates is defined and alternates %}
+                               {% for lang, alternate in alternates %}
+                                       <link rel="alternate" href="{{ alternate.absolute }}" hreflang="{{ lang }}" />
+                               {% endfor %}
+                       {% endif %}
+                       {% if facebook is defined and facebook %}
+                               {% for property, contents in facebook %}
+                                       {% if contents is iterable %}
+                                               {% for content in contents %}
+                                                       <meta property="{{ property }}" content="{{ content }}" />
+                                               {% endfor %}
+                                       {% else %}
+                                               <meta property="{{ property }}" content="{{ contents }}" />
+                                       {% endif %}
+                               {% endfor %}
+                       {% endif %}
+               {% endblock %}
        </head>
        <body>
-               {% block body %}{% endblock %}
-               {% block javascripts %}{% endblock %}
+               {% block body %}
+                       {% block header %}
+                               <header id="header">
+                                       <div>
+                                               {% if logo is defined and logo %}
+                                                       {% block header_title %}<a id="logo" href="{{ root }}" title="{{ title.site }}"><img src="{{ asset(logo.svg) }}?20221024100144" srcset="{{ asset(logo.png) }}?20221024100144 200w, {{ asset(logo.svg) }}?20221024100144 400w" sizes="(min-width:400px) 400px, 200px" alt="{{ title.site }}" width="100" height="45" /><span>{{ title.site }}</span></a>{% endblock %}
+                                               {% endif %}
+                                               <h1 id="title"><a href="{{ canonical }}">{{ title.page }}</a></h1>
+                                       </div>
+                                       {% block header_nav %}
+                                               <nav id="nav">
+                                                       {#<h2>{% trans %}Navigation{% endtrans %}</h2>#}
+                                                       <a href="{{ path('rapsysair') }}" rel="home">{% trans %}Home{% endtrans %}</a>
+                                                       <a href="{{ path('rapsysair_contact') }}" rel="contact">{% trans %}Contact{% endtrans %}</a>
+                                                       <a href="{{ path('rapsysair_frequently_asked_questions') }}">{% trans %}Frequently asked questions{% endtrans %}</a>
+                                                       {% if is_granted('ROLE_ADMIN') %}
+                                                               <a href="{{ path('rapsysair_user') }}">{% trans %}Users{% endtrans %}</a>
+                                                       {% endif %}
+                                                       {% if is_granted('ROLE_GUEST') %}
+                                                               <a href="{{ path('rapsysair_organizer_regulation') }}">{% trans %}Organizer regulation{% endtrans %}</a>
+                                                       {% endif %}
+                                                       {% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
+                                                               <a href="{{ path('rapsysuser_edit', {mail: app.user.mail|short, hash: app.user.mail|short|hash}) }}">{% trans %}My account{% endtrans %}</a>
+                                                               <a href="{{ path('rapsysuser_logout') }}">{% trans %}Logout{% endtrans %}</a>
+                                                       {% else %}
+                                                               <a href="{{ path('rapsysuser_login') }}">{% trans %}Login{% endtrans %}</a>
+                                                               <a href="{{ path('rapsysuser_register') }}">{% trans %}Register{% endtrans %}</a>
+                                                       {% endif %}
+                                               </nav>
+                                       {% endblock %}
+                       {#
+                                       {% block site_subtitle %}{% endblock %}
+                                       {% block site_tagline %}
+                                               {% if tags is defined and tags %}
+                                                       <ul>
+                                                               {% for id, tag in tags %}
+                                                                       <li><h2><a href="#{{id}}">{{tag}}</a></h2></li>
+                                                               {% endfor %}
+                                                       </ul>
+                                               {% endif %}
+                                       {% endblock %}
+                       #}
+                               </header>
+                       {% endblock %}
+                       {% block message %}
+                               {# pass an array argument to get the messages of those types (['warning', 'error']) #}
+                               {% for label, messages in app.flashes %}
+                                       {% if messages %}
+                                               <div class="message {{label}}">
+                                                       <ul>
+                                                               {% for message in messages %}
+                                                                       <li>{{ message }}</li>
+                                                               {% endfor %}
+                                                       </ul>
+                                               </div>
+                                       {% endif %}
+                               {% endfor %}
+                       {% endblock %}
+               {#
+                       {% block sidebar %}<aside id="sidebar"></aside>{% endblock %}
+               #}
+                       {% block content %}
+                               <article>
+                                       <header>
+                                               <h2>{% trans %}Outdoor space reservation system{% endtrans %}</h2>
+                                       </header>
+                               </article>
+                       {% endblock %}
+                       {% block footer %}
+                               <footer id="footer">
+                                       <a href="{{ path('rapsysair_about') }}">{% trans %}About{% endtrans %}</a>
+                                       {% if copy is defined and copy %}
+                                               <details><summary>{{ copy.long }}</summary><span>{{ copy.short }} <a href="{{ copy.link }}" title="{{ copy.title }}" rel="author">{{ copy.by }}</a></span></details>
+                                       {% endif %}
+                                       <a href="{{ path('rapsysair_terms_of_service') }}">{% trans %}Terms of service{% endtrans %}</a>
+                                       {% if alternates is defined and alternates %}
+                                               {% set langs = alternates|keys|filter(v => v|length == 5) %}
+                                               {% if langs|length > 1 %}
+                                                       <ul>
+                                                               {% for lang in langs %}
+                                                                       <li><a href="{{ alternates[lang].relative }}" hreflang="{{ lang|replace({'_': '-'}) }}" title="{{ alternates[lang].title }}">{{ alternates[lang].translated }}</a></li>
+                                                               {% endfor %}
+                                                       </ul>
+                                               {% else %}
+                                                       {% set lang = langs|first %}
+                                                       <a href="{{ alternates[lang].relative }}" hreflang="{{ lang|replace({'_': '-'}) }}" title="{{ alternates[lang].title }}">{{ alternates[lang].translated }}</a>
+                                               {% endif %}
+                                       {% else %}
+                                               <span>&nbsp;</span>
+                                       {% endif %}
+                               </footer>
+                       {% endblock %}
+               {% endblock %}
+               {% block javascript %}
+                       {% stopwatch 'javascript' %}
+                               {#{% javascript '@RapsysAir/js/*.js' %}
+                                       <script type="text/javascript" src="{{ asset_url }}"></script>
+                               {% endjavascript %}#}
+                       {% endstopwatch %}
+               {% endblock %}
        </body>
 </html>
diff --git a/Resources/views/body.html.twig b/Resources/views/body.html.twig
deleted file mode 100644 (file)
index d2c8224..0000000
+++ /dev/null
@@ -1,156 +0,0 @@
-{% extends '@RapsysAir/base.html.twig' %}
-{% block metas %}
-       <meta charset="UTF-8" />
-{% endblock %}
-{% block stylesheets %}
-       <meta name="viewport" content="width=device-width, initial-scale=1" />
-       {% if description is defined and description %}
-               <meta name="description" content="{{ description }}" />
-       {% endif %}
-       {% if keywords is defined and keywords %}
-               <meta name="keywords" content="{{ keywords|join(', ') }}" />
-       {% endif %}
-       {% if site is defined and site %}
-               <link rel="shortcut icon" type="image/x-icon" href="{{ asset(site.ico) }}" />
-               <link rel="icon" type="image/svg+xml" href="{{ asset(site.svg) }}" />
-               {% for size, icon in site.png %}
-                       {# Apple #}
-                       {% if size in [120, 152, 167, 180] %}
-                               {% if size == 180 %}
-                                       <link rel="apple-touch-icon" href="{{ asset(icon) }}" />
-                               {% endif %}
-                               <link rel="apple-touch-icon" sizes="{{ size }}x{{ size }}" href="{{ asset(icon) }}" />
-                       {# Windows #}
-                       {% elseif size in [70, 150, 310] %}
-                               <meta name="msapplication-square{{ size }}x{{ size }}logo" content="{{ asset(icon) }}" />
-                       {# Others #}
-                       {% else %}
-                               <link rel="icon" type="image/png" sizes="{{ size }}x{{ size }}" href="{{ asset(icon) }}" />
-                       {% endif %}
-               {% endfor %}
-       {% endif %}
-       {# stylesheet '//fonts.googleapis.com/css?family=Irish+Grover' '//fonts.googleapis.com/css?family=La+Belle+Aurore' '@RapsysAirBundle/Resources/public/css/{reset,screen}.css' #}
-       {% stopwatch 'stylesheet' %}
-               {% stylesheet '@rapsys_air_bundle/css/{reset,droidsans,screen}.css' %}
-                       <link rel="stylesheet" type="text/css" href="{{ asset_url }}" />
-               {% endstylesheet %}
-       {% endstopwatch %}
-       {% if canonical is defined and canonical %}
-               <link rel="canonical" href="{{ canonical }}"{% if locale is defined and locale %} hreflang="{{ locale }}"{% endif %} />
-       {% endif %}
-       {% if alternates is defined and alternates %}
-               {% for lang, alternate in alternates %}
-                       <link rel="alternate" href="{{ alternate.absolute }}" hreflang="{{ lang }}" />
-               {% endfor %}
-       {% endif %}
-       {% if facebook['metas'] is defined and facebook['metas'] %}
-               {% for property, contents in facebook['metas'] %}
-                       {% if contents is iterable %}
-                               {% for content in contents %}
-                                       <meta property="{{ property }}" content="{{ content }}" />
-                               {% endfor %}
-                       {% else %}
-                               <meta property="{{ property }}" content="{{ contents }}" />
-                       {% endif %}
-               {% endfor %}
-       {% endif %}
-{% endblock %}
-{% block javascripts %}
-       {% stopwatch 'javascript' %}
-               {#{% javascript '@RapsysAir/js/*.js' %}
-                       <script type="text/javascript" src="{{ asset_url }}"></script>
-               {% endjavascript %}#}
-       {% endstopwatch %}
-{% endblock %}
-{% block title %}{{ [site.title, section, title]|filter(v => v)|join(' - ') }}{% endblock %}
-{% block body %}
-       {% block header %}
-               <header id="header">
-                       {% if site is defined and site %}
-                               {% block header_title %}<h1><a href="{{ site.url }}" title="{{ site.title }}"><img src="{{ asset(site.logo) }}" alt="{{ site.title }}" width="171" height="32" /></a></h1>{% endblock %}
-                       {% endif %}
-                       {% block header_nav %}
-                               <nav>
-                                       <h2>{% trans %}Navigation{% endtrans %}</h2>
-                                       <ul>
-                                               <li><a href="{{ path('rapsys_air') }}" rel="home">{% trans %}Home{% endtrans %}</a></li>
-                                               <li><a href="{{ path('rapsys_air_contact') }}" rel="contact">{% trans %}Contact{% endtrans %}</a></li>
-                                               <li><a href="{{ path('rapsys_air_frequently_asked_questions') }}">{% trans %}Frequently asked questions{% endtrans %}</a></li>
-                                               {% if is_granted('ROLE_ADMIN') %}
-                                                       <li><a href="{{ path('rapsys_air_user') }}">{% trans %}Users{% endtrans %}</a></li>
-                                               {% endif %}
-                                               {% if is_granted('ROLE_GUEST') %}
-                                                       <li><a href="{{ path('rapsys_air_organizer_regulation') }}">{% trans %}Organizer regulation{% endtrans %}</a></li>
-                                               {% endif %}
-                                               {% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
-                                                       <li><a href="{{ path('rapsys_user_edit', {mail: app.user.mail|short, hash: app.user.mail|short|hash}) }}">{% trans %}My account{% endtrans %}</a></li>
-                                                       <li><a href="{{ path('rapsys_user_logout') }}">{% trans %}Logout{% endtrans %}</a></li>
-                                               {% else %}
-                                                       <li><a href="{{ path('rapsys_user_login') }}">{% trans %}Login{% endtrans %}</a></li>
-                                                       <li><a href="{{ path('rapsys_user_register') }}">{% trans %}Register{% endtrans %}</a></li>
-                                               {% endif %}
-                                       </ul>
-                               </nav>
-                       {% endblock %}
-{#
-                       {% block site_subtitle %}{% endblock %}
-                       {% block site_tagline %}
-                               {% if tags is defined and tags %}
-                                       <ul>
-                                               {% for id, tag in tags %}
-                                                       <li><h2><a href="#{{id}}">{{tag}}</a></h2></li>
-                                               {% endfor %}
-                                       </ul>
-                               {% endif %}
-                       {% endblock %}
-#}
-               </header>
-       {% endblock %}
-       {% block message %}
-               {# pass an array argument to get the messages of those types (['warning', 'error']) #}
-               {% for label, messages in app.flashes %}
-                       {% if messages %}
-                               <div class="message {{label}}">
-                                       <ul>
-                                               {% for message in messages %}
-                                                       <li>{{ message }}</li>
-                                               {% endfor %}
-                                       </ul>
-                               </div>
-                       {% endif %}
-               {% endfor %}
-       {% endblock %}
-{#
-       {% block sidebar %}<aside id="sidebar"></aside>{% endblock %}
-#}
-       {% block content %}
-               <section id="content">
-                       <h2><a href="{{ path('rapsys_air_homepage') }}">{{ section }}</a></h2>
-                       <p>{% trans %}Outdoor space reservation system{% endtrans %}</p>
-               </section>
-       {% endblock %}
-       {% block footer %}
-               <footer id="footer">
-                       <a href="{{ path('rapsys_air_about') }}">{% trans %}About{% endtrans %}</a>
-                       {% if copy is defined and copy %}
-                               <details><summary>{{ copy.long }}</summary><span>{{ copy.short }} <a href="{{ copy.link }}" title="{{ copy.title }}" rel="author">{{ copy.by }}</a></span></details>
-                       {% endif %}
-                       <a href="{{ path('rapsys_air_terms_of_service') }}">{% trans %}Terms of service{% endtrans %}</a>
-                       {% if alternates is defined and alternates %}
-                               {% set langs = alternates|keys|filter(v => v|length == 5) %}
-                               {% if langs|length > 1 %}
-                                       <ul>
-                                               {% for lang in langs %}
-                                                       <li><a href="{{ alternates[lang].relative }}" hreflang="{{ lang|replace({'_': '-'}) }}" title="{{ alternates[lang].title }}">{{ alternates[lang].translated }}</a></li>
-                                               {% endfor %}
-                                       </ul>
-                               {% else %}
-                                       {% set lang = langs|first %}
-                                       <a href="{{ alternates[lang].relative }}" hreflang="{{ lang|replace({'_': '-'}) }}" title="{{ alternates[lang].title }}">{{ alternates[lang].translated }}</a>
-                               {% endif %}
-                       {% else %}
-                               <span>&nbsp;</span>
-                       {% endif %}
-               </footer>
-       {% endblock %}
-{% endblock %}
index 8b18f8280916655dd14c1c55eadf997bb0bdbe43..42db49f1cf8491e741f29ebc9836a1e17e48943c 100644 (file)
@@ -1,8 +1,8 @@
-{% extends '@RapsysAir/body.html.twig' %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
        <section id="form">
                <header>
-                       <h2><a href="{{ path('rapsys_air_calendar') }}">{{ section }}</a></h2>
+                       <h2><a href="{{ path('rapsysair_calendar') }}">{{ section }}</a></h2>
                        <p>{{ description }}</p>
                </header>
                {% if error is defined %}
index e650aeeed62536154e471ad7d9d8f0c4a16e7ab6..ea7e36d4a6e9edefa3c7197f011b4442925fa3d5 100644 (file)
@@ -1,8 +1,8 @@
-{% extends '@RapsysAir/body.html.twig' %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
        <section id="form">
                <header>
-                       <h2><a href="{{ path('rapsys_air_calendar') }}">{{ section }}</a></h2>
+                       <h2><a href="{{ path('rapsysair_calendar') }}">{{ section }}</a></h2>
                        <p>{{ description }}</p>
                </header>
                {{ form_start(form) }}
diff --git a/Resources/views/default/_city.html.twig b/Resources/views/default/_city.html.twig
new file mode 100644 (file)
index 0000000..5119a50
--- /dev/null
@@ -0,0 +1,46 @@
+{# Display cities #}
+{% if cities is defined and cities %}
+       <article>
+               <header>
+                       <h2><a href="{{ path('rapsysair_city') }}" title="{% trans %}Libre Air cities{% endtrans %}">{% trans %}Cities{% endtrans %}</a></h2>
+                       <p>{% trans %}Libre Air city list{% endtrans%}
+               </header>
+               <div class="panel">
+                       {% if multimap is defined and multimap %}
+                               <div class="multimap">
+                                       <a href="{{ multimap.link }}" title="{{ multimap.caption }}">
+                                               <figure>
+                                                       <img src="{{ multimap.src }}" alt="{{ multimap.caption }}" width="{{ multimap.width }}" height="{{ multimap.height }}" />
+                                                       <figcaption>{{ multimap.caption }}</figcaption>
+                                               </figure>
+                                       </a>
+                               </div>
+                       {% endif %}
+                       <div class="grid{% if cities|length > 1 %} two{% endif %}">
+                               {% for id, city in cities %}
+                                       <article>
+                                               <header>
+                                                       <h3><a href="{{ city.link }}">{{ city.city }} ({{ city.id }})</a></h3>
+                                               </header>
+                                               {# {% if city.locations is defined and city.locations %}
+                                                       <div class="panel grid">
+                                                               {% for id, location in city.locations %}
+                                                                       <article class="cell">
+                                                                               <h4>{% if multimap is defined and multimap %}{{ id }} {% endif %}<a href="{{ location.link }}">{{ location.title }}</a></h4>
+                                                                       </article>
+                                                               {% endfor %}
+                                                       </div>
+                                               {% endif %} #}
+                                               {% if city.locations is defined and city.locations %}
+                                                       <ul>
+                                                               {% for id, location in city.locations %}
+                                                                       <li><a href="{{ location.link }}">{% if multimap is defined and multimap %}{{ id }} {% endif %}{{ location.title }}</a></li>
+                                                               {% endfor %}
+                                                       </ul>
+                                               {% endif %}
+                                       </article>
+                               {% endfor %}
+                       </div>
+               </div>
+       </article>
+{% endif %}
index 565e409ff47fc3e1a70903a5f0e37535347f09bc..beca449aeddc9d287135cd11ac71087ab282b0b0 100644 (file)
 {% if locations is defined and locations %}
        <article class="location">
                <header>
-                       <h2><a href="{{ path('rapsys_air_location') }}">{% trans %}Locations{% endtrans %}</a></h2>
-                       {% if forms.snippets is defined %}
+                       <h2><a href="{{ locations_link }}">{{ locations_title }}</a></h2>
+                       {% if locations_description is defined %}
+                               <p>{{ locations_description }}</p>
+                       {% elseif forms.snippets is defined %}
                                <p>{% trans %}Organizer's snippet by dance space{% endtrans %}</p>
                        {% else %}
                                <p>{% trans %}Libre Air location list{% endtrans %}</p>
                        {% endif %}
                </header>
                <div class="panel">
-                       <div class="grid four">
-                               {% for id, title in locations %}
-                                       <article class="cell">
-                                               <h3><a href="{{ path('rapsys_air_location_view', {'id': id}) }}">{{ title }}</a></h3>
-                                               {% if forms.snippets is defined and forms.snippets[id] is defined and forms.snippets[id] %}
-                                                       {{ form_start(forms.snippets[id]) }}
-                                                               <div>
-                                                                       {{ form_row(forms.snippets[id].description) }}
-
-                                                                       {{ form_row(forms.snippets[id].class) }}
+                       {% if multimap is defined and multimap %}
+                               <div class="multimap">
+                                       <a href="{{ multimap.link }}" title="{{ multimap.caption }}">
+                                               <figure>
+                                                       <img src="{{ multimap.src }}" alt="{{ multimap.caption }}" width="{{ multimap.width }}" height="{{ multimap.height }}" />
+                                                       <figcaption>{{ multimap.caption }}</figcaption>
+                                               </figure>
+                                       </a>
+                               </div>
+                       {% endif %}
+                       {% if forms.snippets is defined %}
+                               <div class="grid">
+                                       {% for i, l in locations %}
+                                               <article class="cell{% if l.count is defined and l.count or location.id is defined and location.id == l.id or session.location.id is defined and session.location.id == l.id %} highlight{% endif %}">
+                                                       <header>
+                                                               {# TODO XXX virer le if l.link id defined when user view is fixed !!! #}
+                                                               <h3><a href="{{ l.link }}">{% if multimap is defined and multimap %}{{ i }} {% endif %}{{ l.title }}</a></h3>
+                                                       </header>
+                                                       {% if forms.snippets[i] is defined and forms.snippets[i] %}
+                                                               {{ form_start(forms.snippets[i]) }}
+                                                                       <div>
+                                                                               {% if forms.snippets[i].description is defined %}
+                                                                                       {{ form_row(forms.snippets[i].description) }}
+                                                                               {% endif %}
 
-                                                                       {{ form_row(forms.snippets[id].short) }}
+                                                                               {% if forms.snippets[i].class is defined %}
+                                                                                       {{ form_row(forms.snippets[i].class) }}
+                                                                               {% endif %}
 
-                                                                       {{ form_row(forms.snippets[id].rate) }}
+                                                                               {% if forms.snippets[i].short is defined %}
+                                                                                       {{ form_row(forms.snippets[i].short) }}
+                                                                               {% endif %}
 
-                                                                       {{ form_row(forms.snippets[id].hat) }}
+                                                                               {% if forms.snippets[i].rate is defined %}
+                                                                                       {{ form_row(forms.snippets[i].rate) }}
+                                                                               {% endif %}
 
-                                                                       {{ form_row(forms.snippets[id].contact) }}
+                                                                               {% if forms.snippets[i].hat is defined %}
+                                                                                       {{ form_row(forms.snippets[i].hat) }}
+                                                                               {% endif %}
 
-                                                                       {{ form_row(forms.snippets[id].donate) }}
+                                                                               {% if forms.snippets[i].contact is defined %}
+                                                                                       {{ form_row(forms.snippets[i].contact) }}
+                                                                               {% endif %}
 
-                                                                       {{ form_row(forms.snippets[id].link) }}
+                                                                               {% if forms.snippets[i].donate is defined %}
+                                                                                       {{ form_row(forms.snippets[i].donate) }}
+                                                                               {% endif %}
 
-                                                                       {{ form_row(forms.snippets[id].profile) }}
+                                                                               {% if forms.snippets[i].link is defined %}
+                                                                                       {{ form_row(forms.snippets[i].link) }}
+                                                                               {% endif %}
 
-                                                                       {{ form_row(forms.snippets[id].image) }}
+                                                                               {% if forms.snippets[i].profile is defined %}
+                                                                                       {{ form_row(forms.snippets[i].profile) }}
+                                                                               {% endif %}
 
-                                                                       {{ form_row(forms.snippets[id].submit) }}
+                                                                               {{ form_row(forms.snippets[i].submit) }}
+                                                                       </div>
 
-                                                                       {% if forms.snippets[id].delete is defined %}
-                                                                               {{ form_row(forms.snippets[id].delete) }}
-                                                                       {% endif %}
+                                                                       {# render csrf token etc .#}
+                                                                       <footer style="display:none">{{ form_rest(forms.snippets[i]) }}</footer>
+                                                               {{ form_end(forms.snippets[i]) }}
+                                                       {% endif %}
+                                                       {% if l.image is defined and l.image %}
+                                                               <div class="thumb">
+                                                                       <a href="{{ l.image.link }}" title="{{ l.image.caption }}">
+                                                                               <figure>
+                                                                                       <img src="{{ l.image.src }}" alt="{{ l.image.caption }}" width="{{ l.image.width }}" height="{{ l.image.height }}" />
+                                                                                       <figcaption>{{ l.image.caption }}</figcaption>
+                                                                               </figure>
+                                                                       </a>
                                                                </div>
+                                                       {% endif %}
+                                                       {% if forms.images is defined and forms.images[i] is defined and forms.images[i] %}
+                                                               {{ form_start(forms.images[i]) }}
+                                                                       <div>
+                                                                               {% if forms.images[i].image is defined %}
+                                                                                       {{ form_row(forms.images[i].image) }}
+                                                                               {% endif %}
 
-                                                               {# render csrf token etc .#}
-                                                               <footer style="display:none">{{ form_rest(forms.snippets[id]) }}</footer>
-                                                       {{ form_end(forms.snippets[id]) }}
-                                               {% endif %}
-                                       </article>
-                               {% endfor %}
-                       </div>
+                                                                               {{ form_row(forms.images[i].submit) }}
+
+                                                                               {% if forms.images[i].delete is defined %}
+                                                                                       {{ form_row(forms.images[i].delete) }}
+                                                                               {% endif %}
+                                                                       </div>
+
+                                                                       {# render csrf token etc .#}
+                                                                       <footer style="display:none">{{ form_rest(forms.images[i]) }}</footer>
+                                                               {{ form_end(forms.images[i]) }}
+                                                       {% endif %}
+                                               </article>
+                                       {% endfor %}
+                               </div>
+                       {% else %}
+                               <ul class="grid{% if locations|length > 1 %} two{% endif %}">
+                                       {% for i, l in locations %}
+                                               {# TODO XXX virer le if l.link id defined when user view is fixed !!! #}
+                                               <li><a href="{{ l.link }}">{% if multimap is defined and multimap %}{{ i }} {% endif %}{{ l.title }}</a></li>
+                                       {% endfor %}
+                               </ul>
+                       {% endif %}
                </div>
        </article>
 {% endif %}
index db9a100af692224a531306db4c50b18a97a21b7a..9510a7a8a32a0860d28e9fb6200e61542c7623ac 100644 (file)
@@ -1,12 +1,13 @@
-{% extends '@RapsysAir/body.html.twig' %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
-       <section id="dashboard">
+       <article id="dashboard">
                <header>
-                       <h2><a href="{{ path('rapsys_air_about') }}">{{ title }}</a></h2>
-                       <p>{{ description }}</p>
+                       <h2>{{ description }}</h2>
                </header>
                <nav>
-                       <strong>{% trans %}Table of contents{% endtrans %}</strong>
+                       <header>
+                               <h3>{% trans %}Table of contents{% endtrans %}</h3>
+                       </header>
                        <ul>
                                <li><a href="#about">{% trans %}About Libre Air{% endtrans %}</a></li>
                                <li><a href="#mission">{% trans %}Our mission{% endtrans %}</a></li>
                        </ul>
                </nav>
                <section>
-                       <h3 id="about">{% trans %}About Libre Air{% endtrans %}</h3>
-                       <p>{{ '%title%, an information site for outdoor dancers and organizers.'|trans({'%title%': '<a href="' ~ absolute_url(site.url) ~ '" title="' ~ site.title ~ '">' ~ site.title ~ '</a>'})|raw }}</p>
+                       <header>
+                               <h3 id="about">{% trans %}About Libre Air{% endtrans %}</h3>
+                       </header>
+                       <p>{{ '%title%, an information site for outdoor dancers and organizers.'|trans({'%title%': '<a href="' ~ absolute_url(root) ~ '" title="' ~ title.site ~ '">' ~ title.site ~ '</a>'})|raw }}</p>
                </section>
                <section>
-                       <h3 id="mission">{% trans %}Our mission{% endtrans %}</h3>
-                       <p>{{ '%title%\'s mission is simple: provide dancers with the information they need to easily find organizers dance sessions.'|trans({'%title%': '<a href="' ~ absolute_url(site.url) ~ '" title="' ~ site.title ~ '">' ~ site.title ~ '</a>'})|raw }}</p>
+                       <header>
+                               <h3 id="mission">{% trans %}Our mission{% endtrans %}</h3>
+                       </header>
+                       <p>{{ '%title%\'s mission is simple: provide dancers with the information they need to easily find organizers dance sessions.'|trans({'%title%': '<a href="' ~ absolute_url(root) ~ '" title="' ~ title.site ~ '">' ~ title.site ~ '</a>'})|raw }}</p>
                        <p>{% trans %}If it's an outdoor place we want to document it.{% endtrans %}</p>
-                       {# <p><a href="{{ absolute_url(site.url) }}" title="{{ site.title }}">{{ site.title }}</a>{% trans %}, an information site for outdoor dancers and organizers.{% endtrans %}</p> #}
+                       {# <p><a href="{{ absolute_url(root) }}" title="{{ title.site }}">{{ title.site }}</a>{% trans %}, an information site for outdoor dancers and organizers.{% endtrans %}</p> #}
                </section>
                <section>
-                       <h3 id="howtohelp">{% trans %}How you can help{% endtrans %}</h3>
+                       <header>
+                               <h3 id="howtohelp">{% trans %}How you can help{% endtrans %}</h3>
+                       </header>
                        <p>{% trans %}Our community is composed of amazing dancers all around the world who participate, review dance session and help us promote them.{% endtrans %}</p>
-                       <p>{{ 'However, you don\'t need to be a professional dancer or organizer to help %title%!'|trans({'%title%': '<a href="' ~ absolute_url(site.url) ~ '" title="' ~ site.title ~ '">' ~ site.title ~ '</a>'})|raw }}</p>
-                       <p>{{ 'Donate on %title% to help us fund our mission.'|trans({'%title%': '<a href="' ~ site.donate ~ '" title="' ~ 'Fund our mission'|trans ~ '">' ~ 'our donation page'|trans ~ '</a>'})|raw }}</p>
+                       <p>{{ 'However, you don\'t need to be a professional dancer or organizer to help %title%!'|trans({'%title%': '<a href="' ~ absolute_url(root) ~ '" title="' ~ title.site ~ '">' ~ title.site ~ '</a>'})|raw }}</p>
+                       <p>{{ 'Donate on %title% to help us fund our mission.'|trans({'%title%': '<a href="' ~ donate ~ '" title="' ~ 'Fund our mission'|trans ~ '">' ~ 'our donation page'|trans ~ '</a>'})|raw }}</p>
                </section>
                <section>
-                       <h3 id="history">{% trans %}History of Libre Air{% endtrans %}</h3>
+                       <header>
+                               <h3 id="history">{% trans %}History of Libre Air{% endtrans %}</h3>
+                       </header>
                        {# <p>{% trans %}Our community is composed of amazing dancers all around the world who participate, review dance session and help us promote them.{% endtrans %}</p>
-                       <p>{{ 'However, you don\'t need to be a professional dancer or organizer to help %title%!'|trans({'%title%': '<a href="' ~ absolute_url(site.url) ~ '" title="' ~ site.title ~ '">' ~ site.title ~ '</a>'})|raw }}</p>
-                       <p>{{ 'Donate on %title% to help us fund our mission.'|trans({'%title%': '<a href="' ~ site.donate ~ '" title="' ~ 'Fund our mission'|trans ~ '">' ~ 'our donation page'|trans ~ '</a>'})|raw }}</p> #}
+                       <p>{{ 'However, you don\'t need to be a professional dancer or organizer to help %title%!'|trans({'%title%': '<a href="' ~ absolute_url(root) ~ '" title="' ~ title.site ~ '">' ~ title.site ~ '</a>'})|raw }}</p>
+                       <p>{{ 'Donate on %title% to help us fund our mission.'|trans({'%title%': '<a href="' ~ donate ~ '" title="' ~ 'Fund our mission'|trans ~ '">' ~ 'our donation page'|trans ~ '</a>'})|raw }}</p> #}
 
-                       <p>{{ 'The %title% project started in 2019 when the need for a new dispatch platform for outdoor dance events arose.'|trans({'%title%': '<a href="' ~ absolute_url(site.url) ~ '" title="' ~ site.title ~ '">' ~ site.title ~ '</a>'})|raw }}</p>
+                       <p>{{ 'The %title% project started in 2019 when the need for a new dispatch platform for outdoor dance events arose.'|trans({'%title%': '<a href="' ~ absolute_url(root) ~ '" title="' ~ title.site ~ '">' ~ title.site ~ '</a>'})|raw }}</p>
                        <p>{% trans %}Since then the project has grown and now forms a central point for broadcasting outdoor dance events from different organizers.{% endtrans %}</p>
-                       <p>{{ 'In the future, %title% hopes to become a daily visited resource for participants and organizers of outdoor dance sessions.'|trans({'%title%': '<a href="' ~ absolute_url(site.url) ~ '" title="' ~ site.title ~ '">' ~ site.title ~ '</a>'})|raw }}</p>
+                       <p>{{ 'In the future, %title% hopes to become a daily visited resource for participants and organizers of outdoor dance sessions.'|trans({'%title%': '<a href="' ~ absolute_url(root) ~ '" title="' ~ title.site ~ '">' ~ title.site ~ '</a>'})|raw }}</p>
                </section>
                <section>
-                       <h3 id="technicalsupport">{% trans %}Technical support{% endtrans %}</h3>
+                       <header>
+                               <h3 id="technicalsupport">{% trans %}Technical support{% endtrans %}</h3>
+                       </header>
                        <p>{{ 'The development and hosting services are graciously provided by: %title%'|trans({'%title%': '<a href="' ~ copy.link ~ '" title="' ~ copy.title ~ '">' ~ copy.by ~ '</a>'})|raw }}</p>
-                       <p>{{ 'In case of bug or glitch you may contact us throught the contact form: %title%'|trans({'%title%': '<a href="' ~ path('rapsys_air_contact') ~ '" title="' ~ 'Contact Libre Air'|trans ~ '">' ~ 'Contact'|trans ~ '</a>'})|raw }}</p>
+                       <p>{{ 'In case of bug or glitch you may contact us throught the contact form: %title%'|trans({'%title%': '<a href="' ~ path('rapsysair_contact') ~ '" title="' ~ 'Contact Libre Air'|trans ~ '">' ~ 'Contact'|trans ~ '</a>'})|raw }}</p>
                </section>
-       </section>
+       </article>
 {% endblock %}
diff --git a/Resources/views/default/dispute.html.twig b/Resources/views/default/dispute.html.twig
deleted file mode 100644 (file)
index 6dc341e..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-{% extends '@RapsysAir/body.html.twig' %}
-{% block content %}
-       <section id="form">
-               <header>
-                       <h2><a href="{{ path('rapsys_air_dispute') }}">{{ title }}</a></h2>
-                       <p>{{ description }}</p>
-               </header>
-               {% if sent %}
-                       <p class="message notice">{% trans %}Your message has been sent{% endtrans %}</p>
-               {% else %}
-                       {{ form_start(form) }}
-                               <div>
-                                       {{ form_row(form.offense) }}
-
-                                       {{ form_row(form.court) }}
-
-                                       {{ form_row(form.notice) }}
-
-                                       {{ form_row(form.agent) }}
-
-                                       {{ form_row(form.service) }}
-
-                                       {{ form_row(form.abstract) }}
-
-                                       {{ form_row(form.submit) }}
-                               </div>
-
-                               {# Render CSRF token etc .#}
-                               <footer style="display:none">{{ form_rest(form) }}</footer>
-                       {{ form_end(form) }}
-               {% endif %}
-       </section>
-{% endblock %}
index 78279eaa10c5e8806c5b629b89aefc7e42fead75..df5e8bf3dc56771f3e4d4252aac6f24904466bd4 100644 (file)
@@ -1,12 +1,13 @@
-{% extends '@RapsysAir/body.html.twig' %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
-       <section id="dashboard">
+       <article id="dashboard">
                <header>
-                       <h2><a href="{{ path('rapsys_air_frequently_asked_questions') }}">{{ title }}</a></h2>
-                       <p>{{ description }}</p>
+                       <h2>{{ description }}</h2>
                </header>
                <nav>
-                       <strong>{% trans %}Table of contents{% endtrans %}</strong>
+                       <header>
+                               <h3>{% trans %}Table of contents{% endtrans %}</h3>
+                       </header>
                        <ul>
                                <li><a href="#preamble">{% trans %}Preamble{% endtrans %}</a></li>
                                <li><a href="#whatisit">{% trans %}What is it ?{% endtrans %}</a></li>
                        </ul>
                </nav>
                <section>
-                       <h3 id="preamble">{% trans %}Preamble{% endtrans %}</h3>
+                       <header>
+                               <h3 id="preamble">{% trans %}Preamble{% endtrans %}</h3>
+                       </header>
                        <p>{% trans %}On this page you will find the most recurring questions of Libre Air users.{% endtrans %}</p>
                        <p>{% trans %}Questions asked via the contact form will be added to this page as they arise.{% endtrans %}</p>
                </section>
                <section>
-                       <h3 id="whatisit">{% trans %}What is it ?{% endtrans %}</h3>
+                       <header>
+                               <h3 id="whatisit">{% trans %}What is it ?{% endtrans %}</h3>
+                       </header>
                        <p>{% trans %}Libre Air is an initiative to have a powerful and shared calendar for outdoor Argentine Tango sessions.{% endtrans %}</p>{# sur paris dans un premier temps #}
                </section>
                <section>
-                       <h3 id="whocancreateanaccount">{% trans %}Who can create an account ?{% endtrans %}</h3>
+                       <header>
+                               <h3 id="whocancreateanaccount">{% trans %}Who can create an account ?{% endtrans %}</h3>
+                       </header>
                        <p>{% trans %}Anyone can create an account on Air Libre, dancer or organizer.{% endtrans %}</p>
                        <p>{% trans %}The dancer can follow an organizer by email.{% endtrans %}</p>
                        <p>{% trans %}The organizer will have a convenient way to automatically notify subscribers with minimum effort.{% endtrans %}</p>
@@ -42,5 +49,5 @@
                        <h3 id="">{% trans %}{% endtrans %}</h3>
                        <p>{% trans %}{% endtrans %}</p>
                </section>#}
-       </section>
+       </article>
 {% endblock %}
index e04f1b369cd7720b2ad4654475a3d2601aca88bf..8aeec00bdaf40dcb4b20fbfce8041fa0673c19c6 100644 (file)
@@ -1,45 +1,46 @@
-{% extends '@RapsysAir/body.html.twig' %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
-       <article id="dashboard">
+       <article>
                <header>
-                       <h2><a href="{{ path('rapsys_air') }}">{{ title }}</a></h2>
-                       <p>{{ description }}</p>
+                       <h2>{{ description }}</h2>
                </header>
                <div class="panel">
                        {% if calendar is defined and calendar %}
                                <div class="grid calendar seven">
                                        {% for date, day in calendar %}
-                                               <article class="{{ day.class|join(' ') }}">
-                                                       <h3>{{ day.title }}</h3>
+                                               <section class="{{ day.class|join(' ') }}">
+                                                       <header>
+                                                               <h3>{{ day.title }}</h3>
+                                                       </header>
                                                        {% if day.sessions is not empty %}
                                                                <ul>
                                                                        {% for session in day.sessions %}
                                                                                <li class="{{ session.class|join(' ') }}">
-                                                                                       <a href="{{ path('rapsys_air_session_view', {'id': session.id}) }}" title="{{ session.applications|join('\n') }}">
-                                                                                               <span>{{ session.start|localizeddate('none', 'short') }}</span>
-                                                                                               <span class="reducible">{{ session.location }}</span>
-                                                                                               <span class="info">
-                                                                                                       {% if session.weather is defined and session.weather %}
-                                                                                                               <span title="{{ session.weathertitle }}">{{ session.weather }}</span>
-                                                                                                       {% endif %}
-                                                                                                       <span title="{{ session.slottitle }}">{{ session.slot }}</span>
-                                                                                               </span>
-                                                                                               <span>{{ session.stop|localizeddate('none', 'short') }}</span>
-                                                                                               {% if session.pseudonym is defined and session.pseudonym %}
-                                                                                                       <span class="reducible{% if session.rate is not defined and session.hat is not defined %} pseudonym{% endif %}">{{ session.pseudonym }}</span>
+                                                                                       <a href="{{ session.link }}" title="{{ session.title }}">
+                                                                                               <span>{{ session.start|intldate('none', 'short') }}</span>
+                                                                                               <span class="reducible">{{ session.location.title }}</span>
+                                                                                               <span class="temperature"{% if session.temperature.title is defined and session.temperature.title %} title="{{ session.temperature.title }}"{% endif %}><span class="glyph">{{ session.temperature.glyph }}</span></span>
+                                                                                               <span>{{ session.stop|intldate('none', 'short') }}</span>
+                                                                                               <span class="reducible">{% if session.application.user.title is defined and session.application.user.title %}{{ session.application.user.title }}{% endif %}</span>
+                                                                                               <span class="rain"{% if session.rain.title is defined and session.rain.title %} title="{{ session.rain.title }}"{% endif %}><span class="glyph">{{ session.rain.glyph }}</span></span>
+                                                                                               {% if session.rate is defined and session.rate %}
+                                                                                                       <span class="rate" title="{{ session.rate.title }}">{% if session.rate.rate is defined and session.rate.rate %}{{ session.rate.rate }} {% endif %}<span class="glyph">{{ session.rate.glyph }}</span></span>
+                                                                                               {% else %}
+                                                                                                       <span></span>
                                                                                                {% endif %}
-                                                                                               {% if session.rate is defined %}<span class="info">{{ session.rate }} {% if session.hat is defined and session.hat %}🎩{% else %}€{% endif %}</span>{% endif %}
+                                                                                               <span class="reducible">{{ session.location.zipcode }} {{ session.location.city }}</span>
+                                                                                               <span></span>
                                                                                        </a>
                                                                                </li>
                                                                        {% endfor %}
                                                                </ul>
                                                        {% endif %}
-                                               </article>
+                                               </section>
                                        {% endfor %}
                                </div>
                        {% endif %}
                        {{ include('@RapsysAir/form/_toolbox.html.twig') }}
                </div>
        </article>
-       {{ include('@RapsysAir/default/_location.html.twig') }}
+       {{ include('@RapsysAir/default/_city.html.twig') }}
 {% endblock %}
index 35917f747b239e263b0cf1173317fde2baa31803..ac6c315f6da7154b78f9c35a308d367c28e04bd4 100644 (file)
@@ -1,12 +1,14 @@
-{% extends '@RapsysAir/body.html.twig' %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
-       <section id="dashboard">
+       <article>
                <header>
-                       <h2><a href="{{ path('rapsys_air_organizer_regulation') }}">{{ title }}</a></h2>
+                       <h2><a href="{{ path('rapsysair_organizer_regulation') }}">{{ title.page }}</a></h2>
                        <p>{{ description }}</p>
                </header>
                <nav>
-                       <strong>{% trans %}Table of contents{% endtrans %}</strong>
+                       <header>
+                               <h3>{% trans %}Table of contents{% endtrans %}</h3>
+                       </header>
                        <ul>
                                <li><a href="#preamble">{% trans %}Preamble{% endtrans %}</a></li>
                                <li><a href="#article_1">Article 1 : Présentation</a></li>
                        </ul>
                </nav>
                <section>
-                       <h3 id="preamble">{% trans %}Preamble{% endtrans %}</h3>
+                       <header>
+                               <h3 id="preamble">{% trans %}Preamble{% endtrans %}</h3>
+                       </header>
                        <p>Préalablement à la visite du site <a href="https://airlibre.eu" title="Air Libre">www.airlibre.eu</a>, ci-après dénommé « le Site », et à l'utilisation des services qui y sont proposés, il convient de procéder à la lecture des conditions générales d'utilisation et du règlement organisateur ci-dessous exposé.</p>
                        <p>Ce règlement organisateur a pour objet de définir les modalités de mise à disposition du service de réservation de séances de Tango Argentin sur l'espace public, ci-après « le Service Réservation », du service d'affichage, ci-après « le Service Affichage », du service d'attribution, ci-après « le Service Attribution », des services connexes, ci-après « les Services Connexes » et les conditions d'utilisation de ce dernier par l'organisateur.</p>
                        <p>Tout accès au Service Réservation ou toute utilisation des services connexes suppose le respect préalable de l'ensemble des dispositions ci-dessous ainsi que leur acceptation inconditionnelle. </p>
                        <p>Dans la mesure où l'organisateur ne souhaite pas accepter tout ou partie des présentes conditions spécifiques, il lui est demandé de renoncer à tout usage du service. Le préambule fait partie des conditions générales d'utilisation. </p>
                </section>
                <section>
-                       <h3 id="article_1">Article 1 : Présentation</h3>
+                       <header>
+                               <h3 id="article_1">Article 1 : Présentation</h3>
+                       </header>
                        <p>Le Service Affichage du Site est ouvert à toute personne souhaitant consulter les séances de Tango Argentin attribuées à un organisateur sur les espaces de danse public.</p>
                        <p>Le Service Réservation du Site est ouvert à toute personne avec la volonté d'assurer une séance de Tango Argentin sur un espace de danse en public tant au niveau artistique, logistique, réglementaire et sans exclure l'aspect sécurité.</p>
                        <p>Le Service Attribution du Site assure une répartition équitable entre les différents organisateurs des séances sur un créneau et un lieu.</p>
                        <p>Les Services Connexes sont mis à disposition pour améliorer l'expérience utilisateur du Site.</p>
                </section>
                <section>
-                       <h3 id="article_2">Article 2 : Inscription, réunion et contribution</h3>
+                       <header>
+                               <h3 id="article_2">Article 2 : Inscription, réunion et contribution</h3>
+                       </header>
                        <section>
-                               <h4 id="article_2_1">Article 2.1 : Inscription</h4>
+                               <header>
+                                       <h4 id="article_2_1">Article 2.1 : Inscription</h4>
+                               </header>
                                <p>La personne souhaitant accéder au Service Réservation, ci-après « l'Organisateur » crée un compte sur le Site, puis soumet sa demande d'affectation à un groupe par tout moyen.</p>
                                <p>L'Organisateur joint à sa demande la liste du matériel dont il dispose de la jouissance exclusive ou partagée nécessaire à l'organisation en extérieur :</p>
                                <ul>
                                <p>La candidature engage le membre à mettre en commun tout lieu accessible au public sur lequel il souhaite organiser et ne pas réaliser une séance attribuée à un autre Organisateur.</p>
                        </section>
                        <section>
-                               <h4 id="article_2_2">Article 2.2 : Réunion</h4>
+                               <header>
+                                       <h4 id="article_2_2">Article 2.2 : Réunion</h4>
+                               </header>
                                <p>Des réunions publiques d'information sont réalisées en début d'année, avant l'été et après l'été, ci-après « la Réunion ».</p>
                                <p>La Réunion permet de faire un point de situation, soumettre et voter les propositions de modification du présent règlement.</p>
                                <p>La Réunion en début d'année sera planifiée dès que possible, celle avant l'été sera planifiée après une affluence de plus de 50 personnes sur une séance, celle après l'été sera planifiée après la rentrée.</p>
                        </section>
                        <section>
-                               <h4 id="article_2_3">Article 2.3 : Contribution</h4>
+                               <header>
+                                       <h4 id="article_2_3">Article 2.3 : Contribution</h4>
+                               </header>
                                <p>Une participation sera demandée aux frais de fonctionnement, sur la base du coût total annuel, réparti entre les différents Organisateurs. La contribution des Organisateurs intégrés après la collecte initiale sera provisionné pour l'année suivante.</p>
                                <p>Les frais à prévoir sont :</p>
                                <ul>
                        </section>
                </section>
                <section>
-                       <h3 id="article_3">Article 3 : Répartition</h3>
+                       <header>
+                               <h3 id="article_3">Article 3 : Répartition</h3>
+                       </header>
                        <p>Les membres seront répartis en différents groupes selon leur matériels et niveau de participation.</p>
                        <section>
-                               <h4 id="article_3_1">Article 3.1 : Invité</h4>
+                               <header>
+                                       <h4 id="article_3_1">Article 3.1 : Invité</h4>
+                               </header>
                                <p>L'Organisateur sans matériel ou extérieur à l'Île-de-France sera affecté au groupe invité, ci-après « l'Invité ».</p>
                                <p>Il s'assure de :</p>
                                <ul>
                                </ul>
                        </section>
                        <section>
-                               <h4 id="article_3_2">Article 3.2 : Régulier</h4>
+                               <header>
+                                       <h4 id="article_3_2">Article 3.2 : Régulier</h4>
+                               </header>
                                <p>L'Organisateur détenteur d'un matériel d'une puissance inférieure à 500W ou ne disposant pas de l'intégralité du Matériel Essentiel sera intégré au groupe régulier, ci-après « le Régulier ».</p>
                                <p>Il s'assure d' :</p>
                                <ul>
                                </ul>
                        </section>
                        <section>
-                               <h4 id="article_3_3">Article 3.3 : Senior</h4>
+                               <header>
+                                       <h4 id="article_3_3">Article 3.3 : Senior</h4>
+                               </header>
                                <p>L'Organisateur avec matériel d'une puissance supérieure ou égale à 500W et ayant réalisé plus de 5 séances sur plus de 2 (deux) lieux peut être intégré au groupe senior.</p>
                        </section>
                        <section>
-                               <h4 id="article_3_4">Article 3.4 : Administrateur</h4>
+                               <header>
+                                       <h4 id="article_3_4">Article 3.4 : Administrateur</h4>
+                               </header>
                                <p>Un Senior, élu par le collège des Organisateurs ayant réalisé plus de 20 (vingt) séances sur 2 (deux) lieux différents, son mandat est remis en jeu à chaque Réunion, sera en charge de faire appliquer le présent règlement au mieux, d'informer et d'accompagner les différents Organisateurs en cas de besoin, ci-après « l'Administrateur ».</p>
                        </section>
                        <section>
-                               <h4 id="article_3_5">Article 3.5 : Responsable</h4>
+                               <header>
+                                       <h4 id="article_3_5">Article 3.5 : Responsable</h4>
+                               </header>
                                <p>La personne en gestion du Site sera chargée d'affecter les Organisateurs, après réception de leur contribution aux frais, aux différents groupes en fonction des critères ci-dessus, ci-après « le Responsable ».</p>
                        </section>
                </section>
                <section>
-                       <h3 id="article_4">Article 4 : Lieux de danse, horaires et réalisation</h3>
+                       <header>
+                               <h3 id="article_4">Article 4 : Lieux de danse, horaires et réalisation</h3>
+                       </header>
                        <section>
-                               <h4 id="article_4_1">Article 4.1 : Lieux de danse</h4>
+                               <header>
+                                       <h4 id="article_4_1">Article 4.1 : Lieux de danse</h4>
+                               </header>
                                <p>Les espaces de danse pris en compte sont :</p>
                                <ul>
                                        <li>l'Opéra Garnier, ci-après « Garnier » ;</li>
                                <p>Pour Tokyo, la réservation se fera sur Trocadéro avec une modification de lieu une fois la séance attribuée.</p>
                        </section>
                        <section>
-                               <h4 id="article_4_2">Article 4.2 : Horaires</h4>
+                               <header>
+                                       <h4 id="article_4_2">Article 4.2 : Horaires</h4>
+                               </header>
                                <p>Les séances en après-midi commencent à 14h et finissent à 19h par défaut.</p>
                                <p>Les séances en soirée commencent à 19h et finissent à l'heure du dernier métro par défaut, soit 1h ou 2h les vendredi, samedi et veille de jours fériés.</p>
                                <p>Les horaires seront adaptés en fonction des contraintes des différents lieux.</p>
                                <p>La séance à Garnier notamment pourra commencer à partir de 21h en cas d'empêchement sur place.</p>
                        </section>
                        <section>
-                               <h4 id="article_4_3">Article 4.3 : Réalisation</h4>
+                               <header>
+                                       <h4 id="article_4_3">Article 4.3 : Réalisation</h4>
+                               </header>
                                <p>L'Organisateur procède au nettoyage de la zone de danse, le passage du balai est obligatoire pour les Quais et Trocadéro.</p>
                                <p>L'Organisateur assure un niveau sonore raisonnable de Tango Argentin sur la séance.</p>
                                <p>L'Organisateur programme par groupes de 3 ou 4 morceaux de Tango, Valse ou Milonga, maximum 3 pour la Milonga, séparé par une séparation musicale de 30 secondes à 3 minutes maximum.</p>
                        </section>
                </section>
                <section>
-                       <h3 id="article_5">Article 5 : Affichage, réservation et modifications</h3>
+                       <header>
+                               <h3 id="article_5">Article 5 : Affichage, réservation et modifications</h3>
+                       </header>
                        <section>
-                               <h4 id="article_5_1">Article 5.1 : Affichage</h4>
+                               <header>
+                                       <h4 id="article_5_1">Article 5.1 : Affichage</h4>
+                               </header>
                                <p>Le Service Affichage présente un agenda des séances à venir sur 5 (cinq) semaines.</p>
                                <p>Les séances attribuées et/ou annulées sont visibles sans identification.</p>
                                <p>L'ensemble des séances attribuées, annulées, verrouillées et en attente d'attribution sont visibles après identification.</p>
                                <p>Une vue spécifique à chaque séance présente les informations de cette dernière.</p>
                        </section>
                        <section>
-                               <h4 id="article_5_2">Article 5.2 : Réservation</h4>
+                               <header>
+                                       <h4 id="article_5_2">Article 5.2 : Réservation</h4>
+                               </header>
                                <p>L'Organisateur effectue une réservation en déposant une demande sur une séance.</p>
                                <p>L'Organisateur peut annuler sa réservation à tout moment.</p>
                                <p>L'Organisateur peut annuler pour pluie une séance même réalisée, avec une pluviométrie attendue minimale de 2 mm (deux millimètres), à tout moment, elle sera considérée comme annulée 24h avant son début.</p>
                                <p>L'Organisateur peut demander l'annulation pour cas de force majeure, sur justificatif photo, auprès d'un Administrateur ou du Responsable. En cas de motif valable, elle sera considérée comme annulée 24h avant son début.</p>
                        </section>
                        <section>
-                               <h4 id="article_5_3">Article 5.3 : Modification d'horaire</h4>
+                               <header>
+                                       <h4 id="article_5_3">Article 5.3 : Modification d'horaire</h4>
+                               </header>
                                <p>Le Régulier ou Senior peut modifier les horaires d'une séance qui lui est attribuée jusqu'à son dénouement.</p>
                        </section>
                        <section>
-                               <h4 id="article_5_4">Article 5.4 : Modification de lieu</h4>
+                               <header>
+                                       <h4 id="article_5_4">Article 5.4 : Modification de lieu</h4>
+                               </header>
                                <p>Le Senior peut déplacer une séance sur un autre lieu disponible avant son dénouement.</p>
                        </section>
                        <section>
-                               <h4 id="article_5_5">Article 5.5 : Actions administrateur</h4>
+                               <header>
+                                       <h4 id="article_5_5">Article 5.5 : Actions administrateur</h4>
+                               </header>
                                <p>L'Administrateur peut modifier l'horaire, déplacer, annuler, annuler pour pluie, annuler de force, attribuer, auto attribuer et verrouiller une séance.</p>
                                <p>En cas d'attribution ou d'annulation forcée, le score de la réservation en question est positionnée à 42.</p>
                                <p>Chaque action administrateur devra être dûment justifiée sous peine de perte du dit statut.</p>
                        </section>
                </section>
                <section>
-                       <h3 id="article_6">Article 6 : Services attribution et connexes</h3>
+                       <header>
+                               <h3 id="article_6">Article 6 : Services attribution et connexes</h3>
+                       </header>
                        <p>Le Service Attribution favorise l'organisation sur l'ensemble des lieux de danse accessibles aux danseurs de tango et maintient leur disponibilité sans défavoriser les Organisateurs qui assurent les créneaux moins favorables.</p>
                        <p>Les Services Connexes sont conçu pour faciliter l'expérience utilisateur.</p>
                        <section>
-                               <h4 id="article_6_1">Article 6.1 : Service attribution</h4>
+                               <header>
+                                       <h4 id="article_6_1">Article 6.1 : Service attribution</h4>
+                               </header>
                                <p>Une tâche automatisée attribue la séance, à partir de 4 (quatre) jours avant son début, à un Organisateur l'ayant réservée, en fonction des critères suivants. Elle s'exécutera toutes les 5 (cinq) minutes avec un délai initial aléatoire.</p>
                                <p>La réservation de l'Organisateur sans aucune séance prise en compte ou à défaut avec la plus ancienne sur le même lieu sera retenue.</p>
                                <p>En cas d'égalité, la réservation de l'Organisateur sans aucune séance prise en compte ou à défaut avec la plus ancienne sur l'ensemble des lieux sera retenue.</p>
                                <p>En cas d'égalité, la réservation de l'Organisateur avec le plus d'ancienneté sera retenue.</p>
                                <p>Pour ce faire, un score est calculé pour les séances sur le même lieu et un autre sur tous les lieux, ci-après « le Score », seul le premier est consigné lors de l'attribution sur chaque réservation prise en compte.</p>
                                <section>
-                                       <h5 id="article_6_1_1">Article 6.1.1 : Score</h5>
+                                       <header>
+                                               <h5 id="article_6_1_1">Article 6.1.1 : Score</h5>
+                                       </header>
                                        <p>Pour chaque réservation, le Score est calculé lors de l'attribution de la séance.</p>
                                        <p>Pour chaque séance prise en compte de l'Organisateur un ratio est calculé, ci-après « le Ratio », le Score en est la somme.</p>
                                        <p>Le Ratio est l'inverse de la distance, en nombre de jour, entre le début de la séance réservée et celle attribuée.</p>
                                        <p>Soit somme (1 / valeur absolue (date séance réservée – date séance réalisée)).</p>
                                </section>
                                <section>
-                                       <h5 id="article_6_1_2">Article 6.1.2 : Périmètre</h5>
+                                       <header>
+                                               <h5 id="article_6_1_2">Article 6.1.2 : Périmètre</h5>
+                                       </header>
                                        <p>Seules sont prises en compte pour le calcul du score les séances :</p>
                                        <ul>
                                                <li>non verrouillées ;</li>
                                        </ul>
                                </section>
                                <section>
-                                       <h5 id="article_6_1_3">Article 6.1.3 : Premium</h5>
+                                       <header>
+                                               <h5 id="article_6_1_3">Article 6.1.3 : Premium</h5>
+                                       </header>
                                        <p>Les séances en après-midi les samedi, dimanche et jours fériés sont considérées comme premium.</p>
                                        <p>Les séances en soirée le vendredi, samedi et veille de jours fériés sont considérées comme premium.</p>
                                </section>
                                <section>
-                                       <h5 id="article_6_1_4">Article 6.1.4 : Hotspot</h5>
+                                       <header>
+                                               <h5 id="article_6_1_4">Article 6.1.4 : Hotspot</h5>
+                                       </header>
                                        <p>L'Opéra Garnier et les Quais sont considérés en raison de leur affluence naturelle comme des hotspots.</p>
                                </section>
                                <section>
-                                       <h5 id="article_6_1_5">Article 6.1.5 : Délais de grâce</h5>
+                                       <header>
+                                               <h5 id="article_6_1_5">Article 6.1.5 : Délais de grâce</h5>
+                                       </header>
                                        <p>Pour une séance réservée moins de 4 (quatre) jours avant son début, le processus d'attribution commencera après un délais supplémentaire égal au 1/4 (quart) de la période entre la première réservation et le début.</p>
                                        <p>L'Invité avec une séance déjà attribuée sur le même lieu moins de 30 (trente) jours avant, verra sa réservation prise en compte seulement dans les 2 (deux) jours avant le début de la séance demandée.</p>
                                        <p>Le Régulier réservant une séance premium sur un hotspot, verra sa réservation prise en compte seulement dans les 3 (trois) jours avant le début de la séance demandée.</p>
                                </section>
                        </section>
                        <section>
-                               <h4 id="article_6_2">Article 6.2 : Services connexes</h4>
+                               <header>
+                                       <h4 id="article_6_2">Article 6.2 : Services connexes</h4>
+                               </header>
                                <p>Une tâche automatisée relève sur un site météo, puis consolide sur les séances, dans la mesure du possible, les prévisions de température, température ressentie, risque de pluie et pluviométrie.</p>
                                <p>Un service d'envoi de courriel pourra être mis en place pour informer des attributions.</p>
                                <p>Un service d'envoi de courriel pourra être mis en place pour informer des séances attribuées à un Organisateur suivi.</p>
                        </section>
                </section>
-       </section>
+       </article>
 {% endblock %}
index b94e180991f73deb22c3ab982a2d1cf260355f24..90047c338cb7f5e92afb050b0e97bd2656d48f0b 100644 (file)
@@ -1,7 +1,7 @@
-{% extends '@RapsysAir/body.html.twig' %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
        <section id="regulation">
-               <h2><a href="{{ path('rapsys_air_regulation') }}">{{ section }}</a></h2>
+               <h2><a href="{{ path('rapsysair_regulation') }}">{{ section }}</a></h2>
                {#XXX: use https://www.londonschool.com/level-scale/#}
                <section>
                        <h3>{% trans %}Guest{% endtrans %}</h3>
index 5559635ed669cc1b995c88a67c16a9a6fe18d41c..ffe3950154e78a5eda78bd9f8d0aeb6dae275bd9 100644 (file)
@@ -1,12 +1,13 @@
-{% extends '@RapsysAir/body.html.twig' %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
-       <section id="dashboard">
+       <article>
                <header>
-                       <h2><a href="{{ path('rapsys_air_terms_of_service') }}">{{ title }}</a></h2>
-                       <p>{{ description }}</p>
+                       <h2>{{ description }}</h2>
                </header>
                <nav>
-                       <strong>{% trans %}Table of contents{% endtrans %}</strong>
+                       <header>
+                               <h3>{% trans %}Table of contents{% endtrans %}</h3>
+                       </header>
                        <ul>
                                <li><a href="#preamble">{% trans %}Preamble{% endtrans %}</a></li>
                                <li><a href="#article1">Article 1 : Présentation</a></li>
@@ -32,7 +33,9 @@
                        </ul>
                </nav>
                <section>
-                       <h3 id="preamble">{% trans %}Preamble{% endtrans %}</h3>
+                       <header>
+                               <h3 id="preamble">{% trans %}Preamble{% endtrans %}</h3>
+                       </header>
                        <p>Préalablement à la visite du site <a href="//airlibre.eu/" title="Air Libre">airlibre.eu</a> (ci-après dénommé « le Site ») et de l'utilisation des services qui y sont proposés, il convient de procéder à la lecture des conditions générales d'utilisations ci-dessous exposées.</p>
                        <p>Ces conditions générales ont pour objet de définir les modalités de mise à disposition du service de réservation d'espace en plein air, du service d'affichage, du service d'attribution, des services connexes sur le Site et les conditions d'utilisation de ces derniers par l'utilisateur. Tout accès au Site ou toute utilisation de ces services suppose le respect préalable de l'ensemble des dispositions ci-dessous ainsi que leur acceptation inconditionnelle.</p>
                        <p>Les conditions générales d'utilisation pourront être modifiées en début d'année, début d'été et fin d'été par la personne en charge du Site, ci-après dénommé « le Responsable », après consultation des utilisateurs et référendum pour toute modification substantielle.</p>
                        <p>Le préambule fait partie des conditions générales d'utilisation.</p>
                </section>
                <section>
-                       <h3 id="article1">Article 1 : Présentation</h3>
+                       <header>
+                               <h3 id="article1">Article 1 : Présentation</h3>
+                       </header>
                        <p>Le service d'affichage du Site est ouvert à toute personne souhaitant consulter les séances de Tango Argentin attribuées à un organisateur sur les espaces de danse public, ci-après « le Service Affichage ».</p>
                        <p>Le service réservation du Site est ouvert à toute personne ayant la volonté d'assurer une séance de Tango Argentin sur un espace de danse en public tant au niveau artistique, logistique, réglementaire et sans exclure l'aspect sécurité, ci-après « le Service Réservation ».</p>
                        <p>Le service d'attribution du Site assure une répartition équitable entre les différents organisateurs des séances sur un créneau et un lieu, ci-après « le Service Attribution ».</p>
                        <p>Les services connexes sont mis à disposition pour améliorer l'expérience utilisateur du Site.</p>
                </section>
                <section>
-                       <h3 id="article2">Article 2 : Accès, disponibilité et utilisation</h3>
+                       <header>
+                               <h3 id="article2">Article 2 : Accès, disponibilité et utilisation</h3>
+                       </header>
                        <section>
-                               <h4 id="article2_1">Article 2.1 : Accès</h4>
+                               <header>
+                                       <h4 id="article2_1">Article 2.1 : Accès</h4>
+                               </header>
                                <p>Le Site est accessible gratuitement, via le réseau Internet, à toute personne souhaitant consulter le service d'affichage d'affichage des réservations, ci-après dénommée « l'Utilisateur ».</p>
                                <p>Le Site est accessible contre participation aux frais de fonctionnement annuels, via le réseau Internet, à toute personne souhaitant utiliser le service de réservation, ci-après dénommée « l'Organisateur ».</p>
                                <p>Le Responsable ne peut être tenu responsable des dysfonctionnements du réseau et des serveurs ainsi que des événements échappant au contrôle raisonnable qui empêcheraient ou dégraderaient l'accès au service.</p>
                                <p>Néanmoins, le Responsable se réserve le droit, pour des raisons de maintenance, de compléter, changer ou effacer tous les contenus du Site.</p>
                        </section>
                        <section>
-                               <h4 id="article2_2">Article 2.2 : Disponibilité</h4>
+                               <header>
+                                       <h4 id="article2_2">Article 2.2 : Disponibilité</h4>
+                               </header>
                                <p>Le Responsable propose un service disponible 7 jours sur 7 et 24 heures sur 24.</p>
                                <p>Le Responsable pourra cependant et à tout moment suspendre, interrompre ou annuler la mise à disposition du service auprès des utilisateurs sans que sa responsabilité ne puisse être engagée dans les cas suivants :</p>
                                <ul>
@@ -68,7 +79,9 @@
                                </ul>
                        </section>
                        <section>
-                               <h4 id="article2_3">Article 2.3 : Utilisation</h4>
+                               <header>
+                                       <h4 id="article2_3">Article 2.3 : Utilisation</h4>
+                               </header>
                                <p>L'utilisation du service suppose que l'Utilisateur :</p>
                                <ul>
                                        <li>possède un accès Internet dont les coûts éventuels sont à sa charge, ainsi que le matériel et logiciel requis ;</li>
                                </ul>
                        </section>
                        <section>
-                               <h4 id="article2_4">Article 2.4 : Dates d'accessibilité</h4>
+                               <header>
+                                       <h4 id="article2_4">Article 2.4 : Dates d'accessibilité</h4>
+                               </header>
                                <p>Le service sera accessible aux Utilisateurs à partir du 1 janvier 2021 et ce pour une durée de 12 (douze) mois, soit jusqu'au 31 décembre 2021 à 23h59m59s, renouvelable par tacite reconduction.</p>
                                <p>Le service sera accessible aux Organisateurs à partir du 1 janvier 2021, après réception de leur participation aux frais de fonctionnement et ce pour une durée de 12 (douze) mois, soit jusqu'au 31 décembre 2021 à 23h59m59s, renouvelable par tacite reconduction sur participation aux frais de fonctionnement pour la nouvelle période.</p>
                        </section>
                        <section>
-                               <h4 id="article2_5">Article 2.5 : Configuration minimale requise</h4>
+                               <header>
+                                       <h4 id="article2_5">Article 2.5 : Configuration minimale requise</h4>
+                               </header>
                                <p>Un ordinateur PC ou MAC ayant une connexion internet et un navigateur :</p>
                                <ul>
                                        <li>Chrome 86 ;</li>
                        </section>
                </section>
                <section>
-                       <h3 id="article3">Article 3 : Connexion</h3>
+                       <header>
+                               <h3 id="article3">Article 3 : Connexion</h3>
+                       </header>
                        <p>Pour se connecter à la plateforme, l'Utilisateur ou l'Organisateur devra se rendre sur le Site <a href="//airlibre.eu/" title="Air Libre">airlibre.eu</a>.</p>
                        <p>Pour réserver l'Organisateur devra accepter le cookie technique nécessaire aux services connectés puis s'identifier à l'aide de son adresse de courriel et de son mot de passe personnel.</p>
                </section>
                <section>
-                       <h3 id="article4">Article 4 : Exclusion du site par le Responsable</h3>
+                       <header>
+                               <h3 id="article4">Article 4 : Exclusion du site par le Responsable</h3>
+                       </header>
                        <p>Le responsable se réserve le droit de procéder au blocage définitif de tout accès au Site à l'encontre de tout Utilisateur ou Organisateur qui contreviendrait aux présentes conditions générales d'utilisation et notamment qui :</p>
                        <ul>
                                <li>se livrerait à une tentative et/ou à un acte de contournement des mesures techniques de protection des différents services ;</li>
                        </ul>
                </section>
                <section>
-                       <h3 id="article5">Article 5 : Procédure d'alerte et signalement d'abus</h3>
+                       <header>
+                               <h3 id="article5">Article 5 : Procédure d'alerte et signalement d'abus</h3>
+                       </header>
                        <p>Les Utilisateurs sont invités à signaler au Responsable, dans le cadre de la procédure d'alerte ci-dessous décrite, toute utilisation frauduleuse du Site dont il aurait connaissance et notamment tout message dont le contenu contreviendrait aux présentes conditions générales d'utilisation ou plus généralement aux lois et dispositions réglementaires en vigueur.</p>
                        <p>De même, toute personne estimant qu'il y a violation sur le Site d'un droit dont il serait titulaire a la possibilité de le signaler au Responsable (conformément à l'article 6-1-5 de la loi du 21 juin 2004 n°2004-575) par le formulaire de contact, précisant l'ensemble des informations suivantes :</p>
                        <ul>
                        </ul>
                </section>
                <section>
-                       <h3 id="article6">Article 6 : Liens hypertexte</h3>
+                       <header>
+                               <h3 id="article6">Article 6 : Liens hypertexte</h3>
+                       </header>
                        <p>Le Site propose des liens hypertextes vers des sites Internet édités et/ou gérés par des tiers sans que sa responsabilité ne puisse toutefois être engagée à quelque titre que ce soit en raison de leur contenu et notamment des publicités, produits, services et/ou tout autre matériel disponible sur et à partir de ces sites tiers.</p>
                </section>
                <section>
-                       <h3 id="article7">Article 7 : Modification du site et des services</h3>
+                       <header>
+                               <h3 id="article7">Article 7 : Modification du site et des services</h3>
+                       </header>
                        <p>Le Responsable se réserve le droit de faire évoluer le service notamment par la mise à disposition de nouvelles fonctionnalités ou par la modification et/ou la suppression de fonctionnalités à ce jour proposées à l'utilisateur à partir du Site.</p>
                </section>
                <section>
-                       <h3 id="article8">Article 8 : Propriété intellectuelle</h3>
+                       <header>
+                               <h3 id="article8">Article 8 : Propriété intellectuelle</h3>
+                       </header>
                        <p>Le Responsable est propriétaire exclusif des droits de propriété intellectuelle sur le Site aussi bien sur sa structure que sur son contenu. Toute reproduction, représentation, modification ou exploitation qu'elles soient intégrales ou partielles, de quelque façon que ce soit et à quelque fin que ce soit, faites sans le consentement du Responsable est illicite et constitue une contrefaçon sanctionnée par les articles L. 335-2 et suivants du Code de la propriété intellectuelle.</p>
                        <p>Les marques du Responsable et des partenaires du Responsable ainsi que les logos figurant sur le site sont des marques déposées. Toute reproduction totale ou partielle de ces marques ou de ces logos, sans le consentement du Responsable ou de ses partenaires est prohibée.</p>
                </section>
                <section>
-                       <h3 id="article9">Article 9 : Responsabilité</h3>
+                       <header>
+                               <h3 id="article9">Article 9 : Responsabilité</h3>
+                       </header>
                        <p>Le Responsable ne saurait être tenu pour responsable vis-à-vis de l'Utilisateur ou de l'Organisateur des conséquences liées à l'utilisation du contenu ou de tout autre élément du service et notamment :</p>
                        <ul>
                                <li>en cas d'utilisation illégale et contrevenante par les Utilisateurs ou Organisateurs des œuvres de l'esprit reproduites sur le Site ;</li>
                        <p>En l'absence de faute de sa part, le Responsable ne pourra être tenu responsable si le Service proposé par le Site s'avère incompatible ou présente des dysfonctionnements avec certains logiciels, configurations, systèmes d'exploitation ou équipements de l'Utilisateur ou l'Organisateur.</p>
                </section>
                <section>
-                       <h3 id="article10">Article 10 : Durée</h3>
+                       <header>
+                               <h3 id="article10">Article 10 : Durée</h3>
+                       </header>
                        <p>Les présentes conditions générales d'utilisation ont vocation à s'appliquer pendant toute la durée d'accessibilité au Service soit à partir du 1er janvier 2021 et ce pour une durée de 12 (douze) mois, soit jusqu'au 31 décembre 2021 à 23h59m59s, renouvelable par tacite reconduction.</p>
                </section>
                <section>
-                       <h3 id="article11">Article 11 : Droit applicable et juridiction compétente</h3>
+                       <header>
+                               <h3 id="article11">Article 11 : Droit applicable et juridiction compétente</h3>
+                       </header>
                        <p>L'intégralité des dispositions des présentes conditions générales d'utilisation est régie par le droit français.</p>
                        <p>Les parties s'efforceront de résoudre à l'amiable tout litige qui surviendrait à l'occasion de l'exécution des présentes. En cas de désaccord définitif, les Tribunaux de Paris seront seuls compétents.</p>
                </section>
-       </section>
+       </article>
 {% endblock %}
index ed9522024fe8eb3e4253354f8a019bcf69b64c3c..16a81371fadff9dc89b62b7de13c6ee082545971 100644 (file)
@@ -1,7 +1,7 @@
-{% extends '@RapsysAir/body.html.twig' %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
 <section id="dashboard">
-       <h2><a href="{{ path('rapsys_air') }}">{{ section }}</a></h2>
+       <h2><a href="{{ path('rapsysair') }}">{{ section }}</a></h2>
        <p class="message error">{% if message %}{{ message }}{% else %}{% trans %}Error{% endtrans %}{% endif %}</p>
        {# {% if app.environment == 'dev' and trace is defined %}<pre class="trace">{{ trace }}</pre>{% endif %} #}
        {% if trace is defined and trace %}<pre class="trace">{{ trace }}</pre>{% endif %}
index 82fe510652c81d145350768561cccd72968affaa..eb84c6060e19699197433832412c042ef2cf9ef2 100644 (file)
@@ -1,5 +1,7 @@
 <section>
-       <h2>{% trans %}Application{% endtrans %}</h2>
+       <header>
+               <h3>{% trans %}Application{% endtrans %}</h3>
+       </header>
        {{ form_start(forms.application) }}
                <div>
                        {% if forms.application.user is defined %}
index 6d8795be6490d5cc5ebd1bf6e2383e590db0ee5d..3824640a5f1c354377773992cc00252a075fa8c0 100644 (file)
@@ -4,7 +4,7 @@
                <div>
                        {{ form_row(forms.location.title) }}
 
-                       {{ form_row(forms.location.short) }}
+                       {{ form_row(forms.location.description) }}
 
                        {{ form_row(forms.location.address) }}
 
@@ -16,6 +16,8 @@
 
                        {{ form_row(forms.location.longitude) }}
 
+                       {{ form_row(forms.location.indoor) }}
+
                        {{ form_row(forms.location.hotspot) }}
 
                        {{ form_row(forms.location.submit) }}
index c62cb8d770a8ba9b7873ad514fec47b674ea21f3..9fb8fada8cf65c784c2aa290604f3efd8739fd20 100644 (file)
@@ -1,5 +1,5 @@
 <section>
-       <h2><a href="{{ path('rapsys_user_login') }}">{% trans %}Login{% endtrans %}</a></h2>
+       <h2><a href="{{ path('rapsysuser_login') }}">{% trans %}Login{% endtrans %}</a></h2>
        {{ form_start(forms.login) }}
                <div>
                        {{ form_row(forms.login.mail) }}
index 6fa28a44f70a9ffe31c2b6388b974f5095786996..1c17feed4eaaa8437db1cb3816c0f876fc0a60bb 100644 (file)
@@ -1,5 +1,5 @@
 <section>
-       <h2><a href="{{ path('rapsys_user_register') }}">{% trans %}Register{% endtrans %}</a></h2>
+       <h2><a href="{{ path('rapsysuser_register') }}">{% trans %}Register{% endtrans %}</a></h2>
        {{ form_start(forms.register) }}
                <div>
                        {% if forms.register.mail is defined %}
index 0d23a3647d174e1c6ca412d417408a74ce5c8cd9..48aceab3fad678e599f98aaf593442762d6e932e 100644 (file)
@@ -1,9 +1,19 @@
 {% if forms.session.modify is defined or forms.session.move is defined or forms.session.cancel is defined or forms.session.raincancel is defined or forms.session.forcecancel is defined or is_granted('ROLE_ADMIN') %}
        <section>
-               <h2>{% trans %}Modify{% endtrans %}</h2>
+               <header>
+                       <h3>{% trans %}Modify{% endtrans %}</h3>
+               </header>
                {{ form_start(forms.session) }}
                        {% if forms.session.modify is defined %}
                                <div>
+                                       {% if forms.session.dance is defined %}
+                                               {{ form_row(forms.session.dance) }}
+                                       {% endif %}
+
+                                       {% if forms.session.slot is defined %}
+                                               {{ form_row(forms.session.slot) }}
+                                       {% endif %}
+
                                        {% if forms.session.date is defined %}
                                                {{ form_row(forms.session.date) }}
                                        {% endif %}
index ff124d5c0166e95679fc0da84a279a27a19755c2..dfc46be39553fa480acaac4c9bdfc293a842e2c5 100644 (file)
@@ -1,9 +1,8 @@
-{% extends '@RapsysAir/body.html.twig' %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
-       <section id="form">
+       <article>
                <header>
-                       <h2><a href="{{ path('rapsys_air_contact') }}">{{ title }}</a></h2>
-                       <p>{{ description }}</p>
+                       <h2>{{ description }}</h2>
                </header>
                {% if sent %}
                        <p class="message notice">{% trans %}Your message has been sent{% endtrans %}</p>
@@ -18,6 +17,8 @@
 
                                        {{ form_row(form.message) }}
 
+                                       {{ form_row(form.captcha) }}
+
                                        {{ form_row(form.submit) }}
                                </div>
 
@@ -25,5 +26,5 @@
                                <footer style="display:none">{{ form_rest(form) }}</footer>
                        {{ form_end(form) }}
                {% endif %}
-       </section>
+       </article>
 {% endblock %}
index 2f7286a644ea229170b159169a04e110f36a107d..608dbdbe2397e31618036ef38fdc21983a440564 100644 (file)
@@ -1,8 +1,9 @@
-{% extends '@RapsysAir/body.html.twig' %}
-{% block title %}{{ site.title }} - {{ title }}{% endblock %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
-       <section id="form">
-               <h2><a href="{{ app.request.requesturi }}">{{ title }}</a></h2>
+       <article>
+               <header>
+                       <h2><a href="{{ app.request.requesturi }}">{{ title.page }}</a></h2>
+               </header>
                {{ form_start(edit) }}
                        <div>
                                {% if edit.mail is defined %}
                                        {{ form_row(edit.pseudonym) }}
                                {% endif %}
 
-                               {% if edit.slug is defined %}
-                                       {{ form_row(edit.slug) }}
+                               {% if edit.zipcode is defined %}
+                                       {{ form_row(edit.zipcode) }}
+                               {% endif %}
+
+                               {% if edit.city is defined %}
+                                       {{ form_row(edit.city) }}
+                               {% endif %}
+
+                               {% if edit.country is defined %}
+                                       {{ form_row(edit.country) }}
                                {% endif %}
 
                                {% if edit.phone is defined %}
                                        {{ form_row(edit.phone) }}
                                {% endif %}
 
+                               {% if edit.dances is defined %}
+                                       {{ form_row(edit.dances) }}
+                               {% endif %}
+
+                               {% if edit.subscriptions is defined %}
+                                       {{ form_row(edit.subscriptions) }}
+                               {% endif %}
+
                                {{ form_row(edit.submit) }}
                        </div>
 
                        {# Render CSRF token etc .#}
                        <footer style="display:none">{{ form_rest(edit) }}</footer>
                {{ form_end(edit) }}
-       </section>
+       </article>
        {% if reset is defined %}
-               <section id="form">
-                       <h2><a href="{{ app.request.requesturi }}">{{ password }}</a></h2>
+               <article>
+                       <header>
+                               <h2><a href="{{ app.request.requesturi }}">{{ password }}</a></h2>
+                       </header>
                        {{ form_start(reset) }}
                                <div>
                                        {% if reset.password is defined %}
                                {# Render CSRF token etc .#}
                                <footer style="display:none">{{ form_rest(reset) }}</footer>
                        {{ form_end(reset) }}
-               </section>
+               </article>
+       {% endif %}
+       {% if calendar is defined %}
+               <article>
+                       <header>
+                               <h2><a href="{{ app.request.requesturi }}">{% trans %}Calendar{% endtrans %}</a></h2>
+                       </header>
+                       {% if calendar.link is defined %}
+                               <section>
+                                       <header>
+                                               <h3>{% trans %}Add{% endtrans %}</h3>
+                                       </header>
+                                       <div>
+                                               <a class="center" href="{{ calendar.link }}"><img src="{{ asset(calendar.logo.svg) }}?20221024100144" srcset="{{ asset(calendar.logo.png) }}?20221024100144 200w, {{ asset(calendar.logo.svg) }}?20221024100144 400w" sizes="(min-width:400px) 400px, 200px" alt="{% trans %}Google Calendar logo{% endtrans %}" width="50" height="50" /><span>{% trans %}Link a Google Calendar account{% endtrans %}</span></a>
+                                       </div>
+                               </section>
+                       {% endif %}
+                       {% if calendar.form is defined %}
+                               {% for mail, form in calendar.form %}
+                                       <section>
+                                               <header>
+                                                       <h3>{{ mail|ucfirst }}</h3>
+                                               </header>
+                                               {{ form_start(form) }}
+                                                       <div>
+                                                               {{ form_widget(form.calendar) }}
+
+                                                               <div class="row">
+                                                                       {{ form_widget(form.submit) }}
+
+                                                                       {% if form.refresh is defined %}
+                                                                               {{ form_widget(form.refresh) }}
+                                                                       {% endif %}
+
+                                                                       {% if form.add is defined %}
+                                                                               {{ form_widget(form.add) }}
+                                                                       {% endif %}
+
+                                                                       {% if form.delete is defined %}
+                                                                               {{ form_widget(form.delete) }}
+                                                                       {% endif %}
+
+                                                                       {% if form.unlink is defined %}
+                                                                               {{ form_widget(form.unlink) }}
+                                                                       {% endif %}
+                                                               </div>
+                                                       </div>
+
+                                                       {# Render CSRF token etc .#}
+                                                       <footer style="display:none">{{ form_rest(form) }}</footer>
+                                               {{ form_end(form) }}
+                                       </section>
+                               {% endfor %}
+                       {% endif %}
+               </article>
        {% endif %}
+       <article>
+               <header>
+                       <h2><a href="{{ app.request.requesturi }}">{% trans %}Newsletter{% endtrans %}</a></h2>
+               </header>
+               <p class="center"><span>TODO: add newsletter stuff here ?</span></p>
+       </article>
 {% endblock %}
index 34af91ef39b57b7196fa67bd340de17c6f7ad527..61c8a03593e14762c9146b2644621fce9f6624c7 100644 (file)
 {%- endblock choice_widget -%}
 
 {%- block choice_widget_expanded -%}
-    <div {{ block('widget_container_attributes') }}>
     {%- for child in form %}
-        {{- form_widget(child) -}}
-        {{- form_label(child, null, {translation_domain: choice_translation_domain}) -}}
+               <div class="row" {{ block('widget_container_attributes') }}>
+                       {{- form_label(child, null, {translation_domain: choice_translation_domain}) -}}
+                       <div>
+                               {{- form_widget(child) -}}
+                       </div>
+               </div>
     {% endfor -%}
-    </div>
 {%- endblock choice_widget_expanded -%}
 
 {%- block choice_widget_collapsed -%}
         {%- endif -%}
         <{{ element|default('label') }}{% if label_attr %}{% with { attr: label_attr } %}{{ block('attributes') }}{% endwith %}{% endif %}>
             {%- if translation_domain is same as(false) -%}
-                {{- label -}}
+                               {%- if label_html is same as(false) -%}
+                                       {{- label -}}
+                               {%- else -%}
+                                       {{- label|raw -}}
+                               {%- endif -%}
             {%- else -%}
-                {{- label|trans({}, translation_domain) -}}
+                               {%- if label_html is same as(false) -%}
+                                       {{- label|trans({}, translation_domain) -}}
+                               {%- else -%}
+                                       {{- label|trans({}, translation_domain)|raw -}}
+                               {%- endif -%}
             {%- endif -%}
         </{{ element|default('label') }}>
     {%- endif -%}
 {%- endblock form_row -%}
 
 {%- block button_row -%}
-    <div>
+    <div class="row">
         {{- form_widget(form) -}}
     </div>
 {%- endblock button_row -%}
index 927c24370000e09b3eb3896f7b1807a6f45ac0b0..54855c327feca97a2cfabcc30f626e17113f6687 100644 (file)
@@ -1,8 +1,9 @@
-{% extends '@RapsysAir/body.html.twig' %}
-{% block title %}{{ site.title }} - {{ title }}{% endblock %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
-       <section id="form">
-               <h2><a href="{{ path('rapsys_user_login') }}">{% trans %}Login{% endtrans %}</a></h2>
+       <article id="form">
+               <header>
+                       <h2><a href="{{ path('rapsysuser_login') }}">{% trans %}Login{% endtrans %}</a></h2>
+               </header>
                {{ form_start(login) }}
                        <div>
                                {{ form_row(login.mail) }}
                        {# Render CSRF token etc .#}
                        <footer style="display:none">{{ form_rest(login) }}</footer>
                {{ form_end(login) }}
-       </section>
+       </article>
        {% if recover is defined %}
-               <section id="recover">
-                       <h2><a href="{{ path('rapsys_user_recover') }}">{% trans %}Recover{% endtrans %}</a></h2>
+               <article id="recover">
+                       <header>
+                               <h2><a href="{{ path('rapsysuser_recover') }}">{% trans %}Recover{% endtrans %}</a></h2>
+                       </header>
                        {{ form_start(recover) }}
 
                                <div>
@@ -30,6 +33,6 @@
                                {# Render CSRF token etc .#}
                                <footer style="display:none">{{ form_rest(recover) }}</footer>
                        {{ form_end(recover) }}
-               </section>
+               </article>
        {% endif %}
 {% endblock %}
index ac5eee45b8a126f2bd8d5e6f19cef4b1c958b02d..7307248e7f83d22c6a4efbc7f2bdc4d2c9735457 100644 (file)
@@ -1,27 +1,28 @@
-{% extends '@RapsysAir/body.html.twig' %}
-{% block title %}{{ site.title }} - {{ title }}{% endblock %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
-       <section id="form">
-               <h2><a href="{{ path('rapsys_user_recover') }}">{% trans %}Recover{% endtrans %}</a></h2>
+       <article>
+               <header>
+                       <h2><a href="{{ path('rapsysuser_recover') }}">{% trans %}Recover{% endtrans %}</a></h2>
+               </header>
                {% if sent %}
                        <p class="message notice">{% trans %}Your recover account message has been sent{% endtrans %}</p>
                {% else %}
-                       {{ form_start(form) }}
+                       {{ form_start(recover) }}
                                <div>
-                                       {% if form.mail is defined %}
-                                               {{ form_row(form.mail) }}
+                                       {% if recover.mail is defined %}
+                                               {{ form_row(recover.mail) }}
                                        {% endif %}
 
-                                       {% if form.password is defined %}
-                                               {{ form_row(form.password) }}
+                                       {% if recover.password is defined %}
+                                               {{ form_row(recover.password) }}
                                        {% endif %}
 
-                                       {{ form_row(form.submit) }}
+                                       {{ form_row(recover.submit) }}
                                </div>
 
                                {# Render CSRF token etc .#}
-                               <footer style="display:none">{{ form_rest(form) }}</footer>
-                       {{ form_end(form) }}
+                               <footer style="display:none">{{ form_rest(recover) }}</footer>
+                       {{ form_end(recover) }}
                {% endif %}
-       </section>
+       </article>
 {% endblock %}
index 5ab1758655380db7af6b714ca42ce64cfcc08a4c..74873f82aa40705600f092c2a984adacbbbe2f54 100644 (file)
@@ -1,8 +1,7 @@
-{% extends '@RapsysAir/body.html.twig' %}
-{% block title %}{{ site.title }} - {{ title }}{% endblock %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
        <section id="form">
-               <h2><a href="{{ path('rapsys_user_recover') }}">{% trans %}Recover{% endtrans %}</a></h2>
+               <h2><a href="{{ path('rapsysuser_recover') }}">{% trans %}Recover{% endtrans %}</a></h2>
                {% if sent %}
                        <p class="message notice">{% trans %}Your password is updated and an account recover message has been sent{% endtrans %}</p>
                {% elseif not found %}
index 914671c39eebe51dc3846750d11c7c7ae0a00f97..cd71a78dc88a7b416ec0b6f85ecc280a139ea517 100644 (file)
@@ -1,56 +1,57 @@
-{% extends '@RapsysAir/body.html.twig' %}
-{% block title %}{{ site.title }} - {{ title }}{% endblock %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
-       <section id="form">
-               <h2><a href="{{ path('rapsys_user_register') }}">{{ title }}</a></h2>
+       <article>
+               <header>
+                       <h2><a href="{{ path('rapsysuser_register') }}">{{ title.page }}</a></h2>
+               </header>
                {% if disabled is defined and disabled %}
                        <p class="message error">{% trans %}Your account has been disabled{% endtrans %}</p>
                {% elseif sent is defined and sent %}
                        <p class="message notice">{% trans %}Your verification mail has been sent, to activate your account you must follow the confirmation link inside{% endtrans %}</p>
                        <p class="message warning">{% trans %}If you did not receive a verification mail, check your Spam or Junk mail folders{% endtrans %}</p>
                {% else %}
-                       {{ form_start(form) }}
-                               <div>
-                                       {% if form.mail is defined %}
-                                               {{ form_row(form.mail) }}
-                                       {% endif %}
+                       {{ form_start(register) }}
+                               {% if register.mail is defined %}
+                                       {{ form_row(register.mail) }}
+                               {% endif %}
 
-                                       {% if form.password is defined %}
-                                               {{ form_row(form.password.first) }}
+                               {% if register.password is defined %}
+                                       {{ form_row(register.password) }}
+                               {% endif %}
 
-                                               {{ form_row(form.password.second) }}
-                                       {% endif %}
+                               {% if register.civility is defined %}
+                                       {{ form_row(register.civility) }}
+                               {% endif %}
 
-                                       {% if form.civility is defined %}
-                                               {{ form_row(form.civility) }}
-                                       {% endif %}
+                               {% if register.forename is defined %}
+                                       {{ form_row(register.forename) }}
+                               {% endif %}
 
-                                       {% if form.forename is defined %}
-                                               {{ form_row(form.forename) }}
-                                       {% endif %}
+                               {% if register.surname is defined %}
+                                       {{ form_row(register.surname) }}
+                               {% endif %}
 
-                                       {% if form.surname is defined %}
-                                               {{ form_row(form.surname) }}
-                                       {% endif %}
+                               {% if register.pseudonym is defined %}
+                                       {{ form_row(register.pseudonym) }}
+                               {% endif %}
 
-                                       {% if form.pseudonym is defined %}
-                                               {{ form_row(form.pseudonym) }}
-                                       {% endif %}
+                               {% if register.slug is defined %}
+                                       {{ form_row(register.slug) }}
+                               {% endif %}
 
-                                       {% if form.slug is defined %}
-                                               {{ form_row(form.slug) }}
-                                       {% endif %}
+                               {% if register.phone is defined %}
+                                       {{ form_row(register.phone) }}
+                               {% endif %}
 
-                                       {% if form.phone is defined %}
-                                               {{ form_row(form.phone) }}
-                                       {% endif %}
+                               {% if register.captcha is defined %}
+                                       {{ form_row(register.captcha) }}
+                               {% endif %}
 
-                                       {{ form_row(form.submit) }}
-                               </div>
+                               {{ form_row(register.submit) }}
 
                                {# Render CSRF token etc .#}
-                               <footer style="display:none">{{ form_rest(form) }}</footer>
-                       {{ form_end(form) }}
+                               <footer style="display:none">{{ form_rest(register) }}</footer>
+                       {{ form_end(register) }}
                {% endif %}
-       </section>
+       </article>
 {% endblock %}
diff --git a/Resources/views/location/add.html.twig b/Resources/views/location/add.html.twig
deleted file mode 100644 (file)
index d0c18e8..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-{% extends '@RapsysAir/body.html.twig' %}
-{% block content %}
-       <section id="form">
-               <h2>{{ section }}</h2>
-               {{ form_start(form) }}
-                       <div>
-                               {{ form_row(form.title) }}
-
-                               {{ form_row(form.short) }}
-
-                               {{ form_row(form.address) }}
-
-                               {{ form_row(form.zipcode) }}
-
-                               {{ form_row(form.city) }}
-
-                               {{ form_row(form.latitude) }}
-
-                               {{ form_row(form.longitude) }}
-
-                               {{ form_row(form.hotspot) }}
-
-                               {{ form_row(form.submit) }}
-                       </div>
-
-                       {# render csrf token etc .#}
-                       <footer style="display:none">{{ form_rest(form) }}</footer>
-               {{ form_end(form) }}
-       </section>
-{% endblock %}
diff --git a/Resources/views/location/cities.html.twig b/Resources/views/location/cities.html.twig
new file mode 100644 (file)
index 0000000..80701cb
--- /dev/null
@@ -0,0 +1,39 @@
+{% extends '@RapsysAir/base.html.twig' %}
+{% block content %}
+       <article>
+               <header>
+                       <h2>{{ description }}</h2>
+               </header>
+               <div class="panel">
+                       {% if cities is defined and cities %}
+                               <div class="grid">
+                                       {% for id, city in cities %}
+                                               <article>
+                                                       <header>
+                                                               <h3><a href="{{ city.link }}">{{ city.city }} ({{ city.id }})</a></h3>
+                                                       </header>
+                                                       <div class="panel">
+                                                               {% if city.multimap is defined and city.multimap %}
+                                                                       <div class="multimap">
+                                                                               <a href="{{ city.multimap.link }}" title="{{ city.multimap.caption }}">
+                                                                                       <figure>
+                                                                                               <img src="{{ city.multimap.src }}" alt="{{ city.multimap.caption }}" width="{{ city.multimap.width }}" height="{{ city.multimap.height }}" />
+                                                                                               <figcaption>{{ city.multimap.caption }}</figcaption>
+                                                                                       </figure>
+                                                                               </a>
+                                                                       </div>
+                                                               {% endif %}
+                                                               <ul class="grid">
+                                                                       {% for id, location in city.locations %}
+                                                                               <li><a href="{{ location.link }}">{% if city.multimap is defined and city.multimap %}{{ id }} {% endif %}{{ location.title }}</a></li>
+                                                                       {% endfor %}
+                                                               </ul>
+                                                       </div>
+                                               </article>
+                                       {% endfor %}
+                               </div>
+                       {% endif %}
+                       {{ include('@RapsysAir/form/_toolbox.html.twig') }}
+               </div>
+       </article>
+{% endblock %}
diff --git a/Resources/views/location/city.html.twig b/Resources/views/location/city.html.twig
new file mode 100644 (file)
index 0000000..d7f53a8
--- /dev/null
@@ -0,0 +1,46 @@
+{% extends '@RapsysAir/base.html.twig' %}
+{% block content %}
+       <article id="dashboard">
+               <header>
+                       <h2>{{ description }}</h2>
+               </header>
+               <div class="panel">
+                       {% if calendar is defined and calendar %}
+                               <div class="grid calendar seven">
+                                       {% for date, day in calendar %}
+                                               <section class="{{ day.class|join(' ') }}">
+                                                       <header>
+                                                               <h3>{{ day.title }}</h3>
+                                                       </header>
+                                                       {% if day.sessions is not empty %}
+                                                               <ul>
+                                                                       {% for session in day.sessions %}
+                                                                               <li class="{{ session.class|join(' ') }}">
+                                                                                       <a href="{{ session.link }}" title="{{ session.title }}">
+                                                                                               <span>{{ session.start|intldate('none', 'short') }}</span>
+                                                                                               <span class="reducible">{{ session.location.title }}</span>
+                                                                                               <span class="temperature"{% if session.temperature.title is defined and session.temperature.title %} title="{{ session.temperature.title }}"{% endif %}><span class="glyph">{{ session.temperature.glyph }}</span></span>
+                                                                                               <span>{{ session.stop|intldate('none', 'short') }}</span>
+                                                                                               <span class="reducible">{% if session.application.user.title is defined and session.application.user.title %}{{ session.application.user.title }}{% endif %}</span>
+                                                                                               <span class="rain"{% if session.rain.title is defined and session.rain.title %} title="{{ session.rain.title }}"{% endif %}><span class="glyph">{{ session.rain.glyph }}</span></span>
+                                                                                               {% if session.rate is defined and session.rate %}
+                                                                                                       <span class="rate" title="{{ session.rate.title }}">{% if session.rate.rate is defined and session.rate.rate %}{{ session.rate.rate }} {% endif %}<span class="glyph">{{ session.rate.glyph }}</span></span>
+                                                                                               {% else %}
+                                                                                                       <span></span>
+                                                                                               {% endif %}
+                                                                                               <span class="reducible">{{ session.location.zipcode }} {{ session.location.city }}</span>
+                                                                                               <span></span>
+                                                                                       </a>
+                                                                               </li>
+                                                                       {% endfor %}
+                                                               </ul>
+                                                       {% endif %}
+                                               </section>
+                                       {% endfor %}
+                               </div>
+                       {% endif %}
+                       {{ include('@RapsysAir/form/_toolbox.html.twig') }}
+               </div>
+       </article>
+       {{ include('@RapsysAir/default/_location.html.twig') }}
+{% endblock %}
index 817fa397d5b84391872220faba773c3b089589b5..0b850155f73b9aedf12eb15f53723518a9813f32 100644 (file)
@@ -1,44 +1,67 @@
-{% extends '@RapsysAir/body.html.twig' %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
-       <article id="dashboard">
+       <article id="dashboard" class="location">
                <header>
-                       <h2><a href="{{ path('rapsys_air_location') }}">{% trans %}Locations{% endtrans %}</a></h2>
-                       <p>{% trans %}Libre Air location list{% endtrans %}</p>
+                       <h2>{{ description }}</h2>
                </header>
                <div class="panel">
-                       <div class="grid four location">
-                               {% for id, title in locations %}
-                                       <article class="cell">
-                                               <h3><a href="{{ path('rapsys_air_location_view', {'id': id}) }}">{{ title }}</a></h3>
-                                               {% if forms.locations is defined and forms.locations[id] is defined and forms.locations[id] %}
-                                                       {{ form_start(forms.locations[id]) }}
-                                                               <div>
-                                                                       {{ form_row(forms.locations[id].title) }}
+                       {% if locations is defined and locations %}
+                               {% if multimap is defined and multimap %}
+                                       <div class="multimap">
+                                               <a href="{{ multimap.link }}" title="{{ multimap.caption }}">
+                                                       <figure>
+                                                               <img src="{{ multimap.src }}" alt="{{ multimap.caption }}"{# width="{{ multimap.width }}" height="{{ multimap.height }}" #} />
+                                                               <figcaption>{{ multimap.caption }}</figcaption>
+                                                       </figure>
+                                               </a>
+                                       </div>
+                               {% endif %}
+                               {% if forms.locations is defined %}
+                                       <div class="grid{% if locations|length > 1%} two{% endif %}">
+                                               {% for id, location in locations %}
+                                                       <article class="cell">
+                                                               <header>
+                                                                       <h3>{% if multimap is defined and multimap %}{{ id }} {% endif %}<a href="{{ location.link }}" title="{{ location.title }}">{{ location.title }}</a></h3>
+                                                               </header>
+                                                               {% if forms.locations[id] is defined and forms.locations[id] %}
+                                                                       {{ form_start(forms.locations[id]) }}
+                                                                               <div>
+                                                                                       {{ form_row(forms.locations[id].title) }}
 
-                                                                       {{ form_row(forms.locations[id].short) }}
+                                                                                       {{ form_row(forms.locations[id].description) }}
 
-                                                                       {{ form_row(forms.locations[id].address) }}
+                                                                                       {{ form_row(forms.locations[id].address) }}
 
-                                                                       {{ form_row(forms.locations[id].zipcode) }}
+                                                                                       {{ form_row(forms.locations[id].zipcode) }}
 
-                                                                       {{ form_row(forms.locations[id].city) }}
+                                                                                       {{ form_row(forms.locations[id].city) }}
 
-                                                                       {{ form_row(forms.locations[id].latitude) }}
+                                                                                       {{ form_row(forms.locations[id].latitude) }}
 
-                                                                       {{ form_row(forms.locations[id].longitude) }}
+                                                                                       {{ form_row(forms.locations[id].longitude) }}
 
-                                                                       {{ form_row(forms.locations[id].hotspot) }}
+                                                                                       {{ form_row(forms.locations[id].indoor) }}
 
-                                                                       {{ form_row(forms.locations[id].submit) }}
-                                                               </div>
+                                                                                       {{ form_row(forms.locations[id].hotspot) }}
 
-                                                               {# render csrf token etc .#}
-                                                               <footer style="display:none">{{ form_rest(forms.locations[id]) }}</footer>
-                                                       {{ form_end(forms.locations[id]) }}
-                                               {% endif %}
-                                       </article>
-                               {% endfor %}
-                       </div>
+                                                                                       {{ form_row(forms.locations[id].submit) }}
+                                                                               </div>
+
+                                                                               {# render csrf token etc .#}
+                                                                               <footer style="display:none">{{ form_rest(forms.locations[id]) }}</footer>
+                                                                       {{ form_end(forms.locations[id]) }}
+                                                               {% endif %}
+                                                       </article>
+                                               {% endfor %}
+                                       </div>
+                               {% else %}
+                                       <ul class="grid{% if locations|length > 1%} two{% endif %}">
+                                               {% for id, location in locations %}
+                                                       <li>{% if multimap is defined and multimap %}{{ id }} {% endif %}<a href="{{ location.link }}" title="{{ location.title }}">{{ location.title }}</a></li>
+                                               {% endfor %}
+                                       </ul>
+                               {% endif %}
+                       {% endif %}
                        {{ include('@RapsysAir/form/_toolbox.html.twig') }}
                </div>
        </article>
index 4c5508b84b2522e9ebfe2f9f8d6831c43798c1aa..636fbcb846853db19d8f60fb6bc9ebb15dc2f2c9 100644 (file)
@@ -1,34 +1,35 @@
-{% extends '@RapsysAir/body.html.twig' %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
-       <article id="dashboard">
+       <article>
                <header>
-                       <h2><a href="{{ path('rapsys_air_location_view', {'id': id}) }}">{{ section }}</a></h2>
-                       <p>{{ description }}</p>
+                       <h2>{{ description }}</h2>
                </header>
                <div class="panel">
                        {% if calendar is defined and calendar %}
                                <div class="grid calendar seven">
                                        {% for date, day in calendar %}
                                                <section class="{{ day.class|join(' ') }}">
-                                                       <h3>{{ day.title }}</h3>
+                                                       <header>
+                                                               <h3>{{ day.title }}</h3>
+                                                       </header>
                                                        {% if day.sessions is not empty %}
                                                                <ul>
                                                                        {% for session in day.sessions %}
                                                                                <li class="{{ session.class|join(' ') }}">
-                                                                                       <a href="{{ path('rapsys_air_session_view', {'id': session.id}) }}" title="{{ session.applications|join('\n') }}">
-                                                                                               <span>{{ session.start|localizeddate('none', 'short') }}</span>
-                                                                                               <span class="reducible">{{ session.location }}</span>
-                                                                                               <span class="info">
-                                                                                                       {% if session.weather is defined and session.weather %}
-                                                                                                               <span title="{{ session.weathertitle }}">{{ session.weather }}</span>
-                                                                                                       {% endif %}
-                                                                                                       <span title="{{ session.slottitle }}">{{ session.slot }}</span>
-                                                                                               </span>
-                                                                                               <span>{{ session.stop|localizeddate('none', 'short') }}</span>
-                                                                                               {% if session.pseudonym is defined and session.pseudonym %}
-                                                                                                       <span class="reducible{% if session.rate is not defined and session.hat is not defined %} pseudonym{% endif %}">{{ session.pseudonym }}</span>
+                                                                                       <a href="{{ session.link }}" title="{{ session.title }}">
+                                                                                               <span>{{ session.start|intldate('none', 'short') }}</span>
+                                                                                               <span class="reducible">{{ session.location.title }}</span>
+                                                                                               <span class="temperature"{% if session.temperature.title is defined and session.temperature.title %} title="{{ session.temperature.title }}"{% endif %}><span class="glyph">{{ session.temperature.glyph }}</span></span>
+                                                                                               <span>{{ session.stop|intldate('none', 'short') }}</span>
+                                                                                               <span class="reducible">{% if session.application.user.title is defined and session.application.user.title %}{{ session.application.user.title }}{% endif %}</span>
+                                                                                               <span class="rain"{% if session.rain.title is defined and session.rain.title %} title="{{ session.rain.title }}"{% endif %}><span class="glyph">{{ session.rain.glyph }}</span></span>
+                                                                                               {% if session.rate is defined and session.rate %}
+                                                                                                       <span class="rate" title="{{ session.rate.title }}">{% if session.rate.rate is defined and session.rate.rate %}{{ session.rate.rate }} {% endif %}<span class="glyph">{{ session.rate.glyph }}</span></span>
+                                                                                               {% else %}
+                                                                                                       <span></span>
                                                                                                {% endif %}
-                                                                                               {% if session.rate is defined %}<span class="info">{{ session.rate }} {% if session.hat is defined and session.hat %}🎩{% else %}€{% endif %}</span>{% endif %}
+                                                                                               <span class="reducible">{{ session.location.zipcode }} {{ session.location.city }}</span>
+                                                                                               <span></span>
                                                                                        </a>
                                                                                </li>
                                                                        {% endfor %}
similarity index 81%
rename from Resources/views/mail/body.html.twig
rename to Resources/views/mail/base.html.twig
index d7aec971477f30d0a1cf8fd0d3689251039600ca..ef4bad1a7bc93deca25ebc6b036092515c98e05c 100644 (file)
@@ -1,5 +1,5 @@
 {% extends '@RapsysAir/base.html.twig' %}
-{% block stylesheets %}
+{% block stylesheet %}
        <meta name="viewport" content="width=device-width" />
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        {% if locale is defined and locale %}<meta http-equiv="Content-Language" content="{{ locale }}" />{% endif %}
@@ -20,8 +20,8 @@
 {% block body %}
        <table class="header">
                <tr>
-                       <td><a href="{{ site.url|escape('html_attr') }}"><img src="{{ email.image(site.logo) }}" alt="{{ site.title|escape('html_attr') }}" /></a></td>
-                       <td><h1><a href="{{ site.url|escape('html_attr') }}">{{ site.title }}</a></h1></td>
+                       <td><a href="{{ root|escape('html_attr') }}"><img src="{{ email.image(logo.png) }}" alt="{{ title.site|escape('html_attr') }}" /></a></td>
+                       <td><h1><a href="{{ root|escape('html_attr') }}">{{ title.site }}</a></h1></td>
                </tr>
        </table>
         {% block content %}{% endblock %}
index 67cd520931dfa18869cf1af38a08625523ac63ac..2a9f97185ac3eb1d1c8e3a14e7552e215baa2a22 100644 (file)
@@ -1,4 +1,4 @@
-{% extends '@RapsysAir/mail/body.html.twig' %}
+{% extends '@RapsysAir/mail/base.html.twig' %}
 {% block title %}{{ subject }}{% endblock %}
 {% block content %}
        <table class="content">
index e62dcc73d24c144403a8b70bf3eebdefc7f70360..95bdf8c7338c6265e68773a4476f2db569ffe036 100644 (file)
@@ -1,4 +1,4 @@
-{% extends '@RapsysAir/mail/body.html.twig' %}
+{% extends '@RapsysAir/mail/base.html.twig' %}
 {% block title %}{{ subject }}{% endblock %}
 {% block content %}
        <table class="content">
@@ -6,7 +6,7 @@
                        <td>&nbsp;</td>
                        <td>
                                <h2>{{ 'Subject: %subject%'|trans({'%subject%': subject})|raw }}</h2>
-                               <p>{{ 'Thanks so much for rejoining %site.title%, the space reservation program.'|trans({'%site.title%': site.title}) }}</p>
+                               <p>{{ 'Thanks so much for rejoining %title.site%, the space reservation program.'|trans({'%title.site%': title.site}) }}</p>
                                <p>{% trans %}To recover your account follow this link:{% endtrans %}</p>
                                <p><a href="{{ recover_url|escape('html_attr') }}" class="link">{{ recover_url }}</a></p>
                        </td>
index eb567d7e2f4e236a5dd9a15d4330d35213cd5eac..b7cbfff44ca8199c63e422636f21bf2ee11f4fc8 100644 (file)
@@ -2,7 +2,7 @@
 {% for i in range(1, subject|length) %}={% endfor %}
 
 
-*{{ 'Thanks so much for rejoining %site.title%, the space reservation program.'|trans({'%site.title%': site.title}) }}*
+*{{ 'Thanks so much for rejoining %title.site%, the space reservation program.'|trans({'%title.site%': title.site}) }}*
 
 {% trans %}To recover your account follow this link:{% endtrans %}
 {{ recover_url }}
index 3bdcb906e9164fe3363f511eace1626b68bb68c8..611ac5c84baa647fdf15ad5ead41cc3318234b40 100644 (file)
@@ -1,12 +1,12 @@
-{% extends '@RapsysAir/mail/body.html.twig' %}
+{% extends '@RapsysAir/mail/base.html.twig' %}
 {% block title %}{{ subject }}{% endblock %}
 {% block content %}
        <table class="content">
                <tr>
                        <td>&nbsp;</td>
                        <td>
-                               <h2>{{ 'Subject: %subject%'|trans({'%subject%': 'welcome back to %site.title%'|trans({'%site.title%': site.title})|raw}) }}</h2>
-                               <p>{{ 'Thanks so much for rejoining %site.title%, the space reservation program.'|trans({'%site.title%': site.title}) }}</p>
+                               <h2>{{ 'Subject: %subject%'|trans({'%subject%': 'welcome back to %title.site%'|trans({'%title.site%': title.site})|raw}) }}</h2>
+                               <p>{{ 'Thanks so much for rejoining %title.site%, the space reservation program.'|trans({'%title.site%': title.site}) }}</p>
                                <p>{{ 'Your account password has been changed, to recover your account you can follow this link: '|trans }}</p>
                                <p><a href="{{ recover_url|escape('html_attr') }}">{{ recover_url }}</a></p>
                        </td>
index b5e09eab41eacb0c736041701a2926e4f2c1ea6c..6c2fe94580fd310eec290af02832ab34bb6003bd 100644 (file)
@@ -2,7 +2,7 @@
 {% for i in range(1, subject|length) %}={% endfor %}
 
 
-*{{ 'Thanks so much for rejoining %site.title%, the space reservation program.'|trans({'%site.title%': site.title}) }}*
+*{{ 'Thanks so much for rejoining %title.site%, the space reservation program.'|trans({'%title.site%': title.site}) }}*
 
 {{ 'Your account password has been changed, to recover your account you can follow this link: %recover_url%'|trans({'%recover_url%': recover_url}) }}
 
index 0659b9f3eef9fbfea134a7202d8bd24e03e768ef..7b293fc2211324dc4855af4bf41aa80a92b3ae3a 100644 (file)
@@ -1,13 +1,12 @@
-{% extends '@RapsysAir/mail/body.html.twig' %}
-{% block title %}{{ subject }}{% endblock %}
+{% extends '@RapsysAir/mail/base.html.twig' %}
 {% block content %}
        <table class="content">
                <tr>
                        <td>&nbsp;</td>
                        <td>
-                               <h2>{{ 'Subject: %subject%'|trans({'%subject%': 'welcome to %site.title%'|trans({'%site.title%': site.title})|raw}) }}</h2>
+                               <h2>{{ 'Subject: %subject%'|trans({'%subject%': 'welcome to %title.site%'|trans({'%title.site%': title.site})|raw}) }}</h2>
                                <h3>{% if recipient_name %}{{ 'Hi %recipient_name%,'|trans({'%recipient_name%': recipient_name}) }}{% else %}{% trans %}Hi,{% endtrans %}{% endif %}</h3>
-                               <p>{{ 'Thanks so much for joining %site.title%, the space reservation program.'|trans({'%site.title%': site.title}) }}</p>
+                               <p>{{ 'Thanks so much for joining %title.site%, the space reservation program.'|trans({'%title.site%': title.site}) }}</p>
                                <p>{% trans %}To create your account you must follow this link:{% endtrans %}</p>
                                <p><a href="{{ confirm_url|escape('html_attr') }}" class="link">{{ confirm_url }}</a></p>
                        </td>
index f50b97aca3a170c5b62796358b683d70d2d54c32..23faead900341c1e7414e466f6b631a1b94000b3 100644 (file)
@@ -2,7 +2,7 @@
 {% for i in range(1, subject|length) %}={% endfor %}
 
 
-*{{ 'Thanks so much for joining %site.title%, the space reservation program.'|trans({'%site.title%': site.title}) }}*
+*{{ 'Thanks so much for joining %title.site%, the space reservation program.'|trans({'%title.site%': title.site}) }}*
 
 {% trans %}To create your account you must follow this link:{% endtrans %}
 {{ confirm_url }}
index 053a832be21048cf41c943d4db713eb81b208a2f..706c4dea5cf95f2cac03bcbdf579cb062e87cff1 100644 (file)
@@ -1,7 +1,7 @@
-{% extends '@RapsysAir/body.html.twig' %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
 <section id="content">
-       <h2><a href="{{ path('rapsys_air') }}">{{ title }}</a></h2>
+       <h2><a href="{{ path('rapsysair') }}">{{ title.page }}</a></h2>
        <p>{% if message is defined %}{{ message }}{% else %}{% trans %}Access denied{% endtrans %}{% endif %}</p>
        {% if is_granted('ROLE_ADMIN') %}{% if trace is defined %}<pre>{{ trace }}</pre>{% endif %}{% endif %}
 </section>
index 6a9abfb2891f4ea089a832d01e421322d0511850..066c03a52824136c60be02c4876d7720d328fbd6 100644 (file)
@@ -1,10 +1,10 @@
-{% extends '@RapsysAir/body.html.twig' %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
        <section id="form">
                <header>
                        <h2>
-                               <a href="{{ path('rapsys_air_session_view', {'id': session.id}) }}">{{ session.title }}</a>
-                               <a href="{{ path('rapsys_air_location_view', {'id': session.location.id}) }}">{{ session.location.at }}</a>
+                               <a href="{{ path('rapsysair_session_view', {'id': session.id}) }}">{{ session.title }}</a>
+                               <a href="{{ path('rapsysair_location_view', {'id': session.location.id}) }}">{{ session.location.at }}</a>
                        </h2>
                        <p>{{ description }}</p>
                </header>
index 9d6e2a6a33f73949e75714385e3ea65d3c2eb27a..9c478790110ab01c2d0f418bc0357870ef992f06 100644 (file)
@@ -1,31 +1,36 @@
-{% extends '@RapsysAir/body.html.twig' %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
-       <article id="dashboard">
-               <h2><a href="{{ path('rapsys_air_session') }}">{{ section }}</a></h2>
+       <article>
+               <header>
+                       <h2>{{ title.page }}</h2>
+                       <p>{{ description }}</p>
+               </header>
                <div class="panel">
                        {% if calendar is defined and calendar %}
                                <div class="grid calendar seven">
                                        {% for date, day in calendar %}
                                                <section class="{{ day.class|join(' ') }}">
-                                                       <h3>{{ day.title }}</h3>
+                                                       <header>
+                                                               <h3>{{ day.title }}</h3>
+                                                       </header>
                                                        {% if day.sessions is not empty %}
                                                                <ul>
                                                                        {% for session in day.sessions %}
                                                                                <li class="{{ session.class|join(' ') }}">
-                                                                                       <a href="{{ path('rapsys_air_session_view', {'id': session.id}) }}" title="{{ session.applications|join('\n') }}">
-                                                                                               <span>{{ session.start|localizeddate('none', 'short') }}</span>
-                                                                                               <span class="reducible">{{ session.location }}</span>
-                                                                                               <span class="info">
-                                                                                                       {% if session.weather is defined and session.weather %}
-                                                                                                               <span title="{{ session.weathertitle }}">{{ session.weather }}</span>
-                                                                                                       {% endif %}
-                                                                                                       <span title="{{ session.slottitle }}">{{ session.slot }}</span>
-                                                                                               </span>
-                                                                                               <span>{{ session.stop|localizeddate('none', 'short') }}</span>
-                                                                                               {% if session.pseudonym is defined and session.pseudonym %}
-                                                                                                       <span class="reducible{% if session.rate is not defined and session.hat is not defined %} pseudonym{% endif %}">{{ session.pseudonym }}</span>
+                                                                                       <a href="{{ session.link }}" title="{{ session.title }}">
+                                                                                               <span>{{ session.start|intldate('none', 'short') }}</span>
+                                                                                               <span class="reducible">{{ session.location.title }}</span>
+                                                                                               <span class="temperature"{% if session.temperature.title is defined and session.temperature.title %} title="{{ session.temperature.title }}"{% endif %}><span class="glyph">{{ session.temperature.glyph }}</span></span>
+                                                                                               <span>{{ session.stop|intldate('none', 'short') }}</span>
+                                                                                               <span class="reducible">{% if session.application.user.title is defined and session.application.user.title %}{{ session.application.user.title }}{% endif %}</span>
+                                                                                               <span class="rain"{% if session.rain.title is defined and session.rain.title %} title="{{ session.rain.title }}"{% endif %}><span class="glyph">{{ session.rain.glyph }}</span></span>
+                                                                                               {% if session.rate is defined and session.rate %}
+                                                                                                       <span class="rate" title="{{ session.rate.title }}">{% if session.rate.rate is defined and session.rate.rate %}{{ session.rate.rate }} {% endif %}<span class="glyph">{{ session.rate.glyph }}</span></span>
+                                                                                               {% else %}
+                                                                                                       <span></span>
                                                                                                {% endif %}
-                                                                                               {% if session.rate is defined %}<span class="info">{{ session.rate }} {% if session.hat is defined and session.hat %}🎩{% else %}€{% endif %}</span>{% endif %}
+                                                                                               <span class="reducible">{{ session.location.zipcode }} {{ session.location.city }}</span>
+                                                                                               <span></span>
                                                                                        </a>
                                                                                </li>
                                                                        {% endfor %}
index 02df6db5460bf7d3125ccf68e804ded92eb88ecd..3c345f95408f5ac9fca139f64ea91f09685554e4 100644 (file)
-{% extends '@RapsysAir/body.html.twig' %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
-       <article id="dashboard"{% if session.application.canceled is defined and session.application.canceled %} class="canceled"{% endif %}>
+       <article class="session{% if session.locked is defined and session.locked %} locked{% elseif session.application.canceled is defined and session.application.canceled %} canceled{% endif %}">
                <header>
-                       <h2>
-                               <a href="{{ path('rapsys_air_session_view', {'id': session.id}) }}">{{ session.title }}</a>
-                               {% if session.application.id is defined and session.application.id %}
-                                       <a href="{{ path('rapsys_air_user_view', {'id': session.application.user.id}) }}">{{ session.application.user.by }}</a>
-                               {% endif %}
-                               <a href="{{ path('rapsys_air_location_view', {'id': session.location.id}) }}">{{ session.location.at }}</a>
-                       </h2>
+                       <h2>{{ title.page }}</h2>
                        <p>{{ description }}</p>
                </header>
                <div class="panel">
-                       <div class="grid four">
+                       <div class="grid three">
                                <section class="cell">
-                                       <h3>{% trans %}Organizer{% endtrans %}</h3>
+                                       <header>
+                                               <h3>{% trans %}Program{% endtrans %}</h3>
+                                       </header>
                                        <dl>
-                                               <dt>{% trans %}Attributed to{% endtrans %}</dt>
-                                               <dd>
-                                                       {% if session.application is null %}
-                                                               {% trans %}None{% endtrans %}
-                                                       {% else %}
-                                                               <a href="{{ path('rapsys_air_user_view', {'id': session.application.user.id}) }}">{{ session.application.user.title }}</a>
-                                                       {% endif %}
-                                               </dd>
+                                               <dt>{% trans %}Date and schedule{% endtrans %}</dt>
+                                               <dd>{{ 'The %date% around %start% until %stop%'|trans({'%date%': session.start|intldate('long', 'none'), '%start%': session.start|intldate('none', 'medium'), '%stop%': session.stop|intldate('none', 'medium')}) }}</dd>
                                        </dl>
-                                       {% if session.snippet.description is defined and session.snippet.description %}
-                                               <dl>
-                                                       <dt>{% trans %}Description{% endtrans %}</dt>
-                                                       <dd>{{ session.snippet.description|striptags|markdown_to_html }}</dd>
-                                               </dl>
-                                       {% endif %}
-                                       {% if session.snippet.class is defined and session.snippet.class %}
-                                               <dl>
-                                                       <dt>{% trans %}Class{% endtrans %}</dt>
-                                                       <dd>{{ session.snippet.class|striptags|markdown_to_html }}</dd>
-                                               </dl>
-                                       {% endif %}
-                                       {% if session.snippet.contact is defined and session.snippet.contact %}
-                                               <dl>
-                                                       <dt>{% trans %}Contact{% endtrans %}</dt>
-                                                       <dd><a href="{{ session.snippet.contact }}">{{ 'Send a message to %pseudonym%'|trans({'%pseudonym%': session.application.user.title}) }}</a></dd>
-                                               </dl>
-                                       {% endif %}
-                                       {% if session.snippet.donate is defined and session.snippet.donate %}
-                                               <dl>
-                                                       <dt>{% trans %}Donate{% endtrans %}</dt>
-                                                       <dd><a href="{{ session.snippet.donate }}">{{ 'Donate to %pseudonym%'|trans({'%pseudonym%': session.application.user.title}) }}</a></dd>
-                                               </dl>
-                                       {% endif %}
-                                       {% if session.snippet.link is defined and session.snippet.link %}
-                                               <dl>
-                                                       <dt>{% trans %}Link{% endtrans %}</dt>
-                                                       <dd><a href="{{ session.snippet.link }}">{{ 'Link to %pseudonym%'|trans({'%pseudonym%': session.application.user.title}) }}</a></dd>
-                                               </dl>
-                                       {% endif %}
-                                       {% if session.snippet.profile is defined and session.snippet.profile %}
-                                               <dl>
-                                                       <dt>{% trans %}Social network{% endtrans %}</dt>
-                                                       <dd><a href="{{ session.snippet.profile }}">{{ 'Consult %pseudonym% profile'|trans({'%pseudonym%': session.application.user.title}) }}</a></dd>
-                                               </dl>
-                                       {% endif %}
-                                       {% if session.snippet.rate is defined and session.snippet.rate %}
-                                               {% if session.snippet.hat is defined and session.snippet.hat %}
+                                       {% if session.application is defined and session.application %}
+                                               {% if session.application.dance is defined and session.application.dance %}
                                                        <dl>
-                                                               <dt>{% trans %}Contribution{% endtrans %}</dt>
-                                                               <dd>{% if session.snippet.rate == 0 %}{% trans %}To the hat, to cover the costs: talc, electricity, bicycle, server{% endtrans %}{% else %}{{ 'To the hat, ideally %rate% €, to cover the costs: talc, electricity, bicycle, server'|trans({'%rate%': session.snippet.rate}) }}{% endif %}</dd>
+                                                               <dt>{% trans %}Activity{% endtrans %}</dt>
+                                                               {#<dd>{{ session.application.dance.title }}</dd>#}
+                                                               <dd><a href="{{ session.application.dance.link }}">{{ session.application.dance.title }}</a></dd>
                                                        </dl>
-                                               {% else %}
+                                               {% endif %}
+                                               {% if session.application.user is defined and session.application.user %}
                                                        <dl>
-                                                               <dt>{% trans %}Rate{% endtrans %}</dt>
-                                                               <dd>{% if session.snippet.rate == 0 %}{% trans %}Free{% endtrans %}{% else %}{{ session.snippet.rate }} €{% endif %}</dd>
+                                                               <dt>{% trans %}Organizer{% endtrans %}</dt>
+                                                               <dd><a href="{{ session.application.user.link }}">{{ session.application.user.title }}</a></dd>
                                                        </dl>
                                                {% endif %}
+                                               {% if session.snippet is defined and session.snippet %}
+                                                       {% if session.snippet.description is defined and session.snippet.description %}
+                                                               <dl>
+                                                                       <dt>{% trans %}Description{% endtrans %}</dt>
+                                                                       <dd>{{ session.snippet.description|striptags|markdown_to_html }}</dd>
+                                                               </dl>
+                                                       {% endif %}
+                                                       {% if session.snippet.class is defined and session.snippet.class %}
+                                                               <dl>
+                                                                       <dt>{% trans %}Class{% endtrans %}</dt>
+                                                                       <dd>{{ session.snippet.class|striptags|markdown_to_html }}</dd>
+                                                               </dl>
+                                                       {% endif %}
+                                                       {% if session.snippet.contact is defined and session.snippet.contact %}
+                                                               <dl>
+                                                                       <dt>{% trans %}Contact{% endtrans %}</dt>
+                                                                       <dd><a href="{{ session.application.user.contact }}">{{ 'Send a message to %pseudonym%'|trans({'%pseudonym%': session.application.user.title}) }}</a></dd>
+                                                               </dl>
+                                                       {% endif %}
+                                                       {% if session.snippet.donate is defined and session.snippet.donate %}
+                                                               <dl>
+                                                                       <dt>{% trans %}Donate{% endtrans %}</dt>
+                                                                       <dd><a href="{{ session.snippet.donate }}">{{ 'Donate to %pseudonym%'|trans({'%pseudonym%': session.application.user.title}) }}</a></dd>
+                                                               </dl>
+                                                       {% endif %}
+                                                       {% if session.snippet.link is defined and session.snippet.link %}
+                                                               <dl>
+                                                                       <dt>{% trans %}Link{% endtrans %}</dt>
+                                                                       <dd><a href="{{ session.snippet.link }}">{{ 'Link to %pseudonym%'|trans({'%pseudonym%': session.application.user.title}) }}</a></dd>
+                                                               </dl>
+                                                       {% endif %}
+                                                       {% if session.snippet.profile is defined and session.snippet.profile %}
+                                                               <dl>
+                                                                       <dt>{% trans %}Social network{% endtrans %}</dt>
+                                                                       <dd><a href="{{ session.snippet.profile }}">{{ 'Consult %pseudonym% profile'|trans({'%pseudonym%': session.application.user.title}) }}</a></dd>
+                                                               </dl>
+                                                       {% endif %}
+                                                       {% if session.snippet.rate is defined and session.snippet.rate %}
+                                                               {% if session.snippet.hat is defined and session.snippet.hat %}
+                                                                       <dl>
+                                                                               <dt>{% trans %}Contribution to costs{% endtrans %}</dt>
+                                                                               <dd>{% if session.snippet.rate == 0 %}{% trans %}To the hat, to cover: talc, electricity, bicycle, website, ...{% endtrans %}{% else %}{{ 'To the hat, ideally %rate% €, to cover: talc, electricity, bicycle, website, ...'|trans({'%rate%': session.snippet.rate}) }}{% endif %}</dd>
+                                                                       </dl>
+                                                               {% else %}
+                                                                       <dl>
+                                                                               <dt>{% trans %}Contribution{% endtrans %}</dt>
+                                                                               <dd>{% if session.snippet.rate == 0 %}{% trans %}Free{% endtrans %}{% else %}{{ session.snippet.rate }} €{% endif %}</dd>
+                                                                       </dl>
+                                                               {% endif %}
+                                                       {% endif %}
+                                               {% endif %}
+                                       {% endif %}
+                                       {% if session.locked is defined and session.locked %}
+                                               <dl>
+                                                       <dt>{% trans %}Locked{% endtrans %}</dt>
+                                                       <dd>{{ session.locked|intldate('long', 'medium') }}</dd>
+                                               </dl>
                                        {% endif %}
-                               </section>
-                               <section class="cell">
-                                       <h3>{% trans %}Schedule{% endtrans %}</h3>
                                        <dl>
-                                               <dt>{% trans %}Date{% endtrans %}</dt>
-                                               <dd>{{ session.start|localizeddate('long', 'none') }}</dd>
+                                               <dt>{% trans %}Created{% endtrans %}</dt>
+                                               <dd>{{ session.created|intldate('long', 'medium') }}</dd>
                                        </dl>
                                        <dl>
-                                               <dt>{% trans %}Start{% endtrans %}</dt>
-                                               <dd>{{ session.start|localizeddate('none', 'medium') }}</dd>
+                                               <dt>{% trans %}Updated{% endtrans %}</dt>
+                                               <dd>{{ session.updated|intldate('long', 'medium') }}</dd>
                                        </dl>
+                               </section>
+                               <section class="cell">
+                                       <header>
+                                               <h3>{% trans %}Location{% endtrans %}</h3>
+                                       </header>
                                        <dl>
-                                               <dt>{% trans %}Stop{% endtrans %}</dt>
-                                               <dd>{{ session.stop|localizeddate('none', 'medium') }}</dd>
+                                               <dt>{% trans %}Place{% endtrans %}</dt>
+                                               <dd><a href="{{ session.location.link }}">{{ session.location.title }}</a></dd>
                                        </dl>
                                        <dl>
-                                               <dt>{% trans %}Length{% endtrans %}</dt>
-                                               <dd>{{ session.length|localizeddate('none', 'short', null, null, 'HH:mm') }}</dd>
+                                               <dt>{% trans %}Description{% endtrans %}</dt>
+                                               <dd>{{ session.location.description }}</dd>
                                        </dl>
                                        <dl>
-                                               <dt>{% trans %}Slot{% endtrans %}</dt>
-                                               <dd>{{ session.slot.title }}</dd>
+                                               <dt>{% trans %}Interiority{% endtrans %}</dt>
+                                               <dd>{% if session.location.indoor is defined and session.location.indoor%}{% trans %}Indoor{% endtrans %}{% else %}{% trans %}Outdoor{% endtrans %}{% endif %}</dd>
                                        </dl>
                                        <dl>
-                                               <dt>{% trans %}Locked{% endtrans %}</dt>
+                                               <dt>{% trans %}Address{% endtrans %}</dt>
                                                <dd>
-                                                       {% if session.locked is null %}
-                                                               {% trans %}None{% endtrans %}
-                                                       {% else %}
-                                                               {{ session.locked|localizeddate('long', 'medium') }}
-                                                       {% endif %}
+                                                       {{ session.location.address }}
+                                                       {{ session.location.zipcode }} {{ session.location.city }}
                                                </dd>
                                        </dl>
                                        <dl>
-                                               <dt>{% trans %}Created{% endtrans %}</dt>
-                                               <dd>{{ session.created|localizeddate('long', 'medium') }}</dd>
+                                               <dt>{% trans %}GPS coordinates{% endtrans %}</dt>
+                                               <dd>
+                                                       {{ session.location.latitude }},{{ session.location.longitude }}
+                                               </dd>
                                        </dl>
                                        <dl>
-                                               <dt>{% trans %}Updated{% endtrans %}</dt>
-                                               <dd>{{ session.updated|localizeddate('long', 'medium') }}</dd>
+                                               <dt>{% trans %}Maps{% endtrans %}</dt>
+                                               <dd>
+                                                       <a href="https://www.google.fr/maps/@{{ session.location.latitude }},{{ session.location.longitude }},19z">Google Maps</a>
+                                               </dd>
+                                               <dd>
+                                                       <a href="https://www.openstreetmap.org/#map=19/{{ session.location.latitude }}/{{ session.location.longitude }}">OpenStreetMap</a>
+                                               </dd>
                                        </dl>
+                                       {% if map is defined and map %}
+                                               <dl class="map">
+                                                       <dt>{% trans %}Access map{% endtrans %}</dt>
+                                                       <dd>
+                                                               <a href="{{ map.link }}" title="{{ map.caption }}">
+                                                                       <figure>
+                                                                               <img src="{{ map.src }}" alt="{{ map.caption }}" width="{{ map.width }}" height="{{ map.height }}" />
+                                                                               <figcaption>{{ map.caption }}</figcaption>
+                                                                       </figure>
+                                                               </a>
+                                                       </dd>
+                                               </dl>
+                                       {% endif %}
                                </section>
                                <section class="cell">
-                                       <h3>{% trans %}Weather{% endtrans %}</h3>
+                                       <header>
+                                               <h3>{% trans %}Weather{% endtrans %}</h3>
+                                       </header>
                                        {% if session.rainrisk is not null %}
                                                <dl>
                                                        <dt>{% trans %}Rainrisk{% endtrans %}</dt>
                                                </dl>
                                        {% endif %}
                                </section>
-                               <section class="cell">
-                                       <h3>{% trans %}Location{% endtrans %}</h3>
-                                       <dl>
-                                               {# infos #}
-                                               <dt>{% trans %}Location{% endtrans %}</dt>
-                                               <dd><a href="{{ path('rapsys_air_location_view', {'id': session.location.id}) }}">{{ session.location.title }}</a></dd>
-                                       </dl>
-                                       <dl>
-                                               {# location #}
-                                               <dt>{% trans %}Address{% endtrans %}</dt>
-                                               <dd>
-                                                       {{ session.location.address }}
-                                                       {{ session.location.zipcode }} {{ session.location.city }}
-                                               </dd>
-                                       </dl>
-                                       <dl>
-                                               <dt>{% trans %}Maps{% endtrans %}</dt>
-                                               <dd><a href="https://www.google.fr/maps/@{{ session.location.latitude }},{{ session.location.longitude }},19z">Google Maps</a></dd>
-                                               <dd><a href="https://www.openstreetmap.org/#map=19/{{ session.location.latitude }}/{{ session.location.longitude }}">OpenStreetMap</a></dd>
-                                       </dl>
-                                       <dl>
-                                               <dt>{% trans %}Minimap{% endtrans %}</dt>
-                                               <dd>TODO: minimap</dd>
-                                       </dl>
-                               </section>
-                       </div>
-                       {{ include('@RapsysAir/form/_toolbox.html.twig') }}
-               </div>
-               <article>
-                       <h3>{% trans %}Candidates{% endtrans %}</h3>
-                       <div class="panel grid four">
-                               {% if session.applications is null %}
+                               {% if is_granted('ROLE_GUEST') %}
                                        <section class="cell">
-                                               {% trans %}None{% endtrans %}
-                                       </section>
-                               {% else %}
-                                       {% for application in session.applications %}
-                                               <article class="cell">
-                                                       <h4><a href="{{ path('rapsys_air_user_view', {'id': application.user.id} ) }}">{{ application.user.title }}</a></h4>
-                                                       <dl>
-                                                               <dt>{% trans %}Score{% endtrans %}</dt>
-                                                               <dd>
-                                                                       {% if application.score is null %}
+                                               <header>
+                                                       <h3>{% trans %}Candidates{% endtrans %}</h3>
+                                               </header>
+                                               <div class="panel">
+                                                       <div class="grid{% if session.applications is defined %}{% if session.applications|length >= 4 %} four{% elseif session.applications|length >= 3 %} three{% elseif session.applications|length >= 2 %} two{% endif %}{% endif %}">
+                                                               {% if session.applications is defined and session.applications %}
+                                                                       {% for application in session.applications %}
+                                                                               <section class="cell">
+                                                                                       <header>
+                                                                                               {% if application.user.id == 1 and application.user.title|slug == 'milonga-raphael' %}
+                                                                                                       <h3><a href="{{ path('rapsysair_user_milongaraphael') }}">{{ application.user.title }}</a></h3>
+                                                                                               {% else %}
+                                                                                                       <h3><a href="{{ path('rapsysair_user_view', {'id': application.user.id, 'user': application.user.title|slug}) }}">{{ application.user.title }}</a></h3>
+                                                                                               {% endif %}
+                                                                                       </header>
+                                                                                       <dl>
+                                                                                               <dt>{% trans %}Score{% endtrans %}</dt>
+                                                                                               <dd>
+                                                                                                       {% if application.score is null %}
+                                                                                                               {% trans %}None{% endtrans %}
+                                                                                                       {% else %}
+                                                                                                               {{ application.score }}</dd>
+                                                                                                       {% endif %}
+                                                                                       </dl>
+                                                                                       <dl>
+                                                                                               <dt>{% trans %}Created{% endtrans %}</dt>
+                                                                                               <dd>{{ application.created|intldate('long', 'medium') }}</dd>
+                                                                                       </dl>
+                                                                                       <dl>
+                                                                                               <dt>{% trans %}Updated{% endtrans %}</dt>
+                                                                                               <dd>{{ application.updated|intldate('long', 'medium') }}</dd>
+                                                                                       </dl>
+                                                                                       <dl>
+                                                                                               <dt>{% trans %}Canceled{% endtrans %}</dt>
+                                                                                               <dd>
+                                                                                                       {% if application.canceled is null %}
+                                                                                                               {% trans %}None{% endtrans %}
+                                                                                                       {% else %}
+                                                                                                               {{ application.canceled|intldate('long', 'medium') }}
+                                                                                                       {% endif %}
+                                                                                               </dd>
+                                                                                       </dl>
+                                                                               </section>
+                                                                       {% endfor %}
+                                                               {% else %}
+                                                                       <section class="cell">
                                                                                {% trans %}None{% endtrans %}
-                                                                       {% else %}
-                                                                               {{ application.score }}</dd>
-                                                                       {% endif %}
-                                                       </dl>
-                                                       <dl>
-                                                               <dt>{% trans %}Created{% endtrans %}</dt>
-                                                               <dd>{{ application.created|localizeddate('long', 'medium') }}</dd>
-                                                       </dl>
-                                                       <dl>
-                                                               <dt>{% trans %}Updated{% endtrans %}</dt>
-                                                               <dd>{{ application.updated|localizeddate('long', 'medium') }}</dd>
-                                                       </dl>
-                                                       <dl>
-                                                               <dt>{% trans %}Canceled{% endtrans %}</dt>
-                                                               <dd>
-                                                                       {% if application.canceled is null %}
-                                                                               {% trans %}None{% endtrans %}
-                                                                       {% else %}
-                                                                               {{ application.canceled|localizeddate('long', 'medium') }}
-                                                                       {% endif %}
-                                                               </dd>
-                                                       </dl>
-                                               </article>
-                                       {% endfor %}
+                                                                       </section>
+                                                               {% endif %}
+                                                       </div>
+                                               </div>
+                                       </section>
                                {% endif %}
                        </div>
-               </article>
+                       {{ include('@RapsysAir/form/_toolbox.html.twig') }}
+               </div>
        </article>
        {{ include('@RapsysAir/default/_location.html.twig') }}
 {% endblock %}
index fddf9a34c183ce2647bff1d188ff9294ab3ef5c5..741be09b6f1b60dbcfd33cfb4269a1458c8f1146 100644 (file)
@@ -1,7 +1,10 @@
-{% extends '@RapsysAir/body.html.twig' %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
        <section id="form">
-               <h2>{{ section }}</h2>
+               <header>
+                       <h2>{{ title.page }}</h2>
+                       <p>{{ description }}</p>
+               </header>
                {{ form_start(form) }}
                        <div>
                                {{ form_row(form.description) }}
index fddf9a34c183ce2647bff1d188ff9294ab3ef5c5..741be09b6f1b60dbcfd33cfb4269a1458c8f1146 100644 (file)
@@ -1,7 +1,10 @@
-{% extends '@RapsysAir/body.html.twig' %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
        <section id="form">
-               <h2>{{ section }}</h2>
+               <header>
+                       <h2>{{ title.page }}</h2>
+                       <p>{{ description }}</p>
+               </header>
                {{ form_start(form) }}
                        <div>
                                {{ form_row(form.description) }}
diff --git a/Resources/views/user/edit.html.twig b/Resources/views/user/edit.html.twig
deleted file mode 100644 (file)
index 18637f9..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-{% extends '@RapsysAir/body.html.twig' %}
-{% block content %}
-       <section id="form">
-               <h2>{{ section }}</h2>
-               {{ form_start(form) }}
-                       <div>
-                               {% if form.mail is defined %}
-                                       {{ form_row(form.mail) }}
-                               {% endif %}
-
-                               {% if form.civility is defined %}
-                                       {{ form_row(form.civility) }}
-                               {% endif %}
-
-                               {% if form.forename is defined %}
-                                       {{ form_row(form.forename) }}
-                               {% endif %}
-
-                               {% if form.surname is defined %}
-                                       {{ form_row(form.surname) }}
-                               {% endif %}
-
-                               {% if form.pseudonym is defined %}
-                                       {{ form_row(form.pseudonym) }}
-                               {% endif %}
-
-                               {% if form.slug is defined %}
-                                       {{ form_row(form.slug) }}
-                               {% endif %}
-
-                               {% if form.phone is defined %}
-                                       {{ form_row(form.phone) }}
-                               {% endif %}
-
-                               {{ form_row(form.submit) }}
-                       </div>
-
-                       {# render csrf token etc .#}
-                       <footer style="display:none">{{ form_rest(form) }}</footer>
-               {{ form_end(form) }}
-       </section>
-{% endblock %}
index 58417a77651b7fe9191a640d23c359dcd37bb547..b70f941112e8ab05eb6285aac7653102f6c4b171 100644 (file)
@@ -1,50 +1,42 @@
-{% extends '@RapsysAir/body.html.twig' %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
-       <article id="dashboard">
+       <article>
                <header>
-                       <h2><a href="{{ path('rapsys_air_user') }}">{{ section }}</a></h2>
+                       <h2>{{ title.page }}</h2>
                        <p>{{ description }}</p>
                </header>
                <div class="panel">
                        {% if groups is defined and groups %}
-                               <div class="grid three">
+                               <div class="grid{% if groups|length > 2 %} three{% elseif groups|length > 1 %} two{% endif %}">
                                        {% for group, users in groups %}
                                                <article class="cell">
-                                                       <h3>{{ group }}</h3>
+                                                       <header>
+                                                               <h3>{{ group }}</h3>
+                                                       </header>
                                                        <ul>
                                                                {% for id, user in users %}
-                                                                       <li><a href="{{ path('rapsys_air_user_view', {'id': id}) }}">{{ user.pseudonym }}</a><a href="{{ path('rapsys_user_edit', {'mail': user.mail|short, 'hash': user.mail|short|hash}) }}">{% trans %}Modify{% endtrans %}</a></li>
+                                                                       <li><a href="{{ user.link }}" title="{% if user.forename %}{{ user.forename }} {% endif %}{% if user.surname %}{{ user.surname }} {% endif %}{% if user.pseudonym %}({{ user.pseudonym }}) {% endif %}&lt;{{ user.mail }}&gt;">{{ user.mail }}</a><a href="{{ user.edit }}">{% trans %}Modify{% endtrans %}</a></li>
                                                                {% endfor %}
                                                </article>
                                        {% endfor %}
                                </div>
                        {% elseif users is defined and users %}
-                               <div class="grid four">
+                               <div class="grid{% if users|length > 3 %} four{% elseif users|length > 2 %} three{% elseif users|length > 1 %} two{% endif %}">
                                        {% for id, user in users %}
                                                <article class="cell">
-                                                       <h3><a href="{{ path('rapsys_air_user_view', {'id': id}) }}">{{ user.pseudonym }}</a></h3>
-                                               </article>
-                                       {% endfor %}
-                                       {#{% for date, day in organizers %}
-                                               <article class="{{ ['cell', 'seventh']|merge(day.class)|join(' ') }}">
-                                                       <h3>{{ day.title }}</h3>
-                                                       {% if day.sessions is not empty %}
-                                                               <ul>
-                                                                       {% for session in day.sessions %}
-                                                                               <li class="{{ ['session']|merge(session.class)|join(' ') }}">
-                                                                                       <a href="{{ path('rapsys_air_session_view', {'id': session.id}) }}" title="{{ session.applications|join('\n') }}">{{ session.title }}</a>
-                                                                                       <span>
-                                                                                               {% if session.weather is defined and session.weather %}<span title="{{ session.weathertitle }}">{{ session.weather }}</span>{% endif %}
-                                                                                               <span title="{{ session.slottitle }}">{{ session.slot }}</span>
-                                                                                       </span>
-                                                                               </li>
+                                                       <header>
+                                                               <h3><a href="{{ user.link }}" title="{{ '%pseudonym% calendar'|trans({'%pseudonym%': user.pseudonym}) }}">{{ user.pseudonym }}</a></h3>
+                                                       </header>
+                                                       {% for dname, dinfos in user.dances %}
+                                                               <dl>
+                                                                       <dt><a href="{{ dinfos.link }}">{{ dname }}</a></dt>
+                                                                       {% for dtype, dlink in dinfos.types %}
+                                                                               <dd><a href="{{ dlink }}">{{ dtype }}</a><dd>
                                                                        {% endfor %}
-                                                               </ul>
-                                                       {% else %}
-                                                               &nbsp;
-                                                       {% endif %}
+                                                               </dl>
+                                                       {% endfor %}
                                                </article>
-                                       {% endfor %}#}
+                                       {% endfor %}
                                </div>
                        {% endif %}
                        {{ include('@RapsysAir/form/_toolbox.html.twig') }}
index 5c652e33e4263e359716e0ff5f8edf1d4faeb2a8..9c478790110ab01c2d0f418bc0357870ef992f06 100644 (file)
@@ -1,8 +1,8 @@
-{% extends '@RapsysAir/body.html.twig' %}
+{% extends '@RapsysAir/base.html.twig' %}
 {% block content %}
-       <article id="dashboard">
+       <article>
                <header>
-                       <h2><a href="{{ path('rapsys_air_user_view', {'id': id}) }}">{{ section }}</a></h2>
+                       <h2>{{ title.page }}</h2>
                        <p>{{ description }}</p>
                </header>
                <div class="panel">
                                <div class="grid calendar seven">
                                        {% for date, day in calendar %}
                                                <section class="{{ day.class|join(' ') }}">
-                                                       <h3>{{ day.title }}</h3>
+                                                       <header>
+                                                               <h3>{{ day.title }}</h3>
+                                                       </header>
                                                        {% if day.sessions is not empty %}
                                                                <ul>
                                                                        {% for session in day.sessions %}
                                                                                <li class="{{ session.class|join(' ') }}">
-                                                                                       <a href="{{ path('rapsys_air_session_view', {'id': session.id}) }}" title="{{ session.applications|join('\n') }}">
-                                                                                               <span>{{ session.start|localizeddate('none', 'short') }}</span>
-                                                                                               <span class="reducible">{{ session.location }}</span>
-                                                                                               <span class="info">
-                                                                                                       {% if session.weather is defined and session.weather %}
-                                                                                                               <span title="{{ session.weathertitle }}">{{ session.weather }}</span>
-                                                                                                       {% endif %}
-                                                                                                       <span title="{{ session.slottitle }}">{{ session.slot }}</span>
-                                                                                               </span>
-                                                                                               <span>{{ session.stop|localizeddate('none', 'short') }}</span>
-                                                                                               {% if session.pseudonym is defined and session.pseudonym %}
-                                                                                                       <span class="reducible{% if session.rate is not defined and session.hat is not defined %} pseudonym{% endif %}">{{ session.pseudonym }}</span>
+                                                                                       <a href="{{ session.link }}" title="{{ session.title }}">
+                                                                                               <span>{{ session.start|intldate('none', 'short') }}</span>
+                                                                                               <span class="reducible">{{ session.location.title }}</span>
+                                                                                               <span class="temperature"{% if session.temperature.title is defined and session.temperature.title %} title="{{ session.temperature.title }}"{% endif %}><span class="glyph">{{ session.temperature.glyph }}</span></span>
+                                                                                               <span>{{ session.stop|intldate('none', 'short') }}</span>
+                                                                                               <span class="reducible">{% if session.application.user.title is defined and session.application.user.title %}{{ session.application.user.title }}{% endif %}</span>
+                                                                                               <span class="rain"{% if session.rain.title is defined and session.rain.title %} title="{{ session.rain.title }}"{% endif %}><span class="glyph">{{ session.rain.glyph }}</span></span>
+                                                                                               {% if session.rate is defined and session.rate %}
+                                                                                                       <span class="rate" title="{{ session.rate.title }}">{% if session.rate.rate is defined and session.rate.rate %}{{ session.rate.rate }} {% endif %}<span class="glyph">{{ session.rate.glyph }}</span></span>
+                                                                                               {% else %}
+                                                                                                       <span></span>
                                                                                                {% endif %}
-                                                                                               {% if session.rate is defined %}<span class="info">{% if session.hat is defined and session.hat %}🎩{% else %}{{ session.rate }} €{% endif %}</span>{% endif %}
+                                                                                               <span class="reducible">{{ session.location.zipcode }} {{ session.location.city }}</span>
+                                                                                               <span></span>
                                                                                        </a>
                                                                                </li>
                                                                        {% endfor %}
diff --git a/Token/AnonymousToken.php b/Token/AnonymousToken.php
new file mode 100644 (file)
index 0000000..b6d108a
--- /dev/null
@@ -0,0 +1,22 @@
+<?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\Token;
+
+use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
+
+/**
+ * Anonymous Token
+ *
+ * {@inheritdoc}
+ */
+class AnonymousToken extends AbstractToken {
+}
diff --git a/Transformer/DanceTransformer.php b/Transformer/DanceTransformer.php
new file mode 100644 (file)
index 0000000..123f562
--- /dev/null
@@ -0,0 +1,87 @@
+<?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\Transformer;
+
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
+use Doctrine\ORM\EntityManagerInterface;
+
+use Symfony\Component\Form\DataTransformerInterface;
+use Symfony\Component\Form\Exception\TransformationFailedException;
+
+use Rapsys\AirBundle\Entity\Dance;
+
+/**
+ * {@inheritdoc}
+ */
+class DanceTransformer implements DataTransformerInterface {
+       /**
+        * Public constructor
+        *
+        * @param EntityManagerInterface $manager The entity manager
+        */
+       public function __construct(private EntityManagerInterface $manager) {
+       }
+
+       /**
+        * Transforms a dance object array or collection to an int array
+        *
+        * @param Collection|array $dances The dance instances array
+        * @return array The dance ids
+        */
+       public function transform(mixed $dances): mixed {
+               //Without dances
+               if (null === $dances) {
+                       return [];
+               }
+
+               //With collection instance
+               if ($dances instanceof Collection) {
+                       $dances = $dances->toArray();
+               }
+
+               //Return dance ids
+               return array_map(function ($d) { return $d->getId(); }, $dances);
+       }
+
+       /**
+        * Transforms an int array to a dance object collection
+        *
+        * @param array $ids
+        * @throws TransformationFailedException when object (dance) is not found
+        * @return array The dance instances array
+        */
+       public function reverseTransform(mixed $ids): mixed {
+               //Without ids
+               if ('' === $ids || null === $ids) {
+                       $ids = [];
+                       //With ids
+               } else {
+                       $ids = (array) $ids;
+               }
+
+               //Iterate on ids
+               foreach($ids as $k => $id) {
+                       //Replace with dance instance
+                       $ids[$k] = $this->manager->getRepository(Dance::class)->findOneById($id);
+
+                       //Without dance
+                       if (null === $ids[$k]) {
+                               //Throw exception
+                               throw new TransformationFailedException(sprintf('Dance with id "%d" does not exist!', $id));
+                       }
+               }
+
+               //Return collection
+               return new ArrayCollection($ids);
+       }
+}
diff --git a/Transformer/SubscriptionTransformer.php b/Transformer/SubscriptionTransformer.php
new file mode 100644 (file)
index 0000000..9c82db0
--- /dev/null
@@ -0,0 +1,87 @@
+<?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\Transformer;
+
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
+use Doctrine\ORM\EntityManagerInterface;
+
+use Symfony\Component\Form\DataTransformerInterface;
+use Symfony\Component\Form\Exception\TransformationFailedException;
+
+use Rapsys\AirBundle\Entity\User;
+
+/**
+ * {@inheritdoc}
+ */
+class SubscriptionTransformer implements DataTransformerInterface {
+       /**
+        * Public constructor
+        *
+        * @param EntityManagerInterface $manager The entity manager
+        */
+       public function __construct(private EntityManagerInterface $manager) {
+       }
+
+       /**
+        * Transforms a subscription object array or collection to an int array
+        *
+        * @param Collection|array $subscriptions The subscription instances array
+        * @return array The subscription ids
+        */
+       public function transform(mixed $subscriptions): mixed {
+               //Without subscriptions
+               if (null === $subscriptions) {
+                       return [];
+               }
+
+               //With collection instance
+               if ($subscriptions instanceof Collection) {
+                       $subscriptions = $subscriptions->toArray();
+               }
+
+               //Return subscription ids
+               return array_map(function ($d) { return $d->getId(); }, $subscriptions);
+       }
+
+       /**
+        * Transforms an int array to a subscription object collection
+        *
+        * @param array $ids
+        * @throws TransformationFailedException when object (subscription) is not found
+        * @return Collection The subscription instances array
+        */
+       public function reverseTransform(mixed $ids): mixed {
+               //Without ids
+               if ('' === $ids || null === $ids) {
+                       $ids = [];
+                       //With ids
+               } else {
+                       $ids = (array) $ids;
+               }
+
+               //Iterate on ids
+               foreach($ids as $k => $id) {
+                       //Replace with subscription instance
+                       $ids[$k] = $this->manager->getRepository(User::class)->findOneById($id);
+
+                       //Without subscription
+                       if (null === $ids[$k]) {
+                               //Throw exception
+                               throw new TransformationFailedException(sprintf('User with id "%d" does not exist!', $id));
+                       }
+               }
+
+               //Return collection
+               return new ArrayCollection($ids);
+       }
+}
index 4b932cb538c867a47abf100eb65330b722743483..15300aea8ff1775ab757f93670ae388e6a5bf9d7 100644 (file)
@@ -14,7 +14,8 @@
     },
     "require": {
         "doctrine/doctrine-bundle": "^1.0|^2.0",
-        "doctrine/orm": "^2.7",
+        "doctrine/orm": "^2.0",
+        "erusev/parsedown": "^1.7",
         "rapsys/userbundle": "dev-master",
         "rapsys/packbundle": "dev-master",
         "fpdf/fpdf": "^1.83",