add template tag as_opening_hour_table to display mairie hours (#22469)
This commit is contained in:
parent
266ede325f
commit
4dd11066d7
|
@ -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)
|
|
@ -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é"}}</td><td>{{hours.pm|default_if_none:"fermé"}}</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue