add template tag as_opening_hour_table to display mairie hours (#22469)

This commit is contained in:
Elias Showk 2018-04-05 16:12:12 +02:00
parent 266ede325f
commit 4dd11066d7
3 changed files with 271 additions and 118 deletions

View File

@ -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)

View File

@ -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>

View File

@ -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
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
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()
@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']
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
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:
# 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:
try:
parts = re.match('(\d?\d)h(\d\d)-(\d?\d)h(\d\d)', hours).groups()
# 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
# 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)
))
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]
today = today + datetime.timedelta(days=1)
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)
return (slots, known_format)
# 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'))
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 = 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', []):
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)
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