mairie template support as_opening_hours template tag (#22050)
This commit is contained in:
parent
1c591b3587
commit
622f15044a
|
@ -1,2 +1,3 @@
|
||||||
*.pyc
|
*.pyc
|
||||||
*.egg-info
|
*.egg-info
|
||||||
|
.pytest_cache/
|
||||||
|
|
|
@ -15,20 +15,24 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
import datetime
|
import datetime
|
||||||
import math
|
import math
|
||||||
|
import operator
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import urllib2
|
import urllib2
|
||||||
|
|
||||||
|
from dateutil.parser import parse as dateutil_parse
|
||||||
from requests import RequestException
|
from requests import RequestException
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.dateparse import parse_datetime
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
from django.utils.timezone import is_naive, make_aware
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from combo.apps.dashboard.models import DashboardCell
|
|
||||||
from combo.apps.maps.models import MapLayer
|
from combo.apps.maps.models import MapLayer
|
||||||
from combo.data.models import Page, ConfigJsonCell
|
from combo.data.models import Page, ConfigJsonCell
|
||||||
from combo.public.views import render_cell
|
from combo.public.views import render_cell
|
||||||
|
@ -36,46 +40,156 @@ from combo.utils import requests
|
||||||
|
|
||||||
register = template.Library()
|
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')
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
class TimeSlot(object):
|
class TimeSlot(object):
|
||||||
def __init__(self, start, end):
|
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.start = start
|
||||||
self.end = end
|
self.end = end
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<TimeSlot start=%s - end=%s>' % (self.start.strftime('%c'), self.end.strftime('%c'))
|
||||||
|
|
||||||
@register.filter
|
|
||||||
def as_opening_hours_badge(data):
|
def openinghours_to_datetime(codename, hour, minute, default=None):
|
||||||
if not data:
|
try:
|
||||||
return ''
|
weekday = EN_ABBREV_WEEKDAYS.get(codename, None)
|
||||||
weekdays = ['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche']
|
# 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()
|
||||||
|
"""
|
||||||
slots = []
|
slots = []
|
||||||
today = datetime.date.today()
|
known_format = False
|
||||||
for i in range(7):
|
mdr_weekdays_format = ['%s_am' % day for day in FR_WEEKDAYS] + ['%s_pm' % day for day in FR_WEEKDAYS]
|
||||||
for period in ('am', 'pm'):
|
if any([re.search('|'.join(mdr_weekdays_format), data_key) is not None for data_key in data.keys()]):
|
||||||
hours = data.get('%s_%s' % (weekdays[today.weekday()], period))
|
known_format = True
|
||||||
if not hours:
|
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])),
|
||||||
|
datetime.datetime(today.year, today.month, today.day, int(parts[2]), int(parts[3]))
|
||||||
|
))
|
||||||
|
|
||||||
|
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', [])) and 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
|
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:
|
||||||
|
slots.append(TimeSlot(dateutil_parse(specification['opens']), dateutil_parse(specification['closes'])))
|
||||||
|
else:
|
||||||
|
# case when exclusions are defined
|
||||||
|
exclusion_slots.append(TimeSlot(valid_from, valid_through))
|
||||||
|
|
||||||
|
for openinghours in data.get('openinghours', []):
|
||||||
try:
|
try:
|
||||||
parts = re.match('(\d?\d)h(\d\d)-(\d?\d)h(\d\d)', hours).groups()
|
parts = re.match('(\w\w)-(\w\w) (\d\d):(\d\d)-(\d\d):(\d\d)', openinghours).groups()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
continue
|
continue
|
||||||
slots.append(TimeSlot(
|
for weekday in EN_ABBREV_WEEKDAYS.keys()[
|
||||||
datetime.datetime(today.year, today.month, today.day, int(parts[0]), int(parts[1])),
|
EN_ABBREV_WEEKDAYS.keys().index(parts[0].lower()):
|
||||||
datetime.datetime(today.year, today.month, today.day, int(parts[2]), int(parts[3]))))
|
EN_ABBREV_WEEKDAYS.keys().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)
|
||||||
|
|
||||||
today = today + datetime.timedelta(days=1)
|
# 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)
|
||||||
|
|
||||||
now = datetime.datetime.now()
|
|
||||||
# remove past slots
|
@register.filter
|
||||||
|
def as_opening_hours_badge(data, base_datetime=None):
|
||||||
|
if not data:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
if base_datetime is None:
|
||||||
|
base_datetime = make_aware(datetime.datetime.now())
|
||||||
|
|
||||||
|
# defaults
|
||||||
|
exclusion_slots = []
|
||||||
|
today = base_datetime.date()
|
||||||
|
|
||||||
|
(slots, known_format) = get_slots_from_mdr_format(data, today)
|
||||||
|
if not known_format:
|
||||||
|
(slots, exclusion_slots, known_format) = get_slots_from_mairie_format(data, base_datetime)
|
||||||
|
|
||||||
|
if not known_format:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
# remove past slots and exclude special timeslots
|
||||||
for i, slot in enumerate(slots):
|
for i, slot in enumerate(slots):
|
||||||
if now > slot.end:
|
if base_datetime > slot.end:
|
||||||
slots[i] = None
|
slots[i] = None
|
||||||
slots = [x for x in slots if x]
|
else:
|
||||||
|
for exclusion in exclusion_slots:
|
||||||
|
if slot.start >= exclusion.start and slot.end <= exclusion.end:
|
||||||
|
slots[i] = None
|
||||||
|
|
||||||
|
# parse slots to return the right html
|
||||||
|
slots = [x for x in slots if x]
|
||||||
if not slots:
|
if not slots:
|
||||||
klass = 'closed'
|
klass = 'closed'
|
||||||
label = u'Fermé'
|
label = u'Fermé'
|
||||||
elif now < slots[0].start:
|
elif base_datetime < slots[0].start:
|
||||||
klass = 'closed'
|
klass = 'closed'
|
||||||
verb = u'Réouvre'
|
verb = u'Réouvre'
|
||||||
if slots[0].start.weekday() == today.weekday():
|
if slots[0].start.weekday() == today.weekday():
|
||||||
|
@ -85,16 +199,17 @@ def as_opening_hours_badge(data):
|
||||||
elif slots[0].start.weekday() == (today.weekday() + 1) % 7:
|
elif slots[0].start.weekday() == (today.weekday() + 1) % 7:
|
||||||
day_label = u'demain'
|
day_label = u'demain'
|
||||||
else:
|
else:
|
||||||
day_label = weekdays[slots[0].start.weekday()]
|
day_label = FR_WEEKDAYS[slots[0].start.weekday()]
|
||||||
label = u'%s %s à %sh%02d' % (verb, day_label, slots[0].start.hour, slots[0].start.minute)
|
label = u'%s %s à %sh%02d' % (verb, day_label, slots[0].start.hour, slots[0].start.minute)
|
||||||
elif now < slots[0].end:
|
elif base_datetime < slots[0].end:
|
||||||
if (slots[0].end - now).seconds < 3600:
|
if (slots[0].end - base_datetime).seconds < 3600:
|
||||||
klass = 'soon-to-be-closed'
|
klass = 'soon-to-be-closed'
|
||||||
else:
|
else:
|
||||||
klass = 'open'
|
klass = 'open'
|
||||||
label = u"Ouvert jusqu'à %sh%02d" % (slots[0].end.hour, slots[0].end.minute)
|
label = u"Ouvert jusqu'à %sh%02d" % (slots[0].end.hour, slots[0].end.minute)
|
||||||
|
|
||||||
return mark_safe('<div class="badge %s"><span>%s</span></div>' % (klass, label))
|
return mark_safe(u'<div class="badge %s"><span>%s</span></div>' % (klass, label))
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def onlymoov_duration(string):
|
def onlymoov_duration(string):
|
||||||
|
@ -102,7 +217,7 @@ def onlymoov_duration(string):
|
||||||
# onlymoov, "PT1H16M3S", "PT1M35S"
|
# onlymoov, "PT1H16M3S", "PT1M35S"
|
||||||
try:
|
try:
|
||||||
groups = re.match(r'PT(\d+H)?(\d+M)', string).groups()
|
groups = re.match(r'PT(\d+H)?(\d+M)', string).groups()
|
||||||
except AttributeError: # didn't match
|
except AttributeError: # didn't match
|
||||||
return '?'
|
return '?'
|
||||||
hours = ''
|
hours = ''
|
||||||
if groups[0]:
|
if groups[0]:
|
||||||
|
|
4
setup.py
4
setup.py
|
@ -94,7 +94,9 @@ setup(
|
||||||
'Programming Language :: Python',
|
'Programming Language :: Python',
|
||||||
'Programming Language :: Python :: 2',
|
'Programming Language :: Python :: 2',
|
||||||
],
|
],
|
||||||
install_requires=['django>=1.8',
|
install_requires=[
|
||||||
|
'django>=1.8, <1.12',
|
||||||
|
'python-dateutil',
|
||||||
],
|
],
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
entry_points={
|
entry_points={
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,48 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from dateutil.parser import parse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
from combo_plugin_gnm.templatetags.gnm import as_opening_hours_badge
|
||||||
|
|
||||||
|
|
||||||
|
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||||
|
GEOJSON = json.load(open(os.path.join(
|
||||||
|
BASE_DIR, 'tests/data/mairie-geojson.json')))['features']
|
||||||
|
TZOFFSETS = {"Europe/Paris": 3600}
|
||||||
|
|
||||||
|
|
||||||
|
def test_every_mairie_closed_at_midnight():
|
||||||
|
"""every mairie is closed at mignight"""
|
||||||
|
midnight = parse("2018-03-05T00:00:00+01:00")
|
||||||
|
opening_hours = [as_opening_hours_badge(x, base_datetime=midnight) for x in GEOJSON]
|
||||||
|
assert len([x for x in opening_hours if 'open' in x]) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_mairie_data_parsed_correct():
|
||||||
|
"""everything got parsed correctly"""
|
||||||
|
opening_hours = [as_opening_hours_badge(x) for x in GEOJSON
|
||||||
|
if x['properties'].get('openinghours') or x['properties'].get('openinghoursspecification')]
|
||||||
|
# actually not everything, Jonage and Solaize are not handled correctly as
|
||||||
|
# they only use openinghoursspecification → 2
|
||||||
|
assert opening_hours.count('') == 2 # FIXME
|
||||||
|
|
||||||
|
|
||||||
|
def test_mairie_nodata_as_opening_hours_templatetag():
|
||||||
|
""""no data return the right empty html"""
|
||||||
|
test_datetime = parse("2018-03-05T15:59:00+01:00")
|
||||||
|
test_html = as_opening_hours_badge(GEOJSON[0]['properties'], base_datetime=test_datetime)
|
||||||
|
assert test_html == ''
|
||||||
|
|
||||||
|
|
||||||
|
def test_mairie_open_as_opening_hours_templatetag():
|
||||||
|
"""mairie as_opening_hours with a fixed datetime"""
|
||||||
|
klass = 'open'
|
||||||
|
label = u"Ouvert jusqu'à 17h00"
|
||||||
|
test_datetime = parse("2018-03-05T15:55:00+01:00")
|
||||||
|
test_html = as_opening_hours_badge(GEOJSON[1]['properties'], base_datetime=test_datetime)
|
||||||
|
assert test_html == mark_safe(u'<div class="badge %s"><span>%s</span></div>'% (klass, label))
|
||||||
|
test_html = as_opening_hours_badge(GEOJSON[1], base_datetime=test_datetime)
|
||||||
|
assert test_html == mark_safe(u'<div class="badge %s"><span>%s</span></div>'% (klass, label))
|
Loading…
Reference in New Issue