Added type hints to the Mia core application.

This commit is contained in:
依瑪貓 2020-08-13 08:05:35 +08:00
parent c070b11ea2
commit 1be05c2252
9 changed files with 292 additions and 250 deletions

View File

@ -25,11 +25,30 @@ class UserConverter:
"""The path converter for the user accounts.""" """The path converter for the user accounts."""
regex = ".*" regex = ".*"
def to_python(self, value): def to_python(self, value: str) -> User:
"""Returns the user by her log in ID.
Args:
value: The log in ID.
Returns:
The user.
Raises:
ValueError: When the value is invalid
"""
try: try:
return User.objects.get(login_id=value) return User.objects.get(login_id=value)
except User.DoesNotExist: except User.DoesNotExist:
raise ValueError raise ValueError
def to_url(self, value): def to_url(self, value: User) -> str:
"""Returns the log in ID of a user.
Args:
value: The user.
Returns:
The log in ID.
"""
return value.login_id return value.login_id

View File

@ -22,6 +22,7 @@ application.
import ipaddress import ipaddress
import socket import socket
from functools import wraps from functools import wraps
from typing import Optional
from django.conf import settings from django.conf import settings
from django.db.models import F from django.db.models import F
@ -35,23 +36,23 @@ from .models import User, Country
class AccountBackend: class AccountBackend:
"""The account backend for the django-digest module.""" """The account backend for the django-digest module."""
def get_partial_digest(self, username): def get_partial_digest(self, username: str) -> Optional[str]:
"""Returns the HTTP digest authentication password digest hash """Returns the HTTP digest authentication password digest hash
of a user. of a user.
Args: Args:
username (str): The log in user name. username: The log in user name.
Return: Return:
str: The HTTP digest authentication password hash of The HTTP digest authentication password hash of the user, or None
the user, or None if the user does not exist. if the user does not exist.
""" """
user = User.objects.filter(login_id=username).first() user = User.objects.filter(login_id=username).first()
if user is None: if user is None:
return None return None
return user.password return user.password
def get_user(self, username): def get_user(self, username: str) -> Optional[User]:
"""Returns the user by her log in user name. """Returns the user by her log in user name.
Args: Args:
@ -86,7 +87,7 @@ def login_required(function=None):
return decorator return decorator
def _log_visit(request): def _log_visit(request: HttpRequest) -> None:
"""Logs the visit information for the logged-in user. """Logs the visit information for the logged-in user.
Args: Args:
@ -106,21 +107,29 @@ def _log_visit(request):
request.session["visit_logged"] = True request.session["visit_logged"] = True
def _get_remote_ip(request): def _get_remote_ip(request: HttpRequest) -> str:
"""Returns the IP of the remote client.
Args:
request: The request.
Returns:
The IP of the remote client.
"""
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR") x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for: if x_forwarded_for:
return x_forwarded_for.split(",")[0] return x_forwarded_for.split(",")[0]
return request.META.get('REMOTE_ADDR') return request.META.get('REMOTE_ADDR')
def _get_host(ip): def _get_host(ip: str) -> Optional[str]:
"""Look-up the host name by its IP. """Look-up the host name by its IP.
Args: Args:
ip (str): The IP ip: The IP
Returns: Returns:
str: The host name, or None if the look-up fails. The host name, or None if the look-up fails.
""" """
try: try:
return socket.gethostbyaddr(ip)[0] return socket.gethostbyaddr(ip)[0]
@ -128,31 +137,30 @@ def _get_host(ip):
return None return None
def _get_country(ip): def _get_country(ip: str) -> Optional[Country]:
"""Look-up the country by its IP. """Look-up the country by its IP.
Args: Args:
ip (str): The IP ip: The IP
Returns: Returns:
Country: The country. The country.
""" """
code = _get_country_code(ip) code = _get_country_code(ip)
try: try:
return Country.objects.get(code=code) return Country.objects.get(code=code)
except Country.DoesNotExist: except Country.DoesNotExist:
pass
return None return None
def _get_country_code(ip): def _get_country_code(ip: str) -> Optional[str]:
"""Look-up the country code by its IP. """Look-up the country code by its IP.
Args: Args:
ip (str): The IP ip: The IP
Returns: Returns:
str: The country code, or None if the look-up fails. The country code, or None if the look-up fails.
""" """
try: try:
return geolite2.lookup(ip).country return geolite2.lookup(ip).country

View File

@ -75,7 +75,7 @@ class UserForm(forms.Form):
if errors: if errors:
raise forms.ValidationError(errors) raise forms.ValidationError(errors)
def _validate_login_id_unique(self): def _validate_login_id_unique(self) -> None:
"""Validates whether the log in ID is unique. """Validates whether the log in ID is unique.
Raises: Raises:
@ -93,7 +93,7 @@ class UserForm(forms.Form):
self.add_error("login_id", error) self.add_error("login_id", error)
raise error raise error
def _validate_password_new_required(self): def _validate_password_new_required(self) -> None:
"""Validates whether the password is entered for newly-created users. """Validates whether the password is entered for newly-created users.
Raises: Raises:
@ -108,7 +108,7 @@ class UserForm(forms.Form):
self.add_error("password", error) self.add_error("password", error)
raise error raise error
def _validate_password_login_id_changed_required(self): def _validate_password_login_id_changed_required(self) -> None:
"""Validates whether the password is entered for users whose login ID """Validates whether the password is entered for users whose login ID
changed. changed.
@ -129,7 +129,7 @@ class UserForm(forms.Form):
self.add_error("password", error) self.add_error("password", error)
raise error raise error
def _validate_password2_required(self): def _validate_password2_required(self) -> None:
"""Validates whether the second password is entered. """Validates whether the second password is entered.
Raises: Raises:
@ -145,7 +145,7 @@ class UserForm(forms.Form):
self.add_error("password2", error) self.add_error("password2", error)
raise error raise error
def _validate_passwords_equal(self): def _validate_passwords_equal(self) -> None:
"""Validates whether the two passwords are equa. """Validates whether the two passwords are equa.
Raises: Raises:
@ -162,7 +162,7 @@ class UserForm(forms.Form):
self.add_error("password2", error) self.add_error("password2", error)
raise error raise error
def _validate_is_disabled_not_oneself(self): def _validate_is_disabled_not_oneself(self) -> None:
"""Validates whether the user tries to disable herself """Validates whether the user tries to disable herself
Raises: Raises:

View File

@ -18,7 +18,6 @@
"""The data models of the Mia core application. """The data models of the Mia core application.
""" """
import datetime
import hashlib import hashlib
from dirtyfields import DirtyFieldsMixin from dirtyfields import DirtyFieldsMixin
@ -57,11 +56,12 @@ class Country(DirtyFieldsMixin, models.Model):
return self.code.__str__() + " " + self.name.__str__() return self.code.__str__() + " " + self.name.__str__()
@property @property
def name(self): def name(self) -> str:
"""The country name in the current language."""
return get_multi_lingual_attr(self, "name", "en") return get_multi_lingual_attr(self, "name", "en")
@name.setter @name.setter
def name(self, value): def name(self, value: str) -> None:
set_multi_lingual_attr(self, "name", value) set_multi_lingual_attr(self, "name", value)
class Meta: class Meta:
@ -101,11 +101,11 @@ class User(DirtyFieldsMixin, models.Model):
USERNAME_FIELD = "login_id" USERNAME_FIELD = "login_id"
@property @property
def is_anonymous(self): def is_anonymous(self) -> bool:
return False return False
@property @property
def is_authenticated(self): def is_authenticated(self) -> bool:
return True return True
def set_password(self): def set_password(self):
@ -142,16 +142,16 @@ class User(DirtyFieldsMixin, models.Model):
F"{login_id}:{settings.DIGEST_REALM}:{password}") F"{login_id}:{settings.DIGEST_REALM}:{password}")
@staticmethod @staticmethod
def md5(value): def md5(value: str) -> str:
m = hashlib.md5() m = hashlib.md5()
m.update(value.encode("utf-8")) m.update(value.encode("utf-8"))
return m.hexdigest() return m.hexdigest()
def is_in_use(self): def is_in_use(self) -> bool:
"""Returns whether this user is in use. """Returns whether this user is in use.
Returns: Returns:
bool: True if this user is in use, or False otherwise. True if this user is in use, or False otherwise.
""" """
for table in connection.introspection.table_names(): for table in connection.introspection.table_names():
if self._is_in_use_with(F"SELECT * FROM {table}" if self._is_in_use_with(F"SELECT * FROM {table}"
@ -163,14 +163,14 @@ class User(DirtyFieldsMixin, models.Model):
return True return True
return False return False
def _is_in_use_with(self, sql): def _is_in_use_with(self, sql: str) -> bool:
"""Returns whether this user is in use with a specific SQL statement. """Returns whether this user is in use with a specific SQL statement.
Args: Args:
sql (str): The SQL query statement sql: The SQL query statement
Returns: Returns:
bool: True if this user is in use, or False otherwise. True if this user is in use, or False otherwise.
""" """
with connection.cursor() as cursor: with connection.cursor() as cursor:
try: try:

View File

@ -20,6 +20,8 @@
""" """
import datetime import datetime
import re import re
from datetime import date
from typing import Optional, List, Any, Union
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.template import defaultfilters from django.template import defaultfilters
@ -33,72 +35,73 @@ class Period:
"""The template helper for the period chooser. """The template helper for the period chooser.
Args: Args:
spec (str): The current period specification spec: The current period specification
data_start (datetime.date): The available first day of the data. data_start: The available first day of the data.
data_end (datetime.date): The available last day of the data. data_end: The available last day of the data.
Raises: Raises:
ValueError: When the period specification is invalid. ValueError: When the period specification is invalid.
""" """
def __init__(self, spec=None, data_start=None, data_end=None): def __init__(self, spec: str = None, data_start: datetime.date = None,
data_end: datetime.date = None):
# Raises ValueError # Raises ValueError
self._period = self.Parser(spec) self._period = self.Parser(spec)
self._data_start = data_start self._data_start = data_start
self._data_end = data_end self._data_end = data_end
@property @property
def spec(self): def spec(self) -> str:
"""Returns the period specification. """Returns the period specification.
Returns: Returns:
str: The period specification. The period specification.
""" """
return self._period.spec return self._period.spec
@property @property
def start(self): def start(self) -> datetime.date:
"""Returns the start day of the currently-specified period. """Returns the start day of the currently-specified period.
Returns: Returns:
datetime.date: The start day of the currently-specified period. The start day of the currently-specified period.
""" """
return self._period.start return self._period.start
@property @property
def end(self): def end(self) -> datetime.date:
"""Returns the end day of the currently-specified period. """Returns the end day of the currently-specified period.
Returns: Returns:
datetime.date: The end day of the currently-specified period. The end day of the currently-specified period.
""" """
return self._period.end return self._period.end
@property @property
def description(self): def description(self) -> str:
"""Returns the text description of the currently-specified period. """Returns the text description of the currently-specified period.
Returns: Returns:
str: The text description of the currently-specified period The text description of the currently-specified period
""" """
return self._period.description return self._period.description
@property @property
def prep_desc(self): def prep_desc(self) -> str:
"""Returns the text description with preposition of the """Returns the text description with preposition of the
currently-specified period. currently-specified period.
Returns: Returns:
str: The text description with preposition of the The text description with preposition of the currently-specified
currently-specified period period.
""" """
return self._period.prep_desc return self._period.prep_desc
@staticmethod @staticmethod
def _get_last_month_start(): def _get_last_month_start() -> datetime.date:
"""Returns the first day of the last month. """Returns the first day of the last month.
Returns: Returns:
datetime.date: The first day of the last month. The first day of the last month.
""" """
today = timezone.localdate() today = timezone.localdate()
month = today.month - 1 month = today.month - 1
@ -109,11 +112,11 @@ class Period:
return datetime.date(year, month, 1) return datetime.date(year, month, 1)
@staticmethod @staticmethod
def _get_next_month_start(): def _get_next_month_start() -> datetime.date:
"""Returns the first day of the next month. """Returns the first day of the next month.
Returns: Returns:
datetime.date: The first day of the next month. The first day of the next month.
""" """
today = timezone.localdate() today = timezone.localdate()
month = today.month + 1 month = today.month + 1
@ -123,12 +126,12 @@ class Period:
year = year + 1 year = year + 1
return datetime.date(year, month, 1) return datetime.date(year, month, 1)
def this_month(self): def this_month(self) -> Optional[str]:
"""Returns the specification of this month. """Returns the specification of this month.
Returns: Returns:
str|None: The specification of this month, or None if there is no The specification of this month, or None if there is no data in or
data in or before this month. before this month.
""" """
if self._data_start is None: if self._data_start is None:
return None return None
@ -139,12 +142,12 @@ class Period:
return None return None
return dateformat.format(today, "Y-m") return dateformat.format(today, "Y-m")
def last_month(self): def last_month(self) -> Optional[str]:
"""Returns the specification of last month. """Returns the specification of last month.
Returns: Returns:
str|None: The specification of last month, or None if there is no The specification of last month, or None if there is no data in or
data in or before last month. before last month.
""" """
if self._data_start is None: if self._data_start is None:
return None return None
@ -155,25 +158,25 @@ class Period:
return None return None
return dateformat.format(last_month_start, "Y-m") return dateformat.format(last_month_start, "Y-m")
def since_last_month(self): def since_last_month(self) -> Optional[str]:
"""Returns the specification since last month. """Returns the specification since last month.
Returns: Returns:
str|None: The specification since last month, or None if there is The specification since last month, or None if there is no data in
no data in or before last month. or before last month.
""" """
last_month = self.last_month() last_month = self.last_month()
if last_month is None: if last_month is None:
return None return None
return last_month + "-" return last_month + "-"
def has_months_to_choose(self): def has_months_to_choose(self) -> bool:
"""Returns whether there are months to choose besides this month and """Returns whether there are months to choose besides this month and
last month. last month.
Returns: Returns:
bool: True if there are months to choose besides this month and True if there are months to choose besides this month and last
last month, or False otherwise. month, or False otherwise.
""" """
if self._data_start is None: if self._data_start is None:
return False return False
@ -183,14 +186,13 @@ class Period:
return True return True
return False return False
def chosen_month(self): def chosen_month(self) -> Optional[str]:
"""Returns the specification of the chosen month, or None if the """Returns the specification of the chosen month, or None if the
current period is not a month or is out of available data range. current period is not a month or is out of available data range.
Returns: Returns:
str|None: The specification of the chosen month, or None if the The specification of the chosen month, or None if the current
current period is not a month or is out of available data period is not a month or is out of available data range.
range.
""" """
if self._data_start is None: if self._data_start is None:
return None return None
@ -203,12 +205,12 @@ class Period:
return None return None
return self._period.spec return self._period.spec
def this_year(self): def this_year(self) -> Optional[str]:
"""Returns the specification of this year. """Returns the specification of this year.
Returns: Returns:
str|None: The specification of this year, or None if there is no The specification of this year, or None if there is no data in or
data in or before this year. before this year.
""" """
if self._data_start is None: if self._data_start is None:
return None return None
@ -217,12 +219,12 @@ class Period:
return None return None
return str(this_year) return str(this_year)
def last_year(self): def last_year(self) -> Optional[str]:
"""Returns the specification of last year. """Returns the specification of last year.
Returns: Returns:
str|None: The specification of last year, or None if there is no The specification of last year, or None if there is no data in or
data in or before last year. before last year.
""" """
if self._data_start is None: if self._data_start is None:
return None return None
@ -231,13 +233,13 @@ class Period:
return None return None
return str(last_year) return str(last_year)
def has_years_to_choose(self): def has_years_to_choose(self) -> bool:
"""Returns whether there are years to choose besides this year and """Returns whether there are years to choose besides this year and
last year. last year.
Returns: Returns:
bool: True if there are years to choose besides this year and True if there are years to choose besides this year and last year,
last year, or False otherwise. or False otherwise.
""" """
if self._data_start is None: if self._data_start is None:
return False return False
@ -248,11 +250,12 @@ class Period:
return True return True
return False return False
def years_to_choose(self): def years_to_choose(self) -> Optional[List[str]]:
"""Returns the years to choose besides this year and last year. """Returns the years to choose besides this year and last year.
Returns: Returns:
list[str]: The years to choose besides this year and last year. The years to choose besides this year and last year, or None if
there is no data.
""" """
if self._data_start is None: if self._data_start is None:
return None return None
@ -263,12 +266,12 @@ class Period:
self._data_end.year, this_year, -1)] self._data_end.year, this_year, -1)]
return after + before[::-1] return after + before[::-1]
def today(self): def today(self) -> Optional[None]:
"""Returns the specification of today. """Returns the specification of today.
Returns: Returns:
(str): The specification of today, or None if there is no data The specification of today, or None if there is no data in or
in or before today. before today.
""" """
if self._data_start is None: if self._data_start is None:
return None return None
@ -277,12 +280,12 @@ class Period:
return None return None
return dateformat.format(today, "Y-m-d") return dateformat.format(today, "Y-m-d")
def yesterday(self): def yesterday(self) -> Optional[str]:
"""Returns the specification of yesterday. """Returns the specification of yesterday.
Returns: Returns:
(str): The specification of yesterday, or None if there is no data The specification of yesterday, or None if there is no data in or
in or before yesterday. before yesterday.
""" """
if self._data_start is None: if self._data_start is None:
return None return None
@ -291,21 +294,21 @@ class Period:
return None return None
return dateformat.format(yesterday, "Y-m-d") return dateformat.format(yesterday, "Y-m-d")
def chosen_day(self): def chosen_day(self) -> str:
"""Returns the specification of the chosen day. """Returns the specification of the chosen day.
Returns: Returns:
(str): The specification of the chosen day, or the start day The specification of the chosen day, or the start day of the period
of the period if the current period is not a day. if the current period is not a day.
""" """
return dateformat.format(self._period.start, "Y-m-d") return dateformat.format(self._period.start, "Y-m-d")
def has_days_to_choose(self): def has_days_to_choose(self) -> bool:
"""Returns whether there are more than one day to choose from. """Returns whether there are more than one day to choose from.
Returns: Returns:
bool: True if there are more than one day to choose from, True if there are more than one day to choose from, or False
or False otherwise. otherwise.
""" """
if self._data_start is None: if self._data_start is None:
return False return False
@ -313,33 +316,35 @@ class Period:
return False return False
return True return True
def first_day(self): def first_day(self) -> Optional[str]:
"""Returns the specification of the available first day. """Returns the specification of the available first day.
Returns: Returns:
str: The specification of the available first day. The specification of the available first day, or None if there is
no data.
""" """
if self._data_start is None: if self._data_start is None:
return None return None
return dateformat.format(self._data_start, "Y-m-d") return dateformat.format(self._data_start, "Y-m-d")
def last_day(self): def last_day(self) -> Optional[str]:
"""Returns the specification of the available last day. """Returns the specification of the available last day.
Returns: Returns:
str: The specification of the available last day. The specification of the available last day, or None if there is no
data.
""" """
if self._data_end is None: if self._data_end is None:
return None return None
return dateformat.format(self._data_end, "Y-m-d") return dateformat.format(self._data_end, "Y-m-d")
def chosen_start(self): def chosen_start(self) -> Optional[str]:
"""Returns the specification of of the first day of the """Returns the specification of of the first day of the
specified period. specified period.
Returns: Returns:
str: The specification of of the first day of the The specification of of the first day of the specified period, or
specified period. None if there is no data.
""" """
if self._data_start is None: if self._data_start is None:
return None return None
@ -348,13 +353,13 @@ class Period:
else self._data_start else self._data_start
return dateformat.format(day, "Y-m-d") return dateformat.format(day, "Y-m-d")
def chosen_end(self): def chosen_end(self) -> Optional[str]:
"""Returns the specification of of the last day of the """Returns the specification of of the last day of the
specified period. specified period.
Returns: Returns:
str: The specification of of the last day of the The specification of of the last day of the specified period, or
specified period. None if there is data.
""" """
if self._data_end is None: if self._data_end is None:
return None return None
@ -363,12 +368,12 @@ class Period:
else self._data_end else self._data_end
return dateformat.format(day, "Y-m-d") return dateformat.format(day, "Y-m-d")
def period_before(self): def period_before(self) -> Optional[str]:
"""Returns the specification of the period before the current period. """Returns the specification of the period before the current period.
Returns: Returns:
str|None: The specification of the period before the current The specification of the period before the current period, or None
period, or None if there is no data before the current period. if there is no data before the current period.
""" """
if self._data_start is None: if self._data_start is None:
return None return None
@ -381,11 +386,12 @@ class Period:
return dateformat.format(previous_day, "-Y-m") return dateformat.format(previous_day, "-Y-m")
return dateformat.format(previous_day, "-Y-m-d") return dateformat.format(previous_day, "-Y-m-d")
def month_picker_params(self): def month_picker_params(self) -> Optional[str]:
"""Returns the parameters for the month-picker, as a JSON text string. """Returns the parameters for the month-picker, as a JSON text string.
Returns: Returns:
str: The parameters for the month-picker, as a JSON text string. The parameters for the month-picker, as a JSON text string, or None
if there is no data.
""" """
if self._data_start is None: if self._data_start is None:
return None return None
@ -398,7 +404,7 @@ class Period:
}) })
@staticmethod @staticmethod
def default_spec(): def default_spec() -> str:
"""Returns the specification for the default period. """Returns the specification for the default period.
Returns: Returns:
@ -422,9 +428,9 @@ class Period:
description (str): The text description of the period. description (str): The text description of the period.
prep_desc (str): The text description with preposition. prep_desc (str): The text description with preposition.
""" """
VERY_START = datetime.date(1990, 1, 1) VERY_START: datetime.date = datetime.date(1990, 1, 1)
def __init__(self, spec): def __init__(self, spec: str):
self.spec = None self.spec = None
self.start = None self.start = None
self.end = None self.end = None
@ -574,7 +580,7 @@ class Period:
# Wrong period format # Wrong period format
raise ValueError raise ValueError
def _set_this_month(self): def _set_this_month(self) -> None:
"""Sets the period to this month.""" """Sets the period to this month."""
today = timezone.localdate() today = timezone.localdate()
self.spec = dateformat.format(today, "Y-m") self.spec = dateformat.format(today, "Y-m")
@ -583,14 +589,14 @@ class Period:
self.description = gettext("This Month") self.description = gettext("This Month")
@staticmethod @staticmethod
def _month_last_day(day): def _month_last_day(day: datetime.date) -> datetime.date:
"""Calculates and returns the last day of a month. """Calculates and returns the last day of a month.
Args: Args:
day (datetime.date): A day in the month. day: A day in the month.
Returns: Returns:
date: The last day in the month The last day in the month
""" """
next_month = day.month + 1 next_month = day.month + 1
next_year = day.year next_year = day.year
@ -601,15 +607,15 @@ class Period:
next_year, next_month, 1) - datetime.timedelta(days=1) next_year, next_month, 1) - datetime.timedelta(days=1)
@staticmethod @staticmethod
def _month_text(year, month): def _month_text(year: int, month: int) -> str:
"""Returns the text description of a month. """Returns the text description of a month.
Args: Args:
year (int): The year. year: The year.
month (int): The month. month: The month.
Returns: Returns:
str: The description of the month. The description of the month.
""" """
today = timezone.localdate() today = timezone.localdate()
if year == today.year and month == today.month: if year == today.year and month == today.month:
@ -625,14 +631,14 @@ class Period:
return "%d/%d" % (year, month) return "%d/%d" % (year, month)
@staticmethod @staticmethod
def _year_text(year): def _year_text(year: int) -> str:
"""Returns the text description of a year. """Returns the text description of a year.
Args: Args:
year (int): The year. year: The year.
Returns: Returns:
str: The description of the year. The description of the year.
""" """
this_year = timezone.localdate().year this_year = timezone.localdate().year
if year == this_year: if year == this_year:
@ -642,14 +648,14 @@ class Period:
return str(year) return str(year)
@staticmethod @staticmethod
def _date_text(day): def _date_text(day: datetime.date) -> str:
"""Returns the text description of a day. """Returns the text description of a day.
Args: Args:
day (datetime.date): The date. day: The date.
Returns: Returns:
str: The description of the day. The description of the day.
""" """
today = timezone.localdate() today = timezone.localdate()
if day == today: if day == today:

View File

@ -19,16 +19,18 @@
""" """
import random import random
from typing import Dict, Mapping, Any, Optional
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect, HttpRequest
from django.shortcuts import redirect from django.shortcuts import redirect
from .utils import UrlBuilder from .utils import UrlBuilder
STORAGE_KEY = "stored_post" STORAGE_KEY: str = "stored_post"
def error_redirect(request, url, post): def error_redirect(request: HttpRequest, url: str,
post: Dict[str, str]) -> HttpResponseRedirect:
"""Redirects to a specific URL on error, with the POST data ID appended """Redirects to a specific URL on error, with the POST data ID appended
as the query parameter "s". The POST data can be loaded with the as the query parameter "s". The POST data can be loaded with the
get_previous_post() utility. get_previous_post() utility.
@ -45,7 +47,7 @@ def error_redirect(request, url, post):
return redirect(str(UrlBuilder(url).query(s=post_id))) return redirect(str(UrlBuilder(url).query(s=post_id)))
def get_previous_post(request): def get_previous_post(request: HttpRequest) -> Optional[Dict[str, str]]:
"""Retrieves the previously-stored POST data. """Retrieves the previously-stored POST data.
Args: Args:
@ -59,16 +61,16 @@ def get_previous_post(request):
return _retrieve(request, request.GET["s"]) return _retrieve(request, request.GET["s"])
def _store(request, post): def _store(request: HttpRequest, post: Dict[str, str]) -> str:
"""Stores the POST data into the session, and returns the POST data ID that """Stores the POST data into the session, and returns the POST data ID that
can be used to retrieve it later with _retrieve(). can be used to retrieve it later with _retrieve().
Args: Args:
request (HttpRequest): The request. request: The request.
post (dict): The POST data. post: The POST data.
Returns: Returns:
str: The POST data ID The POST data ID
""" """
if STORAGE_KEY not in request.session: if STORAGE_KEY not in request.session:
request.session[STORAGE_KEY] = {} request.session[STORAGE_KEY] = {}
@ -77,15 +79,15 @@ def _store(request, post):
return post_id return post_id
def _retrieve(request, post_id): def _retrieve(request: HttpRequest, post_id: str) -> Optional[Dict[str, str]]:
"""Retrieves the POST data from the storage. """Retrieves the POST data from the storage.
Args: Args:
request (HttpRequest): The request. request: The request.
post_id (str): The POST data ID. post_id: The POST data ID.
Returns: Returns:
dict: The POST data, or None if the corresponding data does not exist. The POST data, or None if the corresponding data does not exist.
""" """
if STORAGE_KEY not in request.session: if STORAGE_KEY not in request.session:
return None return None
@ -94,7 +96,7 @@ def _retrieve(request, post_id):
return request.session[STORAGE_KEY][post_id] return request.session[STORAGE_KEY][post_id]
def _new_post_id(post_store): def _new_post_id(post_store: Mapping[int, Any]) -> str:
"""Generates and returns a new POST ID that does not exist yet. """Generates and returns a new POST ID that does not exist yet.
Args: Args:

View File

@ -18,12 +18,14 @@
"""The template tags and filters of the Mia core application. """The template tags and filters of the Mia core application.
""" """
import datetime
from datetime import date from datetime import date
from typing import Any
import titlecase import titlecase
from django import template from django import template
from django.http import HttpRequest from django.http import HttpRequest
from django.template import defaultfilters from django.template import defaultfilters, RequestContext
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.safestring import SafeString from django.utils.safestring import SafeString
@ -35,31 +37,31 @@ register = template.Library()
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def setvar(context, key, value): def setvar(context: RequestContext, key: str, value: Any) -> str:
"""Sets a variable in the template. """Sets a variable in the template.
Args: Args:
context (Context): the context context: the context
key (str): The variable name key: The variable name
value (str): The variable value value: The variable value
Returns: Returns:
str: An empty string. An empty string.
""" """
context.dicts[0][key] = value context.dicts[0][key] = value
return "" return ""
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def url_period(context, period_spec): def url_period(context: RequestContext, period_spec: str) -> str:
"""Returns the current URL with a new period. """Returns the current URL with a new period.
Args: Args:
context (RequestContext): The request context. context: The request context.
period_spec (str): The period specification. period_spec: The period specification.
Returns: Returns:
str: The current URL with the new period. The current URL with the new period.
""" """
view_name = "%s:%s" % ( view_name = "%s:%s" % (
context.request.resolver_match.app_name, context.request.resolver_match.app_name,
@ -70,47 +72,47 @@ def url_period(context, period_spec):
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def url_with_return(context, url): def url_with_return(context: RequestContext, url: str) -> str:
"""Returns the URL with the current page added as the "r" query parameter, """Returns the URL with the current page added as the "r" query parameter,
so that returning to this page is possible. so that returning to this page is possible.
Args: Args:
context (RequestContext): The request context. context: The request context.
url (str): The URL. url: The URL.
Returns: Returns:
str: The URL with the current page added as the "r" query parameter. The URL with the current page added as the "r" query parameter.
""" """
return str(UrlBuilder(url).query( return str(UrlBuilder(url).query(
r=str(UrlBuilder(context.request.get_full_path()).remove("s")))) r=str(UrlBuilder(context.request.get_full_path()).remove("s"))))
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def url_keep_return(context, url): def url_keep_return(context: RequestContext, url: str) -> str:
"""Returns the URL with the current "r" query parameter set, so that the """Returns the URL with the current "r" query parameter set, so that the
next processor can still return to the same page. next processor can still return to the same page.
Args: Args:
context (RequestContext): The request context. context: The request context.
url (str): The URL. url: The URL.
Returns: Returns:
str: The URL with the current "r" query parameter set. The URL with the current "r" query parameter set.
""" """
return str(UrlBuilder(url).query(r=context.request.GET.get("r"))) return str(UrlBuilder(url).query(r=context.request.GET.get("r")))
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def add_css(context, url): def add_css(context: RequestContext, url: str) -> str:
"""Adds a local CSS file. The file is added to the "css" template """Adds a local CSS file. The file is added to the "css" template
list variable. list variable.
Args: Args:
context (RequestContext): The request context. context: The request context.
url (str): The URL or path of the CSS file. url: The URL or path of the CSS file.
Returns: Returns:
str: An empty string An empty string
""" """
if "css" not in context.dicts[0]: if "css" not in context.dicts[0]:
context.dicts[0]["css"] = [] context.dicts[0]["css"] = []
@ -119,16 +121,16 @@ def add_css(context, url):
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def add_js(context, url): def add_js(context: RequestContext, url: str) -> str:
"""Adds a local JavaScript file. The file is added to the "js" template """Adds a local JavaScript file. The file is added to the "js" template
list variable. list variable.
Args: Args:
context (RequestContext): The request context. context: The request context.
url (str): The URL or path of the JavaScript file. url: The URL or path of the JavaScript file.
Returns: Returns:
str: An empty string An empty string
""" """
if "js" not in context.dicts[0]: if "js" not in context.dicts[0]:
context.dicts[0]["js"] = [] context.dicts[0]["js"] = []
@ -137,14 +139,14 @@ def add_js(context, url):
@register.filter @register.filter
def smart_date(value): def smart_date(value: datetime.date) -> str:
"""Formats the date for human friendliness. """Formats the date for human friendliness.
Args: Args:
value (datetime.date): The date. value: The date.
Returns: Returns:
str: The human-friendly format of the date. The human-friendly format of the date.
""" """
if value == date.today(): if value == date.today():
return gettext("Today") return gettext("Today")
@ -156,14 +158,14 @@ def smart_date(value):
@register.filter @register.filter
def smart_month(value): def smart_month(value: datetime.date) -> str:
"""Formats the month for human friendliness. """Formats the month for human friendliness.
Args: Args:
value (datetime.date): The month. value: The month.
Returns: Returns:
str: The human-friendly format of the month. The human-friendly format of the month.
""" """
today = timezone.localdate() today = timezone.localdate()
if value.year == today.year and value.month == today.month: if value.year == today.year and value.month == today.month:
@ -179,14 +181,14 @@ def smart_month(value):
@register.filter @register.filter
def title_case(value): def title_case(value: str) -> str:
"""Formats the title in a proper American-English case. """Formats the title in a proper American-English case.
Args: Args:
value (str): The title. value: The title.
Returns: Returns:
str: The title in a proper American-English case. The title in a proper American-English case.
""" """
value = str(value) value = str(value)
if isinstance(value, SafeString): if isinstance(value, SafeString):
@ -195,16 +197,15 @@ def title_case(value):
@register.filter @register.filter
def is_in_section(request, section_name): def is_in_section(request: HttpRequest, section_name: str) -> bool:
"""Returns whether the request is currently in a section. """Returns whether the request is currently in a section.
Args: Args:
request (HttpRequest): The request. request: The request.
section_name (str): The view name of this section. section_name: The view name of this section.
Returns: Returns:
bool: True if the request is currently in this section, or False True if the request is currently in this section, or False otherwise.
otherwise
""" """
if request is None: if request is None:
return False return False

View File

@ -20,20 +20,22 @@
""" """
import random import random
import urllib.parse import urllib.parse
from typing import Dict, List, Any, Type
from django.conf import settings from django.conf import settings
from django.db.models import Model, Q from django.db.models import Model, Q
from django.http import HttpRequest
from django.utils.translation import pgettext, get_language from django.utils.translation import pgettext, get_language
def new_pk(cls): def new_pk(cls: Type[Model]) -> int:
"""Finds a random ID that does not conflict with the existing data records. """Finds a random ID that does not conflict with the existing data records.
Args: Args:
cls (class): The Django model class. cls: The Django model class.
Returns: Returns:
int: The new random ID. The new random ID.
""" """
while True: while True:
pk = random.randint(100000000, 999999999) pk = random.randint(100000000, 999999999)
@ -43,7 +45,7 @@ def new_pk(cls):
return pk return pk
def strip_post(post): def strip_post(post: Dict[str, str]) -> None:
"""Strips the values of the POSTed data. Empty strings are removed. """Strips the values of the POSTed data. Empty strings are removed.
Args: Args:
@ -59,7 +61,7 @@ class Language:
"""A language. """A language.
Args: Args:
language (str): The Django language code. language: The Django language code.
Attributes: Attributes:
id (str): The language ID id (str): The language ID
@ -67,7 +69,7 @@ class Language:
locale (str); The locale name of this language. locale (str); The locale name of this language.
is_default (bool): Whether this is the default language. is_default (bool): Whether this is the default language.
""" """
def __init__(self, language): def __init__(self, language: str):
self.id = language self.id = language
self.db = "_" + language.lower().replace("-", "_") self.db = "_" + language.lower().replace("-", "_")
if language == "zh-hant": if language == "zh-hant":
@ -87,17 +89,18 @@ class Language:
return Language(get_language()) return Language(get_language())
def get_multi_lingual_attr(model, name, default=None): def get_multi_lingual_attr(model: Model, name: str,
default: str = None) -> str:
"""Returns a multi-lingual attribute of a data model. """Returns a multi-lingual attribute of a data model.
Args: Args:
model (object): The data model. model: The data model.
name (str): The attribute name. name: The attribute name.
default (str): The default language. default: The default language.
Returns: Returns:
(any): The attribute in this language, or in the default The attribute in this language, or in the default language if there is
language if there is no content in the current language. no content in the current language.
""" """
language = Language.current() language = Language.current()
title = getattr(model, name + language.db) title = getattr(model, name + language.db)
@ -110,27 +113,27 @@ def get_multi_lingual_attr(model, name, default=None):
return getattr(model, name + Language.default().db) return getattr(model, name + Language.default().db)
def set_multi_lingual_attr(model, name, value): def set_multi_lingual_attr(model: Model, name: str, value: str) -> None:
"""Sets a multi-lingual attribute of a data model. """Sets a multi-lingual attribute of a data model.
Args: Args:
model (object): The data model. model: The data model.
name (str): The attribute name. name: The attribute name.
value (any): The new value value: The new value
""" """
language = Language.current() language = Language.current()
setattr(model, name + language.db, value) setattr(model, name + language.db, value)
def get_multi_lingual_search(attr, query): def get_multi_lingual_search(attr: str, query: str) -> Q:
"""Returns the query condition on a multi-lingual attribute. """Returns the query condition on a multi-lingual attribute.
Args: Args:
attr (str): The base name of the multi-lingual attribute. attr: The base name of the multi-lingual attribute.
query (str): The query. query: The query.
Returns: Returns:
Q: The query condition The query condition
""" """
language = Language.current() language = Language.current()
if language.is_default: if language.is_default:
@ -147,10 +150,10 @@ class UrlBuilder:
"""The URL builder. """The URL builder.
Attributes: Attributes:
base_path (str): the base path path (str): the base path
params (list[Param]): The query parameters params (list[Param]): The query parameters
""" """
def __init__(self, start_url): def __init__(self, start_url: str):
"""Constructs a new URL builder. """Constructs a new URL builder.
Args: Args:
@ -158,10 +161,10 @@ class UrlBuilder:
""" """
pos = start_url.find("?") pos = start_url.find("?")
if pos == -1: if pos == -1:
self.base_path = start_url self.path = start_url
self.params = [] self.params = []
return return
self.base_path = start_url[:pos] self.path = start_url[:pos]
self.params = [] self.params = []
for piece in start_url[pos + 1:].split("&"): for piece in start_url[pos + 1:].split("&"):
pos = piece.find("=") pos = piece.find("=")
@ -219,25 +222,25 @@ class UrlBuilder:
Returns: Returns:
UrlBuilder: A copy of this URL builder. UrlBuilder: A copy of this URL builder.
""" """
another = UrlBuilder(self.base_path) another = UrlBuilder(self.path)
another.params = [ another.params = [
self.Param(x.name, x.value) for x in self.params] self.Param(x.name, x.value) for x in self.params]
return another return another
def __str__(self): def __str__(self) -> str:
if len(self.params) == 0: if len(self.params) == 0:
return self.base_path return self.path
return self.base_path + "?" + "&".join([ return self.path + "?" + "&".join([
str(x) for x in self.params]) str(x) for x in self.params])
class Param: class Param:
"""A query parameter. """A query parameter.
Attributes: Attributes:
name (str): The parameter name name: The parameter name
value (str): The parameter value value: The parameter value
""" """
def __init__(self, name, value): def __init__(self, name: str, value: str):
"""Constructs a new query parameter """Constructs a new query parameter
Args: Args:
@ -247,7 +250,7 @@ class UrlBuilder:
self.name = name self.name = name
self.value = value self.value = value
def __str__(self): def __str__(self) -> str:
"""Returns the string representation of this query """Returns the string representation of this query
parameter. parameter.
@ -264,9 +267,9 @@ class Pagination:
"""The pagination. """The pagination.
Args: Args:
request (HttpRequest): The request. request: The request.
items (list): All the items. items: All the items.
is_reversed (bool): Whether we should display the last page first. is_reversed: Whether we should display the last page first.
Raises: Raises:
PaginationException: With invalid pagination parameters PaginationException: With invalid pagination parameters
@ -282,7 +285,8 @@ class Pagination:
""" """
DEFAULT_PAGE_SIZE = 10 DEFAULT_PAGE_SIZE = 10
def __init__(self, request, items, is_reversed=False): def __init__(self, request: HttpRequest, items: List[Any],
is_reversed: bool = False):
self.current_url = UrlBuilder(request.get_full_path()) self.current_url = UrlBuilder(request.get_full_path())
self.is_reversed = is_reversed self.is_reversed = is_reversed
self.page_size = self.DEFAULT_PAGE_SIZE self.page_size = self.DEFAULT_PAGE_SIZE
@ -334,7 +338,7 @@ class Pagination:
"""Returns the navigation links of the pagination bar. """Returns the navigation links of the pagination bar.
Returns: Returns:
list[Link]: The navigation links of the pagination bar. List[Link]: The navigation links of the pagination bar.
""" """
base_url = self.current_url.clone().remove("page").remove("s") base_url = self.current_url.clone().remove("page").remove("s")
links = [] links = []
@ -443,14 +447,14 @@ class Pagination:
"""Returns the page size options. """Returns the page size options.
Returns: Returns:
list[PageSizeOption]: The page size options. List[PageSizeOption]: The page size options.
""" """
base_url = self.current_url.remove("page").remove("page-size") base_url = self.current_url.remove("page").remove("page-size")
return [self.PageSizeOption(x, self._page_size_url(base_url, x)) return [self.PageSizeOption(x, self._page_size_url(base_url, x))
for x in [10, 100, 200]] for x in [10, 100, 200]]
@staticmethod @staticmethod
def _page_size_url(base_url, size): def _page_size_url(base_url: UrlBuilder, size: int) -> str:
"""Returns the URL for a new page size. """Returns the URL for a new page size.
Args: Args:
@ -468,14 +472,14 @@ class Pagination:
"""A page size option. """A page size option.
Args: Args:
size (int): The page size. size: The page size.
url (str): The URL of this page size. url: The URL of this page size.
Attributes: Attributes:
size (int): The page size. size (int): The page size.
url (str): The URL for this page size. url (str): The URL for this page size.
""" """
def __init__(self, size, url): def __init__(self, size: int, url: str):
self.size = size self.size = size
self.url = url self.url = url
@ -484,10 +488,10 @@ class PaginationException(Exception):
"""The exception thrown with invalid pagination parameters. """The exception thrown with invalid pagination parameters.
Args: Args:
url_builder (UrlBuilder): The canonical URL to redirect to. url_builder: The canonical URL to redirect to.
Attributes: Attributes:
url (str): The canonical URL to redirect to. url (str): The canonical URL to redirect to.
""" """
def __init__(self, url_builder): def __init__(self, url_builder: UrlBuilder):
self.url = str(url_builder) self.url = str(url_builder)

View File

@ -21,7 +21,8 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import logout as logout_user from django.contrib.auth import logout as logout_user
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpResponse, JsonResponse from django.http import HttpResponse, JsonResponse, HttpRequest, \
HttpResponseRedirect
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@ -47,14 +48,14 @@ class DeleteView(SuccessMessageMixin, CoreDeleteView):
@require_POST @require_POST
def logout(request): def logout(request: HttpRequest) -> HttpResponseRedirect:
"""The view to log out a user. """The view to log out a user.
Args: Args:
request (HttpRequest): The request. request: The request.
Returns: Returns:
HttpRedirectResponse: The redirect response. The redirect response.
""" """
logout_user(request) logout_user(request)
if "next" in request.POST: if "next" in request.POST:
@ -80,15 +81,15 @@ class UserView(DetailView):
@require_GET @require_GET
@login_required @login_required
def user_form(request, user=None): def user_form(request: HttpRequest, user: User = None) -> HttpResponse:
"""The view to edit an accounting transaction. """The view to edit an accounting transaction.
Args: Args:
request (HttpRequest): The request. request: The request.
user (User): The account. user: The account.
Returns: Returns:
HttpResponse: The response. The response.
""" """
previous_post = stored_post.get_previous_post(request) previous_post = stored_post.get_previous_post(request)
if previous_post is not None: if previous_post is not None:
@ -108,15 +109,16 @@ def user_form(request, user=None):
}) })
def user_store(request, user=None): def user_store(request: HttpRequest,
user: User = None) -> HttpResponseRedirect:
"""The view to store a user. """The view to store a user.
Args: Args:
request (HttpRequest): The request. request: The request.
user (Account): The user. user: The user.
Returns: Returns:
HttpResponseRedirect: The response. The response.
""" """
post = request.POST.dict() post = request.POST.dict()
strip_post(post) strip_post(post)
@ -148,15 +150,15 @@ def user_store(request, user=None):
@require_POST @require_POST
@login_required @login_required
def user_delete(request, user): def user_delete(request: HttpRequest, user: User) -> HttpResponseRedirect:
"""The view to delete an user. """The view to delete an user.
Args: Args:
request (HttpRequest): The request. request: The request.
user (User): The user. user: The user.
Returns: Returns:
HttpResponseRedirect: The response. The response.
""" """
message = None message = None
if user.pk == request.user.pk: if user.pk == request.user.pk:
@ -177,14 +179,14 @@ def user_delete(request, user):
@require_GET @require_GET
@login_required @login_required
def my_account_form(request): def my_account_form(request: HttpRequest) -> HttpResponse:
"""The view to edit my account. """The view to edit my account.
Args: Args:
request (HttpRequest): The request. request: The request.
Returns: Returns:
HttpResponse: The response. The response.
""" """
previous_post = stored_post.get_previous_post(request) previous_post = stored_post.get_previous_post(request)
if previous_post is not None: if previous_post is not None:
@ -201,14 +203,14 @@ def my_account_form(request):
}) })
def my_account_store(request): def my_account_store(request: HttpRequest) -> HttpResponseRedirect:
"""The view to store my account. """The view to store my account.
Args: Args:
request (HttpRequest): The request. request: The request.
Returns: Returns:
HttpResponseRedirect: The response. The response.
""" """
post = request.POST.dict() post = request.POST.dict()
strip_post(post) strip_post(post)
@ -232,15 +234,15 @@ def my_account_store(request):
return redirect("mia_core:my-account") return redirect("mia_core:my-account")
def api_users_exists(request, login_id): def api_users_exists(request: HttpRequest, login_id: str) -> JsonResponse:
"""The view to check whether a user with a log in ID exists. """The view to check whether a user with a log in ID exists.
Args: Args:
request (HttpRequest): The request. request: The request.
login_id (str): The log in ID. login_id: The log in ID.
Returns: Returns:
JsonResponse: The response. The response.
""" """
try: try:
User.objects.get(login_id=login_id) User.objects.get(login_id=login_id)