Added Period as the template period helper, and changed PeriodParser to the inner class Period.Parser.

This commit is contained in:
依瑪貓 2020-07-08 10:52:03 +08:00
parent 8cefd0d4bf
commit dde75b9805
2 changed files with 493 additions and 192 deletions

View File

@ -18,6 +18,7 @@
"""The view controllers of the accounting application.
"""
from datetime import date
from django.http import HttpResponseRedirect, HttpResponse
@ -25,11 +26,12 @@ from django.urls import reverse
from django.utils import dateformat
from django.utils.decorators import method_decorator
from django.utils.timezone import localdate
from django.utils.translation import get_language
from django.views import generic
from django.views.decorators.http import require_GET
from accounting.models import Record
from mia_core.period import PeriodParser
from mia_core.period import Period
from mia import settings
from mia_core.digest_auth import digest_login_required
from mia_core.utils import UrlBuilder, Pagination, \
@ -70,12 +72,12 @@ class BaseReportView(generic.ListView):
Attributes:
page_no (int): The specified page number
page_size (int): The specified page size
period_parser (PeriodParser): The period parser
period (Period): The template period helper
"""
page_no = None
page_size = None
pagination = None
period_parser = None
period = None
def get(self, request, *args, **kwargs):
"""Adds object_list to the context.
@ -125,7 +127,7 @@ class BaseReportView(generic.ListView):
def get_context_data(self, **kwargs):
data = super(BaseReportView, self).get_context_data(**kwargs)
data["period"] = self.period_parser
data["period"] = self.period
data["pagination_links"] = self.pagination.links
return data
@ -142,7 +144,9 @@ class CashReportView(BaseReportView):
Returns:
List[Record]: The accounting records for the cash report
"""
self.period_parser = PeriodParser(self.kwargs["period_spec"])
self.period = Period(
get_language(), date(2000, 1, 1), date(2030, 12, 31),
self.kwargs["period_spec"])
if self.kwargs["subject_code"] == "0":
records = Record.objects.raw(
"""SELECT r.*
@ -174,7 +178,7 @@ ORDER BY
t.ord,
CASE WHEN is_credit THEN 1 ELSE 2 END,
r.ord""",
[self.period_parser.start, self.period_parser.end])
[self.period.start, self.period.end])
else:
records = Record.objects.raw(
"""SELECT r.*
@ -200,8 +204,8 @@ ORDER BY
t.ord,
CASE WHEN is_credit THEN 1 ELSE 2 END,
r.ord""",
[self.period_parser.start,
self.period_parser.end,
[self.period.start,
self.period.end,
self.kwargs["subject_code"] + "%",
self.kwargs["subject_code"] + "%"])
self.pagination = Pagination(

View File

@ -22,212 +22,509 @@
import re
from datetime import date, timedelta
from django.core.serializers.json import DjangoJSONEncoder
from django.template import defaultfilters
from django.utils import dateformat
from django.utils.timezone import localdate
from django.utils.translation import gettext, pgettext
from django.utils.translation import gettext
class PeriodParser:
"""The period parser.
class Period:
"""The template helper for the period chooser.
Args:
spec (str): The period specification.
language (str): The current language.
data_start (date): The available first day of the data.
data_end (date): The available last day of the data.
spec (str): The current period specification
Attributes:
spec (str): The currently-using period specification.
start (date): The start of the period.
end (date): The end of the period.
description (str): The text description of the period.
error (str): The period specification format error, or
None on success.
spec (date): The currently-working period specification.
start (date): The start day of the currently-specified period.
end (date): The end day of the currently-specified period.
description (str): The description of the currently-specified
period.
this_month (str): The specification of this month.
last_month (str): The specification of last month.
since_last_month (str): The specification since last month.
has_months_to_choose (bool): Whether there are months to
choose besides this month and
last month.
chosen_month (bool): The specification of the chosen month,
or None if the current period is not a
month or is out of available data range.
this_year (str): The specification of this year.
last_year (str): The specification of last year.
has_years_to_choose (bool): Whether there are years to
choose besides this year and
last year.
years_to_choose (list[str]): This specification of the
available years to choose,
besides this year and last year.
today (str): The specification of today.
yesterday (str): The specification of yesterday.
chosen_start (str): The specification of the first day of the
specified period, as the default date for
the single-day chooser.
has_days_to_choose (bool): Whether there is a day range to
choose.
data_start (str): The specification of the available first day.
data_end (str): The specification of the available last day.
chosen_start (str): The specification of the first day of the
specified period
chosen_end (str): The specification of the last day of the
specified period
month_picker_params (str): The month-picker parameters, as a
JSON text string
"""
spec = None
start = None
end = None
description = None
error = None
_language = None
_data_start = None
_data_end = None
_period = None
def __init__(self, spec):
self.spec = spec
# A specific month
m = re.match("^([0-9]{4})-([0-9]{2})$", spec)
if m is not None:
year = int(m.group(1))
month = int(m.group(2))
try:
self.start = date(year, month, 1)
except ValueError:
self.invalid_period()
return
self.end = self.get_month_last_day(self.start)
self.description = self.get_month_text(year, month)
return
# From a specific month
m = re.match("^([0-9]{4})-([0-9]{2})-$", spec)
if m is not None:
year = int(m.group(1))
month = int(m.group(2))
try:
self.start = date(year, month, 1)
except ValueError:
self.invalid_period()
return
self.end = self.get_month_last_day(localdate())
self.description = gettext(
"Since %s") % self.get_month_text(year, month)
return
# A specific year
m = re.match("^([0-9]{4})$", spec)
if m is not None:
year = int(m.group(1))
try:
self.start = date(year, 1, 1)
except ValueError:
self.invalid_period()
return
self.end = date(year, 12, 31)
today = localdate()
if year == today.year:
self.description = gettext("This Year")
elif year == today.year - 1:
self.description = gettext("Last Year")
else:
self.description = str(year)
return
# All time
if spec == "-":
self.start = date(2000, 1, 1)
self.end = self.get_month_last_day(localdate())
self.description = gettext("All")
return
# A specific date
m = re.match("^([0-9]{4})-([0-9]{2})-([0-9]{2})$",
spec)
if m is not None:
try:
self.start = date(
int(m.group(1)), int(m.group(2)), int(m.group(3)))
except ValueError:
self.invalid_period()
return
self.end = self.start
self.description = self.get_date_text(self.start)
return
# A specific date period
m = re.match(("^([0-9]{4})-([0-9]{2})-([0-9]{2})"
"-([0-9]{4})-([0-9]{2})-([0-9]{2})$"),
spec)
if m is not None:
try:
self.start = date(
int(m.group(1)), int(m.group(2)), int(m.group(3)))
self.end = date(
int(m.group(4)), int(m.group(5)), int(m.group(6)))
except ValueError:
self.invalid_period()
return
today = localdate()
# Spans several years
if self.start.year != self.end.year:
self.description = "%s-%s" % (
defaultfilters.date(self.start, "Y/n/j"),
defaultfilters.date(self.end, "Y/n/j"))
# Spans several months
elif self.start.month != self.end.month:
if self.start.year != today.year:
self.description = "%s-%s" % (
defaultfilters.date(self.start, "Y/n/j"),
defaultfilters.date(self.end, "n/j"))
else:
self.description = "%s-%s" % (
defaultfilters.date(self.start, "n/j"),
defaultfilters.date(self.end, "n/j"))
# Spans several days
elif self.start.day != self.end.day:
if self.start.year != today.year:
self.description = "%s-%s" % (
defaultfilters.date(self.start, "Y/n/j"),
defaultfilters.date(self.end, "j"))
else:
self.description = "%s-%s" % (
defaultfilters.date(self.start, "n/j"),
defaultfilters.date(self.end, "j"))
# At the same day
else:
self.spec = dateformat.format(self.start, "Y-m-d")
self.description = self.get_date_text(self.start)
return
# Wrong period format
self.invalid_period()
def __init__(self, language, data_start, data_end, spec):
self._language = language
self._data_start = data_start
self._data_end = data_end
self._period = self.Parser(spec)
def invalid_period(self):
"""Sets the period when the period specification is invalid.
"""
self.error = gettext("Invalid period.")
today = localdate()
self.spec = dateformat.format(localdate(), "Y-m")
self.start = date(today.year, today.month, 1)
self.end = self.get_month_last_day(self.start)
self.description = gettext("This Month")
@property
def spec(self):
return self._period.spec
@property
def start(self):
return self._period.start
@property
def end(self):
return self._period.end
@property
def description(self):
return self._period.description
@staticmethod
def get_month_last_day(day):
"""Calculates and returns the last day of a month.
Args:
day (date): A day in the month.
def _get_last_month_start():
"""Returns the first day of the last month.
Returns:
date: The last day in the month
"""
next_month = day.month + 1
next_year = day.year
if next_month > 12:
next_month = 1
next_year = next_year + 1
return date(next_year, next_month, 1) - timedelta(days=1)
@staticmethod
def get_month_text(year, month):
"""Returns the text description of a month.
Args:
year (int): The year.
month (int): The month.
Returns:
str: The description of the month.
date: The first day of the last month.
"""
today = localdate()
if year == today.year and month == today.month:
return gettext("This Month")
prev_month = today.month - 1
prev_year = today.year
if prev_month < 1:
prev_month = 12
prev_year = prev_year - 1
prev = date(prev_year, prev_month, 1)
if year == prev.year and month == prev.month:
return gettext("Last Month")
return "%d/%d" % (year, month)
month = today.month - 1
year = today.year
if month < 1:
month = 12
year = year - 1
return date(year, month, 1)
@staticmethod
def get_date_text(day):
"""Returns the text description of a day.
Args:
day (date): The date.
def _get_next_month_start():
"""Returns the first day of the next month.
Returns:
str: The description of the day.
date: The first day of the next month.
"""
today = localdate()
if day == today:
return gettext("Today")
elif day == today - timedelta(days=1):
return gettext("Yesterday")
elif day.year != today.year:
return defaultfilters.date(day, "Y/n/j")
month = today.month + 1
year = today.year
if month > 12:
month = 1
year = year + 1
return date(year, month, 1)
@property
def this_month(self):
if self._data_start is None:
return None
today = localdate()
first_month_start = date(
self._data_start.year, self._data_start.month, 1)
if today < first_month_start:
return None
return dateformat.format(today, "Y-m")
@property
def last_month(self):
if self._data_start is None:
return None
last_month_start = self._get_last_month_start()
first_month_start = date(
self._data_start.year, self._data_start.month, 1)
if last_month_start < first_month_start:
return None
return dateformat.format(last_month_start, "Y-m")
@property
def since_last_month(self):
last_month = self.last_month
if last_month is None:
return None
return self.last_month + "-"
@property
def has_months_to_choose(self):
if self._data_start is None:
return None
if self._data_start < self._get_last_month_start():
return True
if self._data_end >= self._get_next_month_start():
return True
return False
@property
def chosen_month(self):
if self._data_start is None:
return None
m = re.match("^[0-9]{4}-[0-2]{2}", self._period.spec)
if m is None:
return None
if self._period.end < self._data_start:
return None
if self._period.start > self._data_end:
return None
return self._period.spec
@property
def this_year(self):
if self._data_start is None:
return None
this_year = localdate().year
if this_year < self._data_start.year:
return None
return str(this_year)
@property
def last_year(self):
if self._data_start is None:
return None
last_year = localdate().year - 1
if last_year < self._data_start.year:
return None
return str(last_year)
@property
def has_years_to_choose(self):
if self._data_start is None:
return None
this_year = localdate().year
if self._data_start.year < this_year - 1:
return True
if self._data_end.year > this_year:
return True
return False
@property
def years_to_choose(self):
if self._data_start is None:
return None
this_year = localdate().year
before = [str(x) for x in range(
self._data_start.year, this_year - 1)]
after = [str(x) for x in range(
self._data_end.year, this_year, -1)]
return after + before[::-1]
def is_chosen_year(self, year):
"""Returne whether the specified year is the currently-chosen
year.
Args:
year (str): the year.
Returns:
bool: True if the year is the currently-chosen year, or
False otherwise
"""
if self._period.spec == str(year):
return True
@property
def today(self):
if self._data_start is None:
return None
today = localdate()
if today < self._data_start or today > self._data_end:
return None
return dateformat.format(today, "Y-m-d")
@property
def yesterday(self):
if self._data_start is None:
return None
yesterday = localdate() - timedelta(days=1)
if yesterday < self._data_start or yesterday > self._data_end:
return None
return dateformat.format(yesterday, "Y-m-d")
@property
def chosen_day(self):
return dateformat.format(self._period.start, "Y-m-d")
@property
def has_days_to_choose(self):
if self._data_start is None:
return False
if self._data_start == self._data_end:
return False
return True
@property
def first_day(self):
if self._data_start is None:
return None
return dateformat.format(self._data_start, "Y-m-d")
@property
def last_day(self):
if self._data_end is None:
return None
return dateformat.format(self._data_end, "Y-m-d")
@property
def chosen_start(self):
if self._data_start is None:
return None
day = self._period.start \
if self._period.start >= self._data_start \
else self._data_start
return dateformat.format(day, "Y-m-d")
@property
def chosen_end(self):
if self._data_end is None:
return None
day = self._period.end \
if self._period.end <= self._data_end \
else self._data_end
return dateformat.format(day, "Y-m-d")
@property
def month_picker_params(self):
if self._data_start is None:
return None
if self._language == "zh-Hant":
locale = "zh_TW"
elif self._language == "zh-Hans":
locale = "zh_CN"
else:
return defaultfilters.date(day, "n/j")
locale = self._language.replace("-", "_")
start = date(self._data_start.year, self._data_start.month, 1)
return DjangoJSONEncoder().encode({
"locale": locale,
"minDate": start,
"maxDate": self._data_end,
"defaultDate": self.chosen_month,
})
class Parser:
"""The period parser.
Args:
spec (str): The period specification.
Attributes:
spec (str): The currently-using period specification.
start (date): The start of the period.
end (date): The end of the period.
description (str): The text description of the period.
error (str): The period specification format error, or
None on success.
"""
spec = None
start = None
end = None
description = None
error = None
def __init__(self, spec):
self.spec = spec
# A specific month
m = re.match("^([0-9]{4})-([0-9]{2})$", spec)
if m is not None:
year = int(m.group(1))
month = int(m.group(2))
try:
self.start = date(year, month, 1)
except ValueError:
self.invalid_period()
return
self.end = self.get_month_last_day(self.start)
self.description = self.get_month_text(year, month)
return
# From a specific month
m = re.match("^([0-9]{4})-([0-9]{2})-$", spec)
if m is not None:
year = int(m.group(1))
month = int(m.group(2))
try:
self.start = date(year, month, 1)
except ValueError:
self.invalid_period()
return
self.end = self.get_month_last_day(localdate())
self.description = gettext(
"Since %s") % self.get_month_text(year, month)
return
# A specific year
m = re.match("^([0-9]{4})$", spec)
if m is not None:
year = int(m.group(1))
try:
self.start = date(year, 1, 1)
except ValueError:
self.invalid_period()
return
self.end = date(year, 12, 31)
today = localdate()
if year == today.year:
self.description = gettext("This Year")
elif year == today.year - 1:
self.description = gettext("Last Year")
else:
self.description = str(year)
return
# All time
if spec == "-":
self.start = date(2000, 1, 1)
self.end = self.get_month_last_day(localdate())
self.description = gettext("All")
return
# A specific date
m = re.match("^([0-9]{4})-([0-9]{2})-([0-9]{2})$",
spec)
if m is not None:
try:
self.start = date(
int(m.group(1)),
int(m.group(2)),
int(m.group(3)))
except ValueError:
self.invalid_period()
return
self.end = self.start
self.description = self.get_date_text(self.start)
return
# A specific date period
m = re.match(("^([0-9]{4})-([0-9]{2})-([0-9]{2})"
"-([0-9]{4})-([0-9]{2})-([0-9]{2})$"),
spec)
if m is not None:
try:
self.start = date(
int(m.group(1)),
int(m.group(2)),
int(m.group(3)))
self.end = date(
int(m.group(4)),
int(m.group(5)),
int(m.group(6)))
except ValueError:
self.invalid_period()
return
today = localdate()
# Spans several years
if self.start.year != self.end.year:
self.description = "%s-%s" % (
defaultfilters.date(self.start, "Y/n/j"),
defaultfilters.date(self.end, "Y/n/j"))
# Spans several months
elif self.start.month != self.end.month:
if self.start.year != today.year:
self.description = "%s-%s" % (
defaultfilters.date(self.start, "Y/n/j"),
defaultfilters.date(self.end, "n/j"))
else:
self.description = "%s-%s" % (
defaultfilters.date(self.start, "n/j"),
defaultfilters.date(self.end, "n/j"))
# Spans several days
elif self.start.day != self.end.day:
if self.start.year != today.year:
self.description = "%s-%s" % (
defaultfilters.date(self.start, "Y/n/j"),
defaultfilters.date(self.end, "j"))
else:
self.description = "%s-%s" % (
defaultfilters.date(self.start, "n/j"),
defaultfilters.date(self.end, "j"))
# At the same day
else:
self.spec = dateformat.format(self.start, "Y-m-d")
self.description = self.get_date_text(self.start)
return
# Wrong period format
self.invalid_period()
def invalid_period(self):
"""Sets the period when the period specification is
invalid.
"""
self.error = gettext("Invalid period.")
today = localdate()
self.spec = dateformat.format(localdate(), "Y-m")
self.start = date(today.year, today.month, 1)
self.end = self.get_month_last_day(self.start)
self.description = gettext("This Month")
@staticmethod
def get_month_last_day(day):
"""Calculates and returns the last day of a month.
Args:
day (date): A day in the month.
Returns:
date: The last day in the month
"""
next_month = day.month + 1
next_year = day.year
if next_month > 12:
next_month = 1
next_year = next_year + 1
return date(next_year, next_month, 1) - timedelta(days=1)
@staticmethod
def get_month_text(year, month):
"""Returns the text description of a month.
Args:
year (int): The year.
month (int): The month.
Returns:
str: The description of the month.
"""
today = localdate()
if year == today.year and month == today.month:
return gettext("This Month")
prev_month = today.month - 1
prev_year = today.year
if prev_month < 1:
prev_month = 12
prev_year = prev_year - 1
prev = date(prev_year, prev_month, 1)
if year == prev.year and month == prev.month:
return gettext("Last Month")
return "%d/%d" % (year, month)
@staticmethod
def get_date_text(day):
"""Returns the text description of a day.
Args:
day (date): The date.
Returns:
str: The description of the day.
"""
today = localdate()
if day == today:
return gettext("Today")
elif day == today - timedelta(days=1):
return gettext("Yesterday")
elif day.year != today.year:
return defaultfilters.date(day, "Y/n/j")
else:
return defaultfilters.date(day, "n/j")