summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorElias Showk <eshowk@entrouvert.com>2018-04-05 14:12:12 (GMT)
committerElias Showk <eshowk@entrouvert.com>2018-04-06 08:18:41 (GMT)
commit4dd11066d795b7a36c201ad4c55c47c8f8c0a5b1 (patch)
treedf9e072495eec6bce7af6d1e84ef07fde35dad07
parent266ede325f59c3a59b56ba20b7ece2e51dfdda3b (diff)
downloadcombo-plugin-gnm-wip/mairieopeninghourstable.zip
combo-plugin-gnm-wip/mairieopeninghourstable.tar.gz
combo-plugin-gnm-wip/mairieopeninghourstable.tar.bz2
add template tag as_opening_hour_table to display mairie hours (#22469)wip/mairieopeninghourstable
-rw-r--r--combo_plugin_gnm/geojson_utils.py183
-rw-r--r--combo_plugin_gnm/templates/opening_hours_table.html8
-rw-r--r--combo_plugin_gnm/templatetags/gnm.py204
3 files changed, 274 insertions, 121 deletions
diff --git a/combo_plugin_gnm/geojson_utils.py b/combo_plugin_gnm/geojson_utils.py
new file mode 100644
index 0000000..6faa86c
--- /dev/null
+++ b/combo_plugin_gnm/geojson_utils.py
@@ -0,0 +1,183 @@
+# -*- coding: utf-8 -*-
+# combo-plugin-gnm - Combo GNM plugin
+# Copyright (C) 2017 Entr'ouvert
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+from collections import OrderedDict
+import datetime
+import operator
+import re
+
+from dateutil.parser import parse as dateutil_parse
+
+from django.utils.dateparse import parse_datetime
+from django.utils.timezone import is_naive, make_aware
+
+
+FR_WEEKDAYS = ['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche']
+EN_ABBREV_WEEKDAYS = OrderedDict([
+ ('mo', 'Monday'),
+ ('tu', 'Tuesday'),
+ ('we', 'Wednesday'),
+ ('th', 'Thursday'),
+ ('fr', 'Friday'),
+ ('sa', 'Saturday'),
+ ('su', 'Sunday')
+ ])
+
+EN_ABBREV_WEEKDAYS_LIST = list(EN_ABBREV_WEEKDAYS.keys())
+FR_ABBREV_WEEKDAYS_LIST = OrderedDict(zip(EN_ABBREV_WEEKDAYS_LIST, FR_WEEKDAYS))
+WEEKDAYS = list(EN_ABBREV_WEEKDAYS.values())
+
+
+class TimeSlot(object):
+ def __init__(self, start, end):
+ if is_naive(start):
+ start = make_aware(start)
+ if is_naive(end):
+ end = make_aware(end)
+ self.start = start
+ self.end = end
+
+ def __repr__(self):
+ return '<TimeSlot start=%s - end=%s>' % (self.start.strftime('%c'), self.end.strftime('%c'))
+
+
+def get_open_close_from_specification(specification, valid_from, base_datetime):
+ opening_time = datetime.datetime.combine(base_datetime, dateutil_parse(specification['opens']).time())
+ closing_time = datetime.datetime.combine(base_datetime, dateutil_parse(specification['closes']).time())
+ opening_time = opening_time.replace(tzinfo=valid_from.tzinfo)
+ closing_time = closing_time.replace(tzinfo=valid_from.tzinfo)
+ day_of_week = WEEKDAYS.index(specification['dayOfWeek'].split('/')[-1])
+ opening_time = opening_time + datetime.timedelta(
+ days=(7 + (day_of_week - opening_time.weekday())) % 7)
+ closing_time = closing_time + datetime.timedelta(
+ days=(7 + (day_of_week - closing_time.weekday())) % 7)
+ return (opening_time, closing_time, day_of_week)
+
+
+def openinghours_to_datetime(codename, hour, minute, default=None):
+ try:
+ weekday = EN_ABBREV_WEEKDAYS.get(codename, None)
+ # if default is None, return the next date and time after now()
+ # a default datetime instance is used to replace absent parameters for datetime
+ return dateutil_parse('%s %d:%d:00' % (weekday, hour, minute), default=default)
+ except ValueError:
+ return None
+
+
+def get_period_from_data(weekday, start_hour, start_minute, end_hour, end_minute):
+ # add the period opening_hours_dict
+ closing_time = openinghours_to_datetime(weekday, int(end_hour), int(end_minute))
+ opening_time = openinghours_to_datetime(weekday, int(start_hour), int(start_minute))
+ if is_naive(closing_time):
+ closing_time = make_aware(closing_time)
+ if is_naive(opening_time):
+ closing_time = make_aware(opening_time)
+
+ all_day_hours = False
+ if closing_time.hour <= 12:
+ period = 'am'
+ elif opening_time.hour <= 12:
+ period = 'am'
+ all_day_hours = True
+ else:
+ period = 'pm'
+
+ return (period, all_day_hours)
+
+
+def get_slots_from_mdr_format(data, today):
+ """Process data from /ws/grandlyon/ter_territoire.maison_du_rhone/all.json
+ add to slots all the next opening hours in chronological order & beginning from today()
+ """
+ if 'properties' in data:
+ data = data['properties']
+ slots = []
+ known_format = False
+ mdr_weekdays_format = ['%s_am' % day for day in FR_WEEKDAYS] + ['%s_pm' % day for day in FR_WEEKDAYS]
+ if any([re.search('|'.join(mdr_weekdays_format), data_key) is not None for data_key in data.keys()]):
+ known_format = True
+ for i in range(7):
+ for period in ('am', 'pm'):
+ hours = data.get('%s_%s' % (FR_WEEKDAYS[today.weekday()], period))
+ if not hours:
+ continue
+ try:
+ parts = re.match('(\d?\d)h(\d\d)-(\d?\d)h(\d\d)', hours).groups()
+ except AttributeError:
+ continue
+ # add to slots the opening hours in chronological order beginning from today
+ slots.append(TimeSlot(
+ datetime.datetime(today.year, today.month, today.day, int(parts[0]), int(parts[1]), tzinfo=today.tzinfo),
+ datetime.datetime(today.year, today.month, today.day, int(parts[2]), int(parts[3]), tzinfo=today.tzinfo)
+ ))
+
+ today = today + datetime.timedelta(days=1)
+
+ return (slots, known_format)
+
+
+def get_slots_from_mairie_format(data, base_datetime):
+ """Process mairie json and return slots the opening hours in chronological order beginning today
+ """
+ if 'properties' in data:
+ data = data['properties']
+
+ known_format = False
+ slots = []
+ exclusion_slots = []
+ if len(data.get('openinghours', [])) or len(data.get('openinghoursspecification', [])):
+ known_format = True
+ # prepare annual opening exclusions
+ for specification in data.get('openinghoursspecification', []):
+ valid_from, valid_through = (
+ parse_datetime(specification.get('validFrom')),
+ parse_datetime(specification.get('validThrough'))
+ )
+ if not valid_from or not valid_through:
+ continue
+ if 'opens' in specification and 'closes' in specification:
+ # case when opening periods are defined
+ if base_datetime >= valid_from and base_datetime < valid_through:
+ (opening_time, closing_time, day_of_week) = get_open_close_from_specification(
+ specification, valid_from, base_datetime)
+ slots.append(TimeSlot(opening_time, closing_time))
+ else:
+ # case when exclusions are defined
+ exclusion_slots.append(TimeSlot(valid_from, valid_through))
+
+ for openinghours in data.get('openinghours', []):
+ try:
+ parts = re.match('(\w\w)-(\w\w) (\d\d):(\d\d)-(\d\d):(\d\d)', openinghours).groups()
+ except AttributeError:
+ continue
+ for weekday in EN_ABBREV_WEEKDAYS_LIST[
+ EN_ABBREV_WEEKDAYS_LIST.index(parts[0].lower()):
+ EN_ABBREV_WEEKDAYS_LIST.index(parts[1].lower()) + 1]:
+ timeslot = TimeSlot(
+ openinghours_to_datetime(weekday, int(parts[2]), int(parts[3]), default=base_datetime),
+ openinghours_to_datetime(weekday, int(parts[4]), int(parts[5]), default=base_datetime)
+ )
+ # add to slots the opening hours in chronological order beginning from today
+ slots.append(timeslot)
+
+ # order slots and cycle the list beginning with 'base_datetime'
+ slots = sorted(slots, key=operator.attrgetter('start'))
+ if len(slots):
+ def timedelta_key_func(slot):
+ return slot.start - base_datetime
+ nearest_slot_index = slots.index(min(slots, key=timedelta_key_func))
+ slots = slots[nearest_slot_index:] + slots[:nearest_slot_index]
+ return (slots, exclusion_slots, known_format)
diff --git a/combo_plugin_gnm/templates/opening_hours_table.html b/combo_plugin_gnm/templates/opening_hours_table.html
new file mode 100644
index 0000000..5efe8cd
--- /dev/null
+++ b/combo_plugin_gnm/templates/opening_hours_table.html
@@ -0,0 +1,8 @@
+<div class="horaires">
+ <h5>Horaires</h5>
+ <table class="horaires">
+ {% for day, hours in opening_hours %}
+ <tr><th>{{day}}</th><td>{{hours.am|default_if_none:"ferm&eacute;"}}</td><td>{{hours.pm|default_if_none:"ferm&eacute;"}}</td></tr>
+ {% endfor %}
+ </table>
+</div>
diff --git a/combo_plugin_gnm/templatetags/gnm.py b/combo_plugin_gnm/templatetags/gnm.py
index d4ea3c6..578c1d3 100644
--- a/combo_plugin_gnm/templatetags/gnm.py
+++ b/combo_plugin_gnm/templatetags/gnm.py
@@ -18,11 +18,9 @@
from collections import OrderedDict
import datetime
import math
-import operator
import random
import re
-from dateutil.parser import parse as dateutil_parse
from requests import RequestException
from django import template
@@ -39,135 +37,99 @@ from combo.data.models import Page, ConfigJsonCell
from combo.public.views import render_cell
from combo.utils import requests
-register = template.Library()
-
-FR_WEEKDAYS = ['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche']
-EN_ABBREV_WEEKDAYS = OrderedDict([
- ('mo', 'Monday'),
- ('tu', 'Tuesday'),
- ('we', 'Wednesday'),
- ('th', 'Thursday'),
- ('fr', 'Friday'),
- ('sa', 'Saturday'),
- ('su', 'Sunday')
-])
-EN_ABBREV_WEEKDAYS_LIST = list(EN_ABBREV_WEEKDAYS.keys())
-WEEKDAYS = list(EN_ABBREV_WEEKDAYS.values())
-
-
-class TimeSlot(object):
- def __init__(self, start, end):
- if is_naive(start):
- start = make_aware(start)
- if is_naive(end):
- end = make_aware(end)
- self.start = start
- self.end = end
-
- def __repr__(self):
- return '<TimeSlot start=%s - end=%s>' % (self.start.strftime('%c'), self.end.strftime('%c'))
-
-
-def openinghours_to_datetime(codename, hour, minute, default=None):
- try:
- weekday = EN_ABBREV_WEEKDAYS.get(codename, None)
- # if default is None, return the next date and time after now()
- # a default datetime instance is used to replace absent parameters for datetime
- return dateutil_parse('%s %d:%d:00' % (weekday, hour, minute), default=default)
- except ValueError:
- return None
-
-
-def get_slots_from_mdr_format(data, today):
- """Process data from /ws/grandlyon/ter_territoire.maison_du_rhone/all.json
- add to slots all the next opening hours in chronological order & beginning from today()
- """
- if 'properties' in data:
- data = data['properties']
- slots = []
- known_format = False
- mdr_weekdays_format = ['%s_am' % day for day in FR_WEEKDAYS] + ['%s_pm' % day for day in FR_WEEKDAYS]
- if any([re.search('|'.join(mdr_weekdays_format), data_key) is not None for data_key in data.keys()]):
- known_format = True
- for i in range(7):
- for period in ('am', 'pm'):
- hours = data.get('%s_%s' % (FR_WEEKDAYS[today.weekday()], period))
- if not hours:
- continue
- try:
- parts = re.match('(\d?\d)h(\d\d)-(\d?\d)h(\d\d)', hours).groups()
- except AttributeError:
- continue
- # add to slots the opening hours in chronological order beginning from today
- slots.append(TimeSlot(
- datetime.datetime(today.year, today.month, today.day, int(parts[0]), int(parts[1]), tzinfo=today.tzinfo),
- datetime.datetime(today.year, today.month, today.day, int(parts[2]), int(parts[3]), tzinfo=today.tzinfo)
- ))
+from ..geojson_utils import (get_open_close_from_specification, get_slots_from_mdr_format,
+ get_slots_from_mairie_format, openinghours_to_datetime, get_period_from_data)
+from ..geojson_utils import EN_ABBREV_WEEKDAYS_LIST, FR_WEEKDAYS, FR_ABBREV_WEEKDAYS_LIST
- today = today + datetime.timedelta(days=1)
- return (slots, known_format)
+register = template.Library()
-def get_slots_from_mairie_format(data, base_datetime):
- """Process mairie json and return slots the opening hours in chronological order beginning today
+@register.inclusion_tag('opening_hours_table.html')
+def as_opening_hours_table(data):
+ """Process Mairie Geojson to extract data of each day's opening hours
"""
+ if not data:
+ return ''
+
if 'properties' in data:
data = data['properties']
- known_format = False
- slots = []
- exclusion_slots = []
- if len(data.get('openinghours', [])) or len(data.get('openinghoursspecification', [])):
- known_format = True
- # prepare annual opening exclusions
- for specification in data.get('openinghoursspecification', []):
- valid_from, valid_through = (
- parse_datetime(specification.get('validFrom')),
- parse_datetime(specification.get('validThrough'))
- )
- if not valid_from or not valid_through:
- continue
- if 'opens' in specification and 'closes' in specification:
- # case when opening periods are defined
- if base_datetime >= valid_from and base_datetime < valid_through:
- opening_time = datetime.datetime.combine(base_datetime, dateutil_parse(specification['opens']).time())
- closing_time = datetime.datetime.combine(base_datetime, dateutil_parse(specification['closes']).time())
- opening_time = opening_time.replace(tzinfo=valid_from.tzinfo)
- closing_time = closing_time.replace(tzinfo=valid_from.tzinfo)
- day_of_week = WEEKDAYS.index(specification['dayOfWeek'].split('/')[-1])
- opening_time = opening_time + datetime.timedelta(
- days=(7 + (day_of_week - opening_time.weekday())) % 7)
- closing_time = closing_time + datetime.timedelta(
- days=(7 + (day_of_week - closing_time.weekday())) % 7)
- slots.append(TimeSlot(opening_time, closing_time))
- else:
- # case when exclusions are defined
- exclusion_slots.append(TimeSlot(valid_from, valid_through))
-
- for openinghours in data.get('openinghours', []):
+ end_day = None
+ base_datetime = make_aware(datetime.datetime.now())
+ opening_hours_dict = OrderedDict(zip(EN_ABBREV_WEEKDAYS_LIST, [{
+ 'am': None, 'pm': None
+ } for i in range(7)]))
+
+ for openinghours in data.get('openinghours', []):
+ try:
+ # days interval
+ parts = re.match('(\w\w)-(\w\w) (\d\d):(\d\d)-(\d\d):(\d\d)', openinghours).groups()
+ (start_day, end_day, start_hour, start_minute, end_hour, end_minute) = parts
+ except AttributeError:
try:
- parts = re.match('(\w\w)-(\w\w) (\d\d):(\d\d)-(\d\d):(\d\d)', openinghours).groups()
+ # one day
+ parts = re.match('(\w\w) (\d\d):(\d\d)-(\d\d):(\d\d)', openinghours).groups()
+ (start_day, start_hour, start_minute, end_hour, end_minute) = parts
+
except AttributeError:
- continue
- for weekday in EN_ABBREV_WEEKDAYS_LIST[
- EN_ABBREV_WEEKDAYS_LIST.index(parts[0].lower()):
- EN_ABBREV_WEEKDAYS_LIST.index(parts[1].lower())+1]:
- timeslot = TimeSlot(
- openinghours_to_datetime(weekday, int(parts[2]), int(parts[3]), default=base_datetime),
- openinghours_to_datetime(weekday, int(parts[4]), int(parts[5]), default=base_datetime)
- )
- # add to slots the opening hours in chronological order beginning from today
- slots.append(timeslot)
-
- # order slots and cycle the list beginning with 'base_datetime'
- slots = sorted(slots, key=operator.attrgetter('start'))
- if len(slots):
- def timedelta_key_func(slot):
- return slot.start - base_datetime
- nearest_slot_index = slots.index(min(slots, key=timedelta_key_func))
- slots = slots[nearest_slot_index:] + slots[:nearest_slot_index]
- return (slots, exclusion_slots, known_format)
+ try:
+ # comma-separated days list
+ parts = re.match('(\w\w(?:,\w\w)+) (\d\d):(\d\d)-(\d\d):(\d\d)', openinghours).groups()
+ start_day = parts[0].split(',')
+ (start_hour, start_minute, end_hour, end_minute) = parts[1:]
+
+ except AttributeError:
+ continue
+ if start_day and end_day:
+ # days interval
+ days_list = EN_ABBREV_WEEKDAYS_LIST[
+ EN_ABBREV_WEEKDAYS_LIST.index(start_day.lower()):
+ EN_ABBREV_WEEKDAYS_LIST.index(end_day.lower()) + 1]
+ if isinstance(start_day, unicode) and not end_day:
+ # one days
+ days_list = [EN_ABBREV_WEEKDAYS_LIST[
+ EN_ABBREV_WEEKDAYS_LIST.index(start_day.lower())]]
+ if isinstance(start_day, list) and not end_day:
+ # comma-separated days list
+ days_list = [EN_ABBREV_WEEKDAYS_LIST[
+ EN_ABBREV_WEEKDAYS_LIST.index(day.lower())] for day in start_day]
+
+ for weekday in days_list:
+ (period, all_day_hours) = get_period_from_data(weekday, start_hour, start_minute, end_hour, end_minute)
+ if all_day_hours and period == 'am':
+ opening_hours_dict[weekday]['pm'] = '' # empty string to avoid displaying fermé
+ opening_hours_dict[weekday][period] = "%sh%s-%sh%s" % (start_hour, start_minute, end_hour, end_minute)
+
+ # some mairie only have openinghoursspecification (e.g. Jonage)
+ for specification in data.get('openinghoursspecification', []):
+ valid_from, valid_through = (
+ parse_datetime(specification.get('validFrom')),
+ parse_datetime(specification.get('validThrough'))
+ )
+ if not valid_from or not valid_through:
+ continue
+ # case when opening periods are defined
+ if 'opens' in specification and 'closes' in specification:
+ # parse specification only for the current period relative to utcnow()
+ if base_datetime >= valid_from and base_datetime < valid_through:
+ (opening_time, closing_time, day_of_week) = get_open_close_from_specification(specification, valid_from, base_datetime)
+ abbr_day_of_week = EN_ABBREV_WEEKDAYS_LIST[day_of_week]
+ # TODO fix this
+ (period, all_day_hours) = get_period_from_data(weekday, start_hour, start_minute, end_hour, end_minute)
+ if all_day_hours and period == 'am':
+ opening_hours_dict[weekday]['pm'] = '' # empty string to avoid displaying fermé
+
+ opening_hours_dict[abbr_day_of_week][period] = "%sh%s-%sh%s" % (
+ opening_time.strftime('%H'), opening_time.strftime('%M'),
+ closing_time.strftime('%H'), closing_time.strftime('%M'))
+
+ return {
+ 'opening_hours':[
+ (FR_ABBREV_WEEKDAYS_LIST[weekday], hours) for weekday, hours in opening_hours_dict.items()
+ if not (weekday in ['sa', 'su'] and not hours['am'] and not hours['pm'])
+ ]
+ }
@register.filter