From 80ae4bd91c900fb32b87a0fa3d1056afaa2bc6e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BE=9D=E7=91=AA=E8=B2=93?= Date: Tue, 4 Jun 2024 08:23:15 +0800 Subject: [PATCH] Revised the calculation of "today" to use the client's timezone instead of the server's timezone. --- src/accounting/journal_entry/views.py | 5 ++- src/accounting/report/period/chooser.py | 5 ++- src/accounting/report/period/shortcuts.py | 17 +++++---- src/accounting/static/js/timezone.js | 37 +++++++++++++++++++ src/accounting/template_filters.py | 5 ++- src/accounting/templates/accounting/base.html | 3 +- src/accounting/utils/timezone.py | 37 +++++++++++++++++++ tests/test_site/lib.py | 19 ++++++++-- tests/test_site/reset.py | 7 ++-- 9 files changed, 113 insertions(+), 22 deletions(-) create mode 100644 src/accounting/static/js/timezone.js create mode 100644 src/accounting/utils/timezone.py diff --git a/src/accounting/journal_entry/views.py b/src/accounting/journal_entry/views.py index 6258d18..6795152 100644 --- a/src/accounting/journal_entry/views.py +++ b/src/accounting/journal_entry/views.py @@ -1,7 +1,7 @@ # The Mia! Accounting Project. # Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18 -# Copyright (c) 2023 imacat. +# Copyright (c) 2023-2024 imacat. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ from accounting.utils.flash_errors import flash_form_errors from accounting.utils.journal_entry_types import JournalEntryType from accounting.utils.next_uri import inherit_next, or_next from accounting.utils.permission import has_permission, can_view, can_edit +from accounting.utils.timezone import get_tz_today from accounting.utils.user import get_current_user_pk from .forms import sort_journal_entries_in, JournalEntryReorderForm from .template_filters import with_type, to_transfer, format_amount_input, \ @@ -67,7 +68,7 @@ def show_add_journal_entry_form(journal_entry_type: JournalEntryType) -> str: form.validate() else: form = journal_entry_op.form() - form.date.data = dt.date.today() + form.date.data = get_tz_today() return journal_entry_op.render_create_template(form) diff --git a/src/accounting/report/period/chooser.py b/src/accounting/report/period/chooser.py index 4fd4f14..ae300df 100644 --- a/src/accounting/report/period/chooser.py +++ b/src/accounting/report/period/chooser.py @@ -1,7 +1,7 @@ # The Mia! Accounting Project. # Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4 -# Copyright (c) 2023 imacat. +# Copyright (c) 2023-2024 imacat. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import datetime as dt from collections.abc import Callable from accounting.models import JournalEntry +from accounting.utils.timezone import get_tz_today from .period import Period from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \ LastYear, Today, Yesterday, AllTime, TemplatePeriod, YearPeriod @@ -80,7 +81,7 @@ class PeriodChooser: """The available years.""" if self.has_data: - today: dt.date = dt.date.today() + today: dt.date = get_tz_today() self.has_last_month = start < dt.date(today.year, today.month, 1) self.has_last_year = start.year < today.year self.has_yesterday = start < today diff --git a/src/accounting/report/period/shortcuts.py b/src/accounting/report/period/shortcuts.py index b08ac77..827cead 100644 --- a/src/accounting/report/period/shortcuts.py +++ b/src/accounting/report/period/shortcuts.py @@ -1,7 +1,7 @@ # The Mia! Accounting Project. # Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4 -# Copyright (c) 2023 imacat. +# Copyright (c) 2023-2024 imacat. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import datetime as dt from accounting.locale import gettext +from accounting.utils.timezone import get_tz_today from .month_end import month_end from .period import Period @@ -27,7 +28,7 @@ from .period import Period class ThisMonth(Period): """The period of this month.""" def __init__(self): - today: dt.date = dt.date.today() + today: dt.date = get_tz_today() this_month_start: dt.date = dt.date(today.year, today.month, 1) super().__init__(this_month_start, month_end(today)) self.is_default = True @@ -43,7 +44,7 @@ class ThisMonth(Period): class LastMonth(Period): """The period of this month.""" def __init__(self): - today: dt.date = dt.date.today() + today: dt.date = get_tz_today() year: int = today.year month: int = today.month - 1 if month < 1: @@ -63,7 +64,7 @@ class LastMonth(Period): class SinceLastMonth(Period): """The period of this month.""" def __init__(self): - today: dt.date = dt.date.today() + today: dt.date = get_tz_today() year: int = today.year month: int = today.month - 1 if month < 1: @@ -82,7 +83,7 @@ class SinceLastMonth(Period): class ThisYear(Period): """The period of this year.""" def __init__(self): - year: int = dt.date.today().year + year: int = get_tz_today().year start: dt.date = dt.date(year, 1, 1) end: dt.date = dt.date(year, 12, 31) super().__init__(start, end) @@ -97,7 +98,7 @@ class ThisYear(Period): class LastYear(Period): """The period of last year.""" def __init__(self): - year: int = dt.date.today().year + year: int = get_tz_today().year start: dt.date = dt.date(year - 1, 1, 1) end: dt.date = dt.date(year - 1, 12, 31) super().__init__(start, end) @@ -112,7 +113,7 @@ class LastYear(Period): class Today(Period): """The period of today.""" def __init__(self): - today: dt.date = dt.date.today() + today: dt.date = get_tz_today() super().__init__(today, today) self.is_today = True @@ -125,7 +126,7 @@ class Today(Period): class Yesterday(Period): """The period of yesterday.""" def __init__(self): - yesterday: dt.date = dt.date.today() - dt.timedelta(days=1) + yesterday: dt.date = get_tz_today() - dt.timedelta(days=1) super().__init__(yesterday, yesterday) self.is_yesterday = True diff --git a/src/accounting/static/js/timezone.js b/src/accounting/static/js/timezone.js new file mode 100644 index 0000000..3c00489 --- /dev/null +++ b/src/accounting/static/js/timezone.js @@ -0,0 +1,37 @@ +/* The Mia! Accounting Project + * timezone.js: The JavaScript for the timezone + */ + +/* Copyright (c) 2024 imacat. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Author: imacat@mail.imacat.idv.tw (imacat) + * First written: 2024/6/4 + */ +"use strict"; + +// Initializes the page JavaScript. +document.addEventListener("DOMContentLoaded", () => { + setTimeZone(); +}); + +/** + * Sets the time zone. + * + * @private + */ +function setTimeZone() { + document.cookie = `accounting-tz=${Intl.DateTimeFormat().resolvedOptions().timeZone}; SameSite=Strict`; +} diff --git a/src/accounting/template_filters.py b/src/accounting/template_filters.py index 8005911..1faa62d 100644 --- a/src/accounting/template_filters.py +++ b/src/accounting/template_filters.py @@ -1,7 +1,7 @@ # The Mia! Accounting Project. # Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/25 -# Copyright (c) 2023 imacat. +# Copyright (c) 2023-2024 imacat. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ from typing import Any from flask_babel import get_locale from accounting.locale import gettext +from accounting.utils.timezone import get_tz_today def format_amount(value: Decimal | None) -> str | None: @@ -47,7 +48,7 @@ def format_date(value: dt.date) -> str: :param value: The date. :return: The human-friendly date text. """ - today: dt.date = dt.date.today() + today: dt.date = get_tz_today() if value == today: return gettext("Today") if value == today - dt.timedelta(days=1): diff --git a/src/accounting/templates/accounting/base.html b/src/accounting/templates/accounting/base.html index 6aff78a..cad63af 100644 --- a/src/accounting/templates/accounting/base.html +++ b/src/accounting/templates/accounting/base.html @@ -2,7 +2,7 @@ The Mia! Accounting Project base.html: The application-wide base template. - Copyright (c) 2023 imacat. + Copyright (c) 2023-2024 imacat. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -27,5 +27,6 @@ First written: 2023/1/27 {% block scripts %} + {% block accounting_scripts %}{% endblock %} {% endblock %} diff --git a/src/accounting/utils/timezone.py b/src/accounting/utils/timezone.py new file mode 100644 index 0000000..8cdda19 --- /dev/null +++ b/src/accounting/utils/timezone.py @@ -0,0 +1,37 @@ +# The Mia! Accounting Project. +# Author: imacat@mail.imacat.idv.tw (imacat), 2024/6/4 + +# Copyright (c) 2024 imacat. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The timezone utility. + +This module should not import any other module from the application. + +""" + +import datetime as dt + +import pytz +from flask import request + + +def get_tz_today() -> dt.date: + """Returns today in the client timezone. + + :return: today in the client timezone. + """ + tz_name: str | None = request.cookies.get("accounting-tz") + if tz_name is None: + return dt.date.today() + return dt.datetime.now(tz=pytz.timezone(tz_name)).date() diff --git a/tests/test_site/lib.py b/tests/test_site/lib.py index ea460d2..821132a 100644 --- a/tests/test_site/lib.py +++ b/tests/test_site/lib.py @@ -1,7 +1,7 @@ # The Mia! Accounting Demonstration Website. # Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/13 -# Copyright (c) 2023 imacat. +# Copyright (c) 2023-2024 imacat. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ from typing import Any import sqlalchemy as sa from flask import Flask +from accounting.utils.timezone import get_tz_today from . import db from .auth import User @@ -44,6 +45,17 @@ class Accounts: MEAL: str = "6272-001" +def get_today() -> dt.date: + """Returns today, based on the context. + + :return: Today. + """ + try: + return get_tz_today() + except RuntimeError: + return dt.date.today() + + class JournalEntryLineItemData: """The journal entry line item data.""" @@ -183,7 +195,7 @@ class JournalEntryData: :param is_update: True for an update operation, or False otherwise :return: The journal entry as a form. """ - date: dt.date = dt.date.today() - dt.timedelta(days=self.days) + date: dt.date = get_today() - dt.timedelta(days=self.days) form: dict[str, str] = {"csrf_token": csrf_token, "next": encoded_next_uri, "date": date.isoformat()} @@ -260,8 +272,7 @@ class BaseTestData(ABC): existing_j_id: set[int] = {x["id"] for x in self.__journal_entries} existing_l_id: set[int] = {x["id"] for x in self.__line_items} journal_entry_data.id = self.__new_id(existing_j_id) - date: dt.date \ - = dt.date.today() - dt.timedelta(days=journal_entry_data.days) + date: dt.date = get_today() - dt.timedelta(days=journal_entry_data.days) self.__journal_entries.append( {"id": journal_entry_data.id, "date": date, diff --git a/tests/test_site/reset.py b/tests/test_site/reset.py index 72c7512..85ec4c3 100644 --- a/tests/test_site/reset.py +++ b/tests/test_site/reset.py @@ -1,7 +1,7 @@ # The Mia! Accounting Demonstration Website. # Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/12 -# Copyright (c) 2023 imacat. +# Copyright (c) 2023-2024 imacat. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ from flask import Flask, Blueprint, url_for, flash, redirect, session, \ render_template, current_app from flask_babel import lazy_gettext +from accounting.utils.timezone import get_tz_today from . import db from .auth import admin_required from .lib import Accounts, JournalEntryLineItemData, JournalEntryData, \ @@ -117,7 +118,7 @@ class SampleData(BaseTestData): :return: None. """ - today: dt.date = dt.date.today() + today: dt.date = get_tz_today() days: int year: int month: int @@ -160,7 +161,7 @@ class SampleData(BaseTestData): :return: None. """ - today: dt.date = dt.date.today() + today: dt.date = get_tz_today() year: int = today.year - 5 month: int = today.month