From 9052688ed247bccda516ffa84183b7ed442f0a04 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 25 Jan 2021 16:52:14 -0800 Subject: [PATCH] add aurweb.time module This module includes timezone-based utilities for a FastAPI request. This commit introduces use of the AURTZ cookie within get_request_timezone. This cookie should be set to the user or session's timezone. * `make_context` has been modified to parse the request's timezone and include the "timezone" and "timezones" variables, along with a timezone specified "now" date. + Added `Timezone` attribute to aurweb.testing.requests.Request.user. Signed-off-by: Kevin Morris --- aurweb/templates.py | 11 ++++--- aurweb/testing/requests.py | 1 + aurweb/time.py | 63 ++++++++++++++++++++++++++++++++++++++ test/test_time.py | 33 ++++++++++++++++++++ 4 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 aurweb/time.py create mode 100644 test/test_time.py diff --git a/aurweb/templates.py b/aurweb/templates.py index c5f378b8..564f3149 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -1,5 +1,6 @@ import copy import os +import zoneinfo from datetime import datetime from http import HTTPStatus @@ -11,7 +12,7 @@ from fastapi.responses import HTMLResponse import aurweb.config -from aurweb import l10n +from aurweb import l10n, time # Prepare jinja2 objects. loader = jinja2.FileSystemLoader(os.path.join( @@ -26,14 +27,15 @@ env.filters["tr"] = l10n.tr def make_context(request: Request, title: str, next: str = None): """ Create a context for a jinja2 TemplateResponse. """ + timezone = time.get_request_timezone(request) return { "request": request, "language": l10n.get_request_language(request), "languages": l10n.SUPPORTED_LANGUAGES, + "timezone": timezone, + "timezones": time.SUPPORTED_TIMEZONES, "title": title, - # The 'now' context variable will not show proper datetimes - # until we've implemented timezone support here. - "now": datetime.now(), + "now": datetime.now(tz=zoneinfo.ZoneInfo(timezone)), "config": aurweb.config, "next": next if next else request.url.path } @@ -60,4 +62,5 @@ def render_template(request: Request, response = HTMLResponse(rendered, status_code=status_code) response.set_cookie("AURLANG", context.get("language")) + response.set_cookie("AURTZ", context.get("timezone")) return response diff --git a/aurweb/testing/requests.py b/aurweb/testing/requests.py index 2e64fd3d..9976b6fb 100644 --- a/aurweb/testing/requests.py +++ b/aurweb/testing/requests.py @@ -5,6 +5,7 @@ class User: """ A fake User model. """ # Fake columns. LangPreference = aurweb.config.get("options", "default_lang") + Timezone = aurweb.config.get("options", "default_timezone") # A fake authenticated flag. authenticated = False diff --git a/aurweb/time.py b/aurweb/time.py new file mode 100644 index 00000000..0b1dff11 --- /dev/null +++ b/aurweb/time.py @@ -0,0 +1,63 @@ +import zoneinfo + +from collections import OrderedDict +from datetime import datetime + +from fastapi import Request + +import aurweb.config + + +def tz_offset(name: str): + """ Get a timezone offset in the form "+00:00" by its name. + + Example: tz_offset('America/Los_Angeles') + + :param name: Timezone name + :return: UTC offset in the form "+00:00" + """ + dt = datetime.now(tz=zoneinfo.ZoneInfo(name)) + + # Our offset in hours. + offset = dt.utcoffset().total_seconds() / 60 / 60 + + # Prefix the offset string with a - or +. + offset_string = '-' if offset < 0 else '+' + + # Remove any negativity from the offset. We want a good offset. :) + offset = abs(offset) + + # Truncate the floating point digits, giving the hours. + hours = int(offset) + + # Subtract hours from the offset, and multiply the remaining fraction + # (0 - 0.99[repeated]) with 60 minutes to get the number of minutes + # remaining in the hour. + minutes = int((offset - hours) * 60) + + # Pad the hours and minutes by two places. + offset_string += "{:0>2}:{:0>2}".format(hours, minutes) + return offset_string + + +SUPPORTED_TIMEZONES = OrderedDict({ + # Flatten out the list of tuples into an OrderedDict. + timezone: offset for timezone, offset in sorted([ + # Comprehend a list of tuples (timezone, offset display string) + # and sort them by (offset, timezone). + (tz, "(UTC%s) %s" % (tz_offset(tz), tz)) + for tz in zoneinfo.available_timezones() + ], key=lambda element: (tz_offset(element[0]), element[0])) +}) + + +def get_request_timezone(request: Request): + """ Get a request's timezone by its AURTZ cookie. We use the + configuration's [options] default_timezone otherwise. + + @param request FastAPI request + """ + if request.user.is_authenticated(): + return request.user.Timezone + default_tz = aurweb.config.get("options", "default_timezone") + return request.cookies.get("AURTZ", default_tz) diff --git a/test/test_time.py b/test/test_time.py new file mode 100644 index 00000000..2134d217 --- /dev/null +++ b/test/test_time.py @@ -0,0 +1,33 @@ +import aurweb.config + +from aurweb.testing.requests import Request +from aurweb.time import get_request_timezone, tz_offset + + +def test_tz_offset_utc(): + offset = tz_offset("UTC") + assert offset == "+00:00" + + +def test_tz_offset_mst(): + offset = tz_offset("MST") + assert offset == "-07:00" + + +def test_request_timezone(): + request = Request() + tz = get_request_timezone(request) + assert tz == aurweb.config.get("options", "default_timezone") + + +def test_authenticated_request_timezone(): + # Modify a fake request to be authenticated with the + # America/Los_Angeles timezone. + request = Request() + request.user.authenticated = True + request.user.Timezone = "America/Los_Angeles" + + # Get the request's timezone, it should be America/Los_Angeles. + tz = get_request_timezone(request) + assert tz == request.user.Timezone + assert tz == "America/Los_Angeles"