"""Provides various utilities that are useful across the project"""
import datetime
import hmac
from _sha1 import sha1
from base64 import urlsafe_b64decode, urlsafe_b64encode
from collections import namedtuple
from django.conf import settings
from django.template.defaultfilters import urlencode
from django.utils import timezone, dateparse
from rest_framework.exceptions import ParseError
[docs]def dict2obj(d, name="Object"):
return namedtuple(name, d.keys())(*d.values())
[docs]def datetime_to_lectureyear(date):
"""Convert a :class:`~datetime.date` to the start of the lectureyear
>>> from datetime import date, datetime, timezone
>>> nov_23 = date(1990, 11, 7)
>>> datetime_to_lectureyear(nov_23)
1990
>>> mar_2 = date(1993, 3, 2)
>>> datetime_to_lectureyear(mar_2)
1992
Also works on :class:`~datetime.datetime`, but they need to be tz-aware:
>>> new_year = datetime(2000, 1, 1, tzinfo=timezone.utc)
>>> datetime_to_lectureyear(new_year)
1999
"""
if isinstance(date, timezone.datetime):
date = timezone.localtime(date).date()
sept_1 = timezone.make_aware(timezone.datetime(date.year, 9, 1))
if date < sept_1.date():
return date.year - 1
return date.year
[docs]def create_google_maps_url(location, zoom, size):
maps_url = (
f"/maps/api/staticmap?"
f"center={ urlencode(location) }&"
f"zoom={ zoom }&size={ size }&"
f"markers={ urlencode(location) }&"
f"key={ settings.GOOGLE_MAPS_API_KEY }"
)
decoded_key = urlsafe_b64decode(settings.GOOGLE_MAPS_API_SECRET)
signature = hmac.new(decoded_key, maps_url.encode(), sha1)
encoded_signature = urlsafe_b64encode(signature.digest())
maps_url += f"&signature={encoded_signature.decode('utf-8')}"
return "https://maps.googleapis.com" + maps_url
def _extract_date(param):
"""Extract the date from an arbitrary string"""
if param is None:
return None
try:
return dateparse.parse_datetime(param)
except ValueError:
return dateparse.parse_date(param)
[docs]def overlaps(check, others, can_equal=True):
"""Check for overlapping date ranges
This works by checking the maximum of the two `since` times, and the minimum of
the two `until` times. Because there are no infinite dates, the value date_max
is created for when the `until` value is None; this signifies a timespan that
has not ended yet and is the maximum possible date in Python's datetime.
The ranges overlap when the maximum start time is smaller than the minimum
end time, as can be seen in this example of two integer ranges:
check: . . . .[4]. . . . 9
other: . . 2 . .[5]. . . .
check: . . . .[4]. . . . 9
other: . . 2 . . . . . . . [date_max]
And when non overlapping:
check: . . . . . .[6] . . 9
other: . . 2 . .[5]. . . .
4 < 5 == True so these intervals overlap, while 6 < 5 == False so these intervals
don't overlap
The can_equal argument is used for boards, where the end date can't be the same
as the start date.
>>> overlaps( \
dict2obj({ \
'pk': 1 \
, 'since': datetime.date(2018, 12, 1) \
, 'until': datetime.date(2019, 1, 1) \
}) \
, [dict2obj({ \
'pk': 2 \
, 'since': datetime.date(2019, 1, 1) \
, 'until': datetime.date(2019, 1, 31) \
})])
False
>>> overlaps( \
dict2obj({ \
'pk': 1 \
, 'since': datetime.date(2018, 12, 1) \
, 'until': datetime.date(2019, 1, 1) \
}) \
, [dict2obj({ \
'pk': 2 \
, 'since': datetime.date(2019, 1, 1) \
, 'until': datetime.date(2019, 1, 31) \
})], False)
True
>>> overlaps( \
dict2obj({ \
'pk': 1 \
, 'since': datetime.date(2018, 12, 1) \
, 'until': datetime.date(2019, 1, 2) \
}) \
, [dict2obj({ \
'pk': 2 \
, 'since': datetime.date(2019, 1, 1) \
, 'until': datetime.date(2019, 1, 31) \
})])
True
"""
date_max = datetime.date(datetime.MAXYEAR, 12, 31)
for other in others:
if check.pk == other.pk:
# No checks for the object we're validating
continue
max_start = max(check.since, other.since)
min_end = min(check.until or date_max, other.until or date_max)
if max_start == min_end and not can_equal:
return True
if max_start < min_end:
return True
return False