From: Raphaël Gertz Date: Tue, 9 Apr 2024 13:44:41 +0000 (+0200) Subject: Rename rapsysair:calendar2 command to rapsysair:calendar X-Git-Tag: 0.5.0 X-Git-Url: https://git.rapsys.eu/airbundle/commitdiff_plain/refs/heads/master?hp=8b42e55e661fe05af15b0cf13d3e2bdfc094fa1a Rename rapsysair:calendar2 command to rapsysair:calendar --- diff --git a/Command/Calendar2Command.php b/Command/Calendar2Command.php deleted file mode 100644 index 0ce4da8..0000000 --- a/Command/Calendar2Command.php +++ /dev/null @@ -1,110 +0,0 @@ - - * - * 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 Google\Client; -use Google\Service\Calendar; -use Google\Service\Oauth2; - -use Symfony\Component\Cache\Adapter\FilesystemAdapter; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\Routing\RouterInterface; -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' calendar - */ -class Calendar2Command extends Command { - /** - * Set description - * - * Shown with bin/console list - */ - protected string $description = 'Synchronize sessions in users\' calendar'; - - /** - * Set help - * - * Shown with bin/console --help rapsysair:calendar2 - */ - protected string $help = 'This command synchronize sessions in users\' google calendar'; - - /** - * {@inheritdoc} - */ - public function __construct(protected ManagerRegistry $doctrine, protected string $locale, protected RouterInterface $router, protected SluggerUtil $slugger, protected TranslatorInterface $translator, protected Client $google, protected DefaultMarkdown $markdown) { - //Call parent constructor - parent::__construct($this->doctrine, $this->locale, $this->router, $this->slugger, $this->translator); - - //Replace google client redirect uri - $this->google->setRedirectUri($this->router->generate($this->google->getRedirectUri(), [], UrlGeneratorInterface::ABSOLUTE_URL)); - } - - /** - * Process the attribution - */ - protected function execute(InputInterface $input, OutputInterface $output): int { - //Set 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') - ); - - //Iterate on google tokens - foreach($tokens = $this->doctrine->getRepository(GoogleToken::class)->findAllIndexed() as $tid => $token) { - //Iterate on google calendars - foreach($calendars = $token['calendars'] as $cid => $calendar) { - #$calendar['synchronized'] - var_dump($token); - - //TODO: see if we may be smarter here ? - - //TODO: load all calendar events here ? - - //Iterate on sessions to update - foreach($sessions = $this->doctrine->getRepository(Session::class)->findAllByUserIdSynchronized($token['uid'], $calendar['synchronized']) as $session) { - //TODO: insert/update/delete events here ? - } - - //TODO: delete remaining events here ? - } - } - - //TODO: get user filter ? (users_subscriptions+users_dances) - - //TODO: XXX: or fetch directly the events updated since synchronized + matching rubscriptions and/or dances - - exit; - - //Return success - return self::SUCCESS; - } -} diff --git a/Command/CalendarCommand.php b/Command/CalendarCommand.php index dca096b..f722aae 100644 --- a/Command/CalendarCommand.php +++ b/Command/CalendarCommand.php @@ -12,455 +12,602 @@ namespace Rapsys\AirBundle\Command; use Doctrine\Persistence\ManagerRegistry; -use Symfony\Component\Cache\Adapter\FilesystemAdapter; + +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\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 { /** - * Creates new calendar command + * Set description * - * @param ManagerRegistry $doctrine The doctrine instance - * @param string $locale The default locale - * @param RouterInterface $router The router instance - * @param SluggerUtil $slugger The slugger instance - * @param TranslatorInterface $translator The translator instance - * @param string $namespace The cache namespace - * @param int $lifetime The cache lifetime - * @param string $path The cache path + * Shown with bin/console list */ - public function __construct(protected ManagerRegistry $doctrine, protected string $locale, protected RouterInterface $router, protected SluggerUtil $slugger, protected TranslatorInterface $translator, protected string $namespace, protected int $lifetime, protected string $path) { - //Call parent constructor - parent::__construct($this->doctrine, $this->locale, $this->router, $this->slugger, $this->translator); - } + protected string $description = 'Synchronize sessions in users\' calendar'; /** - * Configure attribute command + * Set help + * + * Shown with bin/console --help rapsysair:calendar */ - 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'); - } + protected string $help = 'This command synchronize sessions in users\' google calendar'; /** - * Process the attribution + * Set domain */ - protected function execute(InputInterface $input, OutputInterface $output): int { - //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') - ); + protected string $domain; - //Retrieve events to update - $sessions = $this->doctrine->getRepository(Session::class)->fetchAllByDatePeriod($period, $this->locale); + /** + * Set item + * + * Cache item instance + */ + protected ItemInterface $item; - //Markdown converted instance - $markdown = new DefaultMarkdown; + /** + * Set prefix + */ + protected string $prefix; - //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->namespace, $this->lifetime, $this->path); + /** + * Set service + * + * Google calendar instance + */ + protected Calendar $service; - //Retrieve calendars - $cacheCalendars = $cache->getItem('calendars'); + /** + * {@inheritdoc} + * + * @param CacheInterface $cache The cache instance + * @param Client $google The google client instance + * @param DefaultMarkdown $markdown The markdown instance + */ + 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($this->doctrine, $this->locale, $this->router, $this->slugger, $this->translator); - //Without calendars - if (!$cacheCalendars->isHit()) { - //Return failure - return self::FAILURE; - } + //Replace google client redirect uri + $this->google->setRedirectUri($this->router->generate($this->google->getRedirectUri(), [], UrlGeneratorInterface::ABSOLUTE_URL)); + } - //Retrieve calendars - $calendars = $cacheCalendars->get(); - - //XXX: calendars content - #var_export($calendars); - - //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]); - } + /** + * Process the attribution + */ + protected function execute(InputInterface $input, OutputInterface $output): int { + //Get domain + $this->domain = $this->router->getContext()->getHost(); - //Save calendars - $cacheCalendars->set($calendars); + //Get manager + $manager = $this->doctrine->getManager(); - //Save calendar - $cache->save($cacheCalendars); + //Convert from any to latin, then to ascii and lowercase + $trans = \Transliterator::create('Any-Latin; Latin-ASCII; Lower()'); - //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"; + //Replace every non alphanumeric character by dash then trim dash + $this->prefix = preg_replace(['/(\.[a-z0-9]+)+$/', '/[^a-v0-9]+/'], '', $trans->transliterate($this->domain)); - //Return failure - //XXX: we want that mail and stop here - return self::FAILURE; - } - } - } + //With too short prefix + if ($this->prefix === null || strlen($this->prefix) < 4) { + //Throw domain exception + throw new \DomainException('Prefix too short: '.$this->prefix); } - //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'] - ] - ); - - //With expired token - if ($exp = $googleClient->isAccessTokenExpired()) { - //Last chance to skip this run + //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; } + } - //Get google calendar - $googleCalendar = new \Google\Service\Calendar($googleClient); + //Get google calendar service + $this->service = new Calendar($this->google); - //Retrieve calendar + //Iterate on google calendars + foreach($calendars = $token['calendars'] as $cid => $calendar) { + //Set start + $synchronized = null; + + //Set cache key + $cacheKey = 'command.calendar.'.$this->slugger->short($calendar['mail']); + + //XXX: TODO: remove DEBUG + #$this->cache->delete($cacheKey); + + //Retrieve calendar events try { - $calendar = $googleCalendar->calendars->get($token['calendar']); + //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) { - //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"; + //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()?:''); - //Return failure - return self::FAILURE; + //TODO: warn user by mail ? + + //Skip to next token + continue; + } + + //Throw error + throw new \LogicException('Calendar event list failed', 0, $e); } - //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";*/ + //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); } } + } - //Get page token - $pageToken = $googleEvents->getNextPageToken(); + //Get all sessions + $sessions = $this->doctrine->getRepository(Session::class)->findAllByUserIdSynchronized($token['uid']); - //Handle next page - if ($pageToken) { - //Replace collection with next one - $googleEvents = $service->events->listEvents($token['calendar'], $filters+['pageToken' => $pageToken]); - } else { - break; + //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]); + } } } - //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('%dance% %id% by %pseudonym%', ['%id%' => $sessionId, '%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' => $sessionId, '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) - ]; - - //Init location - $description = 'Emplacement :'."\n".$this->translator->trans($session['l_description']); - $shared['location'] = $markdown->convert(strip_tags($session['l_description'])); - - //Add description - $description .= "\n\n".'Description :'."\n".strip_tags(preg_replace('!([^<]+)!', '\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']; - } + //Persist cache item + $this->cache->commit(); - //Add contact when available - if (!empty($session['p_contact'])) { - $shared['contact'] = $session['p_contact']; - $description .= "\n\n".'Contact :'."\n".$session['p_contact']; - } + //With synchronized + //XXX: only store synchronized on run without caching + if ($synchronized) { + //Get google calendar + $googleCalendar = $this->doctrine->getRepository(GoogleCalendar::class)->findOneById($calendar['id']); - //Add donate when available - if (!empty($session['p_donate'])) { - $shared['donate'] = $session['p_donate']; - $description .= "\n\n".'Contribuer :'."\n".$session['p_donate']; - } + //Set synchronized + $googleCalendar->setSynchronized($synchronized); - //Add link when available - if (!empty($session['p_link'])) { - $shared['link'] = $session['p_link']; - $description .= "\n\n".'Site :'."\n".$session['p_link']; - } + //Queue google calendar save + $manager->persist($googleCalendar); - //Add profile when available - if (!empty($session['p_profile'])) { - $shared['profile'] = $session['p_profile']; - $description .= "\n\n".'Réseau social :'."\n".$session['p_profile']; - } + //Flush to get the ids + $manager->flush(); + } + } + } - //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']), - 'summary' => $source['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]; + //Return success + return self::SUCCESS; + } + + /** + * 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 event id + $eid = $event->getId(); + + //Delete the event + $this->service->events->delete($calendar, $eid); - //With updated event - if ($session['updated'] >= (new \DateTime($event->getUpdated()))) { - //Set summary - #$event->setSummary($session['au_pseudonym'].' '.$this->translator->trans('at '.$session['l_title'])); - $event->setSummary($source['title']); + //Set sid + $sid = intval(substr($event->getId(), strlen($this->prefix))); - //Set description - $event->setDescription($description); + //Remove from events and cache events + unset($cacheEvents[$sid]); - //Set status - $event->setStatus(empty($session['a_canceled'])?'confirmed':'cancelled'); + //Set cache events + $this->item->set($cacheEvents); - //Set location - $event->setLocation(implode(' ', [$session['l_address'], $session['l_zipcode'], $session['l_city']])); + //Save cache item + $this->cache->saveDeferred($this->item); - //Get source - $eventSource = $event->getSource(); + //Return session id + return $sid; + } + + /** + * 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) + ] + ); - //Update source title - $eventSource->setTitle($source['title']); + //Init location + $description = 'Emplacement :'."\n".$this->translator->trans($session['l_description']); + $shared['location'] = strip_tags($this->translator->trans($session['l_description'])); - //Update source url - $eventSource->setUrl($source['url']); + //Add description when available + if(!empty($session['p_description'])) { + $description .= "\n\n".'Description :'."\n".strip_tags(preg_replace('!([^<]+)!', '\1', $this->markdown->convert(strip_tags($session['p_description'])))); + $shared['description'] = $this->markdown->convert(strip_tags($session['p_description'])); + } - //Set source - #$event->setSource($source); + //Add class when available + if (!empty($session['p_class'])) { + $description .= "\n\n".'Classe :'."\n".$session['p_class']; + $shared['class'] = $session['p_class']; + } - //Get extended properties - $extendedProperties = $event->getExtendedProperties(); + //Add contact when available + if (!empty($session['p_contact'])) { + $description .= "\n\n".'Contact :'."\n".$session['p_contact']; + $shared['contact'] = $session['p_contact']; + } - //Update shared - $extendedProperties->setShared($shared); + //Add donate when available + if (!empty($session['p_donate'])) { + $description .= "\n\n".'Contribuer :'."\n".$session['p_donate']; + $shared['donate'] = $session['p_donate']; + } - //TODO: colorId ? - //TODO: attendees[] ? + //Add link when available + if (!empty($session['p_link'])) { + $description .= "\n\n".'Site :'."\n".$session['p_link']; + $shared['link'] = $session['p_link']; + } - //Set start - $start = $event->getStart(); + //Add profile when available + if (!empty($session['p_profile'])) { + $description .= "\n\n".'Réseau social :'."\n".$session['p_profile']; + $shared['profile'] = $session['p_profile']; + } - //Update start datetime - $start->setDateTime($session['start']->format(\DateTime::ISO8601)); + //Set properties + $properties = new EventExtendedProperties( + [ + //Set private property + 'private' => $private, + //Set shared property + 'shared' => $shared + ] + ); - //Set end - $end = $event->getEnd(); + //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()); - //Update stop datetime - $end->setDateTime($session['stop']->format(\DateTime::ISO8601)); + //Set description + $event->setDescription($description); - 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 status + $event->setStatus(empty($session['a_canceled'])?'confirmed':'cancelled'); - //Return failure - return self::FAILURE; - } - } + //Set location + $event->setLocation(implode(' ', [$session['l_address'], $session['l_zipcode'], $session['l_city']])); - //Drop from events array - unset($events[$sessionId]); - } - } + //Get source + #$eventSource = $event->getSource(); - //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 source title + #$eventSource->setTitle($source->getTitle()); + + //Update source url + #$eventSource->setUrl($source->getUrl()); + + //Set source + $event->setSource($source); + + //Get extended properties + #$extendedProperties = $event->getExtendedProperties(); + + //Update private + #$extendedProperties->setPrivate($properties->getPrivate()); + + //Update shared + #$extendedProperties->setShared($properties->getShared()); + + //Set properties + $event->setExtendedProperties($properties); + + //TODO: colorId ? + //TODO: attendees[] ? + + //Set start + $start = $event->getStart(); + + //Update start datetime + $start->setDateTime($session['start']->format(\DateTime::ISO8601)); + + //Set end + $end = $event->getEnd(); + + //Update stop datetime + $end->setDateTime($session['stop']->format(\DateTime::ISO8601)); } - //Return success - return self::SUCCESS; + //Return event + return $event; + } + + /** + * Insert event + * + * @param string $calendar The calendar mail + * @param array $session The session instance + * @return void + */ + 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']; } } diff --git a/Controller/DefaultController.php b/Controller/DefaultController.php index 647baf8..1459096 100644 --- a/Controller/DefaultController.php +++ b/Controller/DefaultController.php @@ -105,6 +105,7 @@ class DefaultController extends AbstractController { //And give the proper parameters $form = $this->factory->create('Rapsys\AirBundle\Form\ContactType', $data, [ 'action' => $this->generateUrl('rapsysair_contact'), + 'captcha' => true, 'method' => 'POST' ]); diff --git a/Form/ContactType.php b/Form/ContactType.php index 93951e9..4142c99 100644 --- a/Form/ContactType.php +++ b/Form/ContactType.php @@ -46,6 +46,9 @@ class ContactType extends CaptchaType { * {@inheritdoc} */ public function configureOptions(OptionsResolver $resolver): void { + //Call parent configure options + parent::configureOptions($resolver); + //Set defaults $resolver->setDefaults(['error_bubbling' => true]); } diff --git a/RapsysAirBundle.php b/RapsysAirBundle.php index 6096959..e1bb77b 100644 --- a/RapsysAirBundle.php +++ b/RapsysAirBundle.php @@ -62,6 +62,6 @@ class RapsysAirBundle extends Bundle { */ public static function getVersion(): string { //Return version - return '0.4.0'; + return '0.5.0'; } } diff --git a/Repository/GoogleTokenRepository.php b/Repository/GoogleTokenRepository.php index fa2a854..29b96d2 100644 --- a/Repository/GoogleTokenRepository.php +++ b/Repository/GoogleTokenRepository.php @@ -29,20 +29,54 @@ class GoogleTokenRepository extends Repository { //Set the request $req = <<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 @@ -97,11 +134,14 @@ SQL; 'uid' => $token['uid'], 'access' => $token['access'], 'refresh' => $token['refresh'], + 'created' => $token['created'], 'expired' => $token['expired'], - 'calendars' => [] + 'calendars' => [], + 'dances' => [], + 'subscriptions' => [] ]; - //Iterate on + //Iterate on calendars foreach($cids as $k => $cid) { $result[$tid]['calendars'][$cid] = [ 'id' => $cid, @@ -110,6 +150,26 @@ SQL; '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 diff --git a/Repository/LocationRepository.php b/Repository/LocationRepository.php index 963f2c8..9b621ef 100644 --- a/Repository/LocationRepository.php +++ b/Repository/LocationRepository.php @@ -134,7 +134,7 @@ FROM ( l.latitude, l.longitude, l.indoor, - GREATEST(l.created, l.updated, COALESCE(s.created, 0), COALESCE(s.updated, 0)) AS modified, + 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 @@ -362,7 +362,7 @@ SELECT a.longitude, a.created, a.updated, - MAX(GREATEST(a.modified, COALESCE(s.created, 0), COALESCE(s.updated, 0))) AS modified, + 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 @@ -461,7 +461,7 @@ SELECT a.longitude, a.created, a.updated, - MAX(GREATEST(a.modified, COALESCE(s3.created, 0), COALESCE(s3.updated, 0))) AS modified, + 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 ( @@ -473,7 +473,7 @@ FROM ( b.longitude, b.created, b.updated, - MAX(GREATEST(b.modified, COALESCE(s2.created, 0), COALESCE(s2.updated, 0))) AS modified, + 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 @@ -716,18 +716,15 @@ SQL; */ public function findComplementBySessionId(int $id): array { //Fetch complement locations - $ret = $this->getEntityManager() - ->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(); - - //TODO: try to improve with: - #->addIndexByScalar('city'); - - //Rekey array - $ret = array_column($ret, 'id', 'title'); - - return $ret; + 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' + ); } /** diff --git a/Repository/SessionRepository.php b/Repository/SessionRepository.php index d44db55..c310b54 100644 --- a/Repository/SessionRepository.php +++ b/Repository/SessionRepository.php @@ -106,7 +106,7 @@ SELECT p.profile AS p_profile, p.rate AS p_rate, p.hat AS p_hat, - GREATEST(COALESCE(s.updated, 0), COALESCE(l.updated, 0), COALESCE(t.updated, 0), COALESCE(p.updated, 0), COALESCE(MAX(sa.updated), 0), COALESCE(MAX(sau.updated), 0), COALESCE(MAX(sad.updated), 0)) AS modified, + 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, @@ -122,8 +122,8 @@ 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\User AS sau ON (sau.id = sa.user_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 @@ -553,12 +553,12 @@ SELECT p.hat AS p_hat, p.rate AS p_rate, p.short AS p_short, - 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, + 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, - GREATEST(COALESCE(s.updated, 0), COALESCE(l.updated, 0), COALESCE(p.updated, 0), COALESCE(MAX(sa.updated), 0), COALESCE(MAX(sau.updated), 0), COALESCE(MAX(sad.updated), 0)) AS modified + 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 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) @@ -615,15 +615,15 @@ SQL; ->addScalarResult('p_rate', 'p_rate', 'integer') ->addScalarResult('p_short', 'p_short', 'string') //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') - //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 + ->addScalarResult('sau_pseudonym', 'sau_pseudonym', 'string') ->addIndexByScalar('id'); //Fetch result @@ -943,7 +943,7 @@ SQL; * @param DateTime $synchronized The synchronized datetime * @return array The session data */ - public function findAllByUserIdSynchronized(int $userId, \DateTime $synchronized): array { + public function findAllByUserIdSynchronized(int $userId, \DateTime $synchronized = new \DateTime('1970-01-01')): array { //Set the request $req = <<addScalarResult('user_id', 'user_id', 'integer'); + $rsm->addScalarResult('subscribed_id', 'subscribed_id', 'integer'); //Set subscription sql part $subscriptionSql = ''; @@ -1010,7 +1010,7 @@ SELECT s.id, s.date, s.locked, - s.updated, + 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, @@ -1044,7 +1044,7 @@ JOIN Rapsys\AirBundle\Entity\Application AS a ON (a.id = s.application_id{$dance 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(s.created, s.updated, a.created, a.updated, ADDTIME(ADDTIME(s.date, s.begin), s.length)) >= :synchronized +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 @@ -1060,7 +1060,7 @@ SQL; ->addScalarResult('id', 'id', 'integer') ->addScalarResult('date', 'date', 'date') ->addScalarResult('locked', 'locked', 'datetime') - ->addScalarResult('updated', 'updated', 'datetime') + ->addScalarResult('modified', 'modified', 'datetime') ->addScalarResult('start', 'start', 'datetime') ->addScalarResult('stop', 'stop', 'datetime') ->addScalarResult('l_id', 'l_id', 'integer') @@ -1099,7 +1099,7 @@ SQL; ->setParameter('dids', $userDances) ->setParameter('uids', $userSubscriptions) ->setParameter('synchronized', $synchronized) - ->getArrayResult(); + ->getResult(AbstractQuery::HYDRATE_ARRAY); } /** diff --git a/Repository/UserRepository.php b/Repository/UserRepository.php index 4e7dae8..725e3ed 100644 --- a/Repository/UserRepository.php +++ b/Repository/UserRepository.php @@ -218,9 +218,9 @@ SELECT 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, - GREATEST(COALESCE(u.updated, 0), COALESCE(c.updated, 0), COALESCE(o.updated, 0)) AS modified + 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) @@ -252,11 +252,11 @@ SQL; ->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') - ->addScalarResult('modified', 'modified', 'datetime') ->addIndexByScalar('id'); //Get result diff --git a/Resources/config/doctrine/User.orm.yml b/Resources/config/doctrine/User.orm.yml index 6002651..fcc5712 100644 --- a/Resources/config/doctrine/User.orm.yml +++ b/Resources/config/doctrine/User.orm.yml @@ -54,7 +54,7 @@ Rapsys\AirBundle\Entity\User: name: user_id inverseJoinColumns: id: - name: subscriber_id + name: subscribed_id locations: targetEntity: Rapsys\AirBundle\Entity\Location inversedBy: users diff --git a/Resources/config/packages/rapsysair.yaml b/Resources/config/packages/rapsysair.yaml index 02197a8..151ec25 100644 --- a/Resources/config/packages/rapsysair.yaml +++ b/Resources/config/packages/rapsysair.yaml @@ -357,11 +357,7 @@ services: tags: [ 'console.command' ] # Register calendar command Rapsys\AirBundle\Command\CalendarCommand: - arguments: [ '@doctrine', '%kernel.default_locale%', '@router', '@rapsyspack.slugger_util', '@translator', 'airlibre', 0, '%kernel.project_dir%/var/cache' ] - tags: [ 'console.command' ] - # Register calendar2 command - Rapsys\AirBundle\Command\Calendar2Command: - arguments: [ '@doctrine', '%kernel.default_locale%', '@router', '@rapsyspack.slugger_util', '@translator', '@google.client', '@twig.markdown.default' ] + 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: diff --git a/Resources/translations/messages.en_gb.yaml b/Resources/translations/messages.en_gb.yaml index 5bb0f5d..e6c2f81 100644 --- a/Resources/translations/messages.en_gb.yaml +++ b/Resources/translations/messages.en_gb.yaml @@ -164,6 +164,7 @@ 'Class': 'Class' '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)' diff --git a/Resources/translations/messages.fr_fr.yaml b/Resources/translations/messages.fr_fr.yaml index 628909a..a82368f 100644 --- a/Resources/translations/messages.fr_fr.yaml +++ b/Resources/translations/messages.fr_fr.yaml @@ -168,6 +168,7 @@ 'Civility': 'Civilité' 'Class': 'Cours' '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' diff --git a/Resources/views/form/register.html.twig b/Resources/views/form/register.html.twig index 74e3aa2..cd71a78 100644 --- a/Resources/views/form/register.html.twig +++ b/Resources/views/form/register.html.twig @@ -43,6 +43,10 @@ {{ form_row(register.phone) }} {% endif %} + {% if register.captcha is defined %} + {{ form_row(register.captcha) }} + {% endif %} + {{ form_row(register.submit) }} {# Render CSRF token etc .#}