2023-08-23 08:54:25 +00:00
|
|
|
import logging
|
2022-03-27 09:47:45 +00:00
|
|
|
import re
|
2024-03-16 22:42:46 +00:00
|
|
|
import unicodedata
|
2024-09-16 10:48:19 +00:00
|
|
|
import urllib.parse
|
2024-09-19 17:29:01 +00:00
|
|
|
import datetime
|
2021-04-06 21:38:15 +00:00
|
|
|
from typing import Optional
|
2021-03-31 07:08:19 +00:00
|
|
|
|
|
|
|
from dateutil.relativedelta import relativedelta
|
2024-09-16 10:48:19 +00:00
|
|
|
from django.http import HttpResponseRedirect
|
2021-03-31 07:08:19 +00:00
|
|
|
from django.template.defaultfilters import pluralize
|
|
|
|
from django.utils import timezone, formats
|
|
|
|
|
2023-08-23 08:54:25 +00:00
|
|
|
try:
|
|
|
|
with open("version.txt", "r") as f:
|
|
|
|
app_version = f.read().strip("\n")
|
|
|
|
except Exception as exc:
|
|
|
|
logging.exception(exc)
|
2024-01-27 10:29:16 +00:00
|
|
|
app_version = ""
|
2023-08-23 08:54:25 +00:00
|
|
|
|
2021-03-31 07:08:19 +00:00
|
|
|
|
2021-01-02 10:30:20 +00:00
|
|
|
def unique(elements, key):
|
|
|
|
return list({key(element): element for element in elements}.values())
|
2021-03-31 07:08:19 +00:00
|
|
|
|
|
|
|
|
|
|
|
weekday_names = {
|
2024-01-27 10:29:16 +00:00
|
|
|
1: "Monday",
|
|
|
|
2: "Tuesday",
|
|
|
|
3: "Wednesday",
|
|
|
|
4: "Thursday",
|
|
|
|
5: "Friday",
|
|
|
|
6: "Saturday",
|
|
|
|
7: "Sunday",
|
2021-03-31 07:08:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2024-09-19 17:29:01 +00:00
|
|
|
def humanize_absolute_date(
|
|
|
|
value: datetime.datetime, now: Optional[datetime.datetime] = None
|
|
|
|
):
|
2021-04-06 21:38:15 +00:00
|
|
|
if not now:
|
|
|
|
now = timezone.now()
|
2021-03-31 07:08:19 +00:00
|
|
|
delta = relativedelta(now, value)
|
|
|
|
yesterday = now - relativedelta(days=1)
|
|
|
|
|
|
|
|
is_older_than_a_week = delta.years > 0 or delta.months > 0 or delta.weeks > 0
|
|
|
|
|
|
|
|
if is_older_than_a_week:
|
2024-01-27 10:29:16 +00:00
|
|
|
return formats.date_format(value, "SHORT_DATE_FORMAT")
|
2021-03-31 07:08:19 +00:00
|
|
|
elif value.day == now.day:
|
2024-01-27 10:29:16 +00:00
|
|
|
return "Today"
|
2021-03-31 07:08:19 +00:00
|
|
|
elif value.day == yesterday.day:
|
2024-01-27 10:29:16 +00:00
|
|
|
return "Yesterday"
|
2021-03-31 07:08:19 +00:00
|
|
|
else:
|
|
|
|
return weekday_names[value.isoweekday()]
|
|
|
|
|
|
|
|
|
2024-09-19 17:29:01 +00:00
|
|
|
def humanize_relative_date(
|
|
|
|
value: datetime.datetime, now: Optional[datetime.datetime] = None
|
|
|
|
):
|
2021-04-06 21:38:15 +00:00
|
|
|
if not now:
|
|
|
|
now = timezone.now()
|
2021-03-31 07:08:19 +00:00
|
|
|
delta = relativedelta(now, value)
|
|
|
|
|
|
|
|
if delta.years > 0:
|
2024-01-27 10:29:16 +00:00
|
|
|
return f"{delta.years} year{pluralize(delta.years)} ago"
|
2021-03-31 07:08:19 +00:00
|
|
|
elif delta.months > 0:
|
2024-01-27 10:29:16 +00:00
|
|
|
return f"{delta.months} month{pluralize(delta.months)} ago"
|
2021-03-31 07:08:19 +00:00
|
|
|
elif delta.weeks > 0:
|
2024-01-27 10:29:16 +00:00
|
|
|
return f"{delta.weeks} week{pluralize(delta.weeks)} ago"
|
2021-03-31 07:08:19 +00:00
|
|
|
else:
|
|
|
|
yesterday = now - relativedelta(days=1)
|
|
|
|
if value.day == now.day:
|
2024-01-27 10:29:16 +00:00
|
|
|
return "Today"
|
2021-03-31 07:08:19 +00:00
|
|
|
elif value.day == yesterday.day:
|
2024-01-27 10:29:16 +00:00
|
|
|
return "Yesterday"
|
2021-03-31 07:08:19 +00:00
|
|
|
else:
|
|
|
|
return weekday_names[value.isoweekday()]
|
2021-08-26 10:33:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
def parse_timestamp(value: str):
|
|
|
|
"""
|
|
|
|
Parses a string timestamp into a datetime value
|
|
|
|
First tries to parse the timestamp as milliseconds.
|
|
|
|
If that fails with an error indicating that the timestamp exceeds the maximum,
|
|
|
|
it tries to parse the timestamp as microseconds, and then as nanoseconds
|
|
|
|
:param value:
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
timestamp = int(value)
|
|
|
|
except ValueError:
|
2024-01-27 10:29:16 +00:00
|
|
|
raise ValueError(f"{value} is not a valid timestamp")
|
2021-08-26 10:33:54 +00:00
|
|
|
|
|
|
|
try:
|
2024-09-19 17:29:01 +00:00
|
|
|
return datetime.datetime.fromtimestamp(timestamp, datetime.UTC)
|
2021-08-26 10:33:54 +00:00
|
|
|
except (OverflowError, ValueError, OSError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
# Value exceeds the max. allowed timestamp
|
|
|
|
# Try parsing as microseconds
|
|
|
|
try:
|
2024-09-19 17:29:01 +00:00
|
|
|
return datetime.datetime.fromtimestamp(timestamp / 1000, datetime.UTC)
|
2021-08-26 10:33:54 +00:00
|
|
|
except (OverflowError, ValueError, OSError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
# Value exceeds the max. allowed timestamp
|
|
|
|
# Try parsing as nanoseconds
|
|
|
|
try:
|
2024-09-19 17:29:01 +00:00
|
|
|
return datetime.datetime.fromtimestamp(timestamp / 1000000, datetime.UTC)
|
2021-08-26 10:33:54 +00:00
|
|
|
except (OverflowError, ValueError, OSError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
# Timestamp is out of range
|
2024-01-27 10:29:16 +00:00
|
|
|
raise ValueError(f"{value} exceeds maximum value for a timestamp")
|
2022-03-25 17:29:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
def get_safe_return_url(return_url: str, fallback_url: str):
|
|
|
|
# Use fallback if URL is none or URL is not on same domain
|
2024-01-27 10:29:16 +00:00
|
|
|
if not return_url or not re.match(r"^/[a-z]+", return_url):
|
2022-03-25 17:29:54 +00:00
|
|
|
return fallback_url
|
|
|
|
return return_url
|
2024-03-16 22:42:46 +00:00
|
|
|
|
|
|
|
|
2024-09-16 10:48:19 +00:00
|
|
|
def redirect_with_query(request, redirect_url):
|
|
|
|
query_string = urllib.parse.urlencode(request.GET)
|
|
|
|
if query_string:
|
|
|
|
redirect_url += "?" + query_string
|
|
|
|
|
|
|
|
return HttpResponseRedirect(redirect_url)
|
|
|
|
|
|
|
|
|
2024-03-16 22:42:46 +00:00
|
|
|
def generate_username(email):
|
|
|
|
# taken from mozilla-django-oidc docs :)
|
|
|
|
|
|
|
|
# Using Python 3 and Django 1.11+, usernames can contain alphanumeric
|
|
|
|
# (ascii and unicode), _, @, +, . and - characters. So we normalize
|
|
|
|
# it and slice at 150 characters.
|
|
|
|
return unicodedata.normalize("NFKC", email)[:150]
|