Moved the local part of the application from the Mia core application to the Mia Womb local application.
This commit is contained in:
		@@ -26,7 +26,7 @@ from django.db import transaction
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
 | 
			
		||||
from accounting.utils import Populator
 | 
			
		||||
from mia_core.models import User
 | 
			
		||||
from mia_womb.models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ from django.urls import path, register_converter
 | 
			
		||||
from django.views.decorators.http import require_GET
 | 
			
		||||
from django.views.generic import RedirectView
 | 
			
		||||
 | 
			
		||||
from mia_core.digest_auth import login_required
 | 
			
		||||
from mia_womb.digest_auth import login_required
 | 
			
		||||
from . import converters, views
 | 
			
		||||
 | 
			
		||||
register_converter(converters.PeriodConverter, "period")
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ from django.utils.translation import gettext as _, gettext_noop
 | 
			
		||||
from django.views.decorators.http import require_GET, require_POST
 | 
			
		||||
from django.views.generic import RedirectView, ListView, DetailView
 | 
			
		||||
 | 
			
		||||
from mia_core.digest_auth import login_required
 | 
			
		||||
from mia_womb.digest_auth import login_required
 | 
			
		||||
from mia_core.period import Period
 | 
			
		||||
from mia_core.utils import Pagination, get_multi_lingual_search, \
 | 
			
		||||
    PaginationException
 | 
			
		||||
 
 | 
			
		||||
@@ -1,54 +0,0 @@
 | 
			
		||||
# The core application of the Mia project.
 | 
			
		||||
#   by imacat <imacat@mail.imacat.idv.tw>, 2020/8/9
 | 
			
		||||
 | 
			
		||||
#  Copyright (c) 2020 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 URL converters.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from .models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserConverter:
 | 
			
		||||
    """The path converter for the user accounts."""
 | 
			
		||||
    regex = ".*"
 | 
			
		||||
 | 
			
		||||
    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:
 | 
			
		||||
            return User.objects.get(login_id=value)
 | 
			
		||||
        except User.DoesNotExist:
 | 
			
		||||
            raise ValueError
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
@@ -1,179 +0,0 @@
 | 
			
		||||
# The core application of the Mia project.
 | 
			
		||||
#   by imacat <imacat@mail.imacat.idv.tw>, 2020/7/5
 | 
			
		||||
 | 
			
		||||
#  Copyright (c) 2020 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 HTTP digest authentication utilities of the Mia core
 | 
			
		||||
application.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
import ipaddress
 | 
			
		||||
import socket
 | 
			
		||||
from functools import wraps
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db.models import F
 | 
			
		||||
from django.db.models.functions import Now
 | 
			
		||||
from django.http import HttpResponse, HttpRequest
 | 
			
		||||
from geoip import geolite2
 | 
			
		||||
 | 
			
		||||
from .models import User, Country
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AccountBackend:
 | 
			
		||||
    """The account backend for the django-digest module."""
 | 
			
		||||
 | 
			
		||||
    def get_partial_digest(self, username: str) -> Optional[str]:
 | 
			
		||||
        """Returns the HTTP digest authentication password digest hash
 | 
			
		||||
        of a user.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            username: The log in user name.
 | 
			
		||||
 | 
			
		||||
        Return:
 | 
			
		||||
            The HTTP digest authentication password hash of the user, or None
 | 
			
		||||
            if the user does not exist.
 | 
			
		||||
        """
 | 
			
		||||
        user = User.objects.filter(login_id=username).first()
 | 
			
		||||
        if user is None:
 | 
			
		||||
            return None
 | 
			
		||||
        return user.password
 | 
			
		||||
 | 
			
		||||
    def get_user(self, username: str) -> Optional[User]:
 | 
			
		||||
        """Returns the user by her log in user name.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            username: The log in user name.
 | 
			
		||||
 | 
			
		||||
        Return:
 | 
			
		||||
            The user, or None if the user does not exist.
 | 
			
		||||
        """
 | 
			
		||||
        return User.objects.filter(login_id=username).first()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def login_required(function=None):
 | 
			
		||||
    """The decorator to check if the user has logged in, and send
 | 
			
		||||
    HTTP 401 if the user has not logged in.
 | 
			
		||||
    """
 | 
			
		||||
    def decorator(view_func):
 | 
			
		||||
        @wraps(view_func)
 | 
			
		||||
        def _wrapped_view(request, *args, **kwargs):
 | 
			
		||||
            if request.user.is_anonymous:
 | 
			
		||||
                return HttpResponse(status=401)
 | 
			
		||||
            if "logout" in request.session:
 | 
			
		||||
                del request.session["logout"]
 | 
			
		||||
                if "visit_logged" in request.session:
 | 
			
		||||
                    del request.session["visit_logged"]
 | 
			
		||||
                return HttpResponse(status=401)
 | 
			
		||||
            if not settings.DEBUG:
 | 
			
		||||
                _log_visit(request)
 | 
			
		||||
            return view_func(request, *args, **kwargs)
 | 
			
		||||
        return _wrapped_view
 | 
			
		||||
    if function:
 | 
			
		||||
        return decorator(function)
 | 
			
		||||
    return decorator
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _log_visit(request: HttpRequest) -> None:
 | 
			
		||||
    """Logs the visit information for the logged-in user.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        request (HttpRequest): The request.
 | 
			
		||||
    """
 | 
			
		||||
    if "visit_logged" in request.session:
 | 
			
		||||
        return
 | 
			
		||||
    user = request.user
 | 
			
		||||
    ip = _get_remote_ip(request)
 | 
			
		||||
    User.objects.filter(pk=user.pk).update(
 | 
			
		||||
        visits=F("visits") + 1,
 | 
			
		||||
        visited_at=Now(),
 | 
			
		||||
        visited_ip=ip,
 | 
			
		||||
        visited_host=_get_host(ip),
 | 
			
		||||
        visited_country=_get_country(ip),
 | 
			
		||||
    )
 | 
			
		||||
    request.session["visit_logged"] = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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")
 | 
			
		||||
    if x_forwarded_for:
 | 
			
		||||
        return x_forwarded_for.split(",")[0]
 | 
			
		||||
    return request.META.get('REMOTE_ADDR')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _get_host(ip: str) -> Optional[str]:
 | 
			
		||||
    """Look-up the host name by its IP.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        ip: The IP
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        The host name, or None if the look-up fails.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        return socket.gethostbyaddr(ip)[0]
 | 
			
		||||
    except socket.herror:
 | 
			
		||||
        return None
 | 
			
		||||
    except socket.gaierror:
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _get_country(ip: str) -> Optional[Country]:
 | 
			
		||||
    """Look-up the country by its IP.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        ip: The IP
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        The country.
 | 
			
		||||
    """
 | 
			
		||||
    code = _get_country_code(ip)
 | 
			
		||||
    try:
 | 
			
		||||
        return Country.objects.get(code=code)
 | 
			
		||||
    except Country.DoesNotExist:
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _get_country_code(ip: str) -> Optional[str]:
 | 
			
		||||
    """Look-up the country code by its IP.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        ip: The IP
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        The country code, or None if the look-up fails.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        return geolite2.lookup(ip).country
 | 
			
		||||
    except ValueError:
 | 
			
		||||
        pass
 | 
			
		||||
    except AttributeError:
 | 
			
		||||
        pass
 | 
			
		||||
    try:
 | 
			
		||||
        ipaddr = ipaddress.ip_address(ip)
 | 
			
		||||
        if ipaddr.is_private:
 | 
			
		||||
            return "AA"
 | 
			
		||||
    except ValueError:
 | 
			
		||||
        pass
 | 
			
		||||
    return None
 | 
			
		||||
@@ -1,183 +0,0 @@
 | 
			
		||||
# The core application of the Mia project.
 | 
			
		||||
#   by imacat <imacat@mail.imacat.idv.tw>, 2020/8/9
 | 
			
		||||
 | 
			
		||||
#  Copyright (c) 2020 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 forms of the Mia core application.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.core.validators import RegexValidator
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
 | 
			
		||||
from mia_core.models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserForm(forms.Form):
 | 
			
		||||
    """A user account form."""
 | 
			
		||||
    login_id = forms.CharField(
 | 
			
		||||
        max_length=32,
 | 
			
		||||
        error_messages={
 | 
			
		||||
            "required": _("Please fill in the log in ID."),
 | 
			
		||||
            "max_length": _("This log in ID is too long (max 32 characters)."),
 | 
			
		||||
        },
 | 
			
		||||
        validators=[
 | 
			
		||||
            RegexValidator(
 | 
			
		||||
                regex="^[^/]+$",
 | 
			
		||||
                message=_("You cannot use slash (/) in the log in ID.")),
 | 
			
		||||
        ])
 | 
			
		||||
    password = forms.CharField(required=False)
 | 
			
		||||
    password2 = forms.CharField(required=False)
 | 
			
		||||
    name = forms.CharField(
 | 
			
		||||
        max_length=32,
 | 
			
		||||
        error_messages={
 | 
			
		||||
            "required": _("Please fill in the name."),
 | 
			
		||||
            "max_length": _("This name is too long (max 32 characters)."),
 | 
			
		||||
        })
 | 
			
		||||
    is_disabled = forms.BooleanField(required=False)
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.user = None
 | 
			
		||||
        self.current_user = None
 | 
			
		||||
 | 
			
		||||
    def clean(self):
 | 
			
		||||
        """Validates the form globally.
 | 
			
		||||
 | 
			
		||||
        Raises:
 | 
			
		||||
            ValidationError: When the validation fails.
 | 
			
		||||
        """
 | 
			
		||||
        errors = []
 | 
			
		||||
        validators = [self._validate_login_id_unique,
 | 
			
		||||
                      self._validate_password_new_required,
 | 
			
		||||
                      self._validate_password_login_id_changed_required,
 | 
			
		||||
                      self._validate_password2_required,
 | 
			
		||||
                      self._validate_passwords_equal,
 | 
			
		||||
                      self._validate_is_disabled_not_oneself]
 | 
			
		||||
        for validator in validators:
 | 
			
		||||
            try:
 | 
			
		||||
                validator()
 | 
			
		||||
            except forms.ValidationError as e:
 | 
			
		||||
                errors.append(e)
 | 
			
		||||
        if errors:
 | 
			
		||||
            raise forms.ValidationError(errors)
 | 
			
		||||
 | 
			
		||||
    def _validate_login_id_unique(self) -> None:
 | 
			
		||||
        """Validates whether the log in ID is unique.
 | 
			
		||||
 | 
			
		||||
        Raises:
 | 
			
		||||
            forms.ValidationError: When the validation fails.
 | 
			
		||||
        """
 | 
			
		||||
        if "login_id" not in self.data:
 | 
			
		||||
            return
 | 
			
		||||
        condition = Q(login_id=self.data["login_id"])
 | 
			
		||||
        if self.user is not None:
 | 
			
		||||
            condition = condition & ~Q(pk=self.user.pk)
 | 
			
		||||
        if User.objects.filter(condition).first() is None:
 | 
			
		||||
            return
 | 
			
		||||
        error = forms.ValidationError(_("This log in ID is already in use."),
 | 
			
		||||
                                      code="login_id_unique")
 | 
			
		||||
        self.add_error("login_id", error)
 | 
			
		||||
        raise error
 | 
			
		||||
 | 
			
		||||
    def _validate_password_new_required(self) -> None:
 | 
			
		||||
        """Validates whether the password is entered for newly-created users.
 | 
			
		||||
 | 
			
		||||
        Raises:
 | 
			
		||||
            forms.ValidationError: When the validation fails.
 | 
			
		||||
        """
 | 
			
		||||
        if self.user is not None:
 | 
			
		||||
            return
 | 
			
		||||
        if "password" in self.data:
 | 
			
		||||
            return
 | 
			
		||||
        error = forms.ValidationError(_("Please fill in the password."),
 | 
			
		||||
                                      code="password_required")
 | 
			
		||||
        self.add_error("password", error)
 | 
			
		||||
        raise error
 | 
			
		||||
 | 
			
		||||
    def _validate_password_login_id_changed_required(self) -> None:
 | 
			
		||||
        """Validates whether the password is entered for users whose login ID
 | 
			
		||||
        changed.
 | 
			
		||||
 | 
			
		||||
        Raises:
 | 
			
		||||
            forms.ValidationError: When the validation fails.
 | 
			
		||||
        """
 | 
			
		||||
        if self.user is None:
 | 
			
		||||
            return
 | 
			
		||||
        if "login_id" not in self.data:
 | 
			
		||||
            return
 | 
			
		||||
        if self.data["login_id"] == self.user.login_id:
 | 
			
		||||
            return
 | 
			
		||||
        if "password" in self.data:
 | 
			
		||||
            return
 | 
			
		||||
        error = forms.ValidationError(
 | 
			
		||||
            _("Please fill in the password to change the log in ID."),
 | 
			
		||||
            code="password_required")
 | 
			
		||||
        self.add_error("password", error)
 | 
			
		||||
        raise error
 | 
			
		||||
 | 
			
		||||
    def _validate_password2_required(self) -> None:
 | 
			
		||||
        """Validates whether the second password is entered.
 | 
			
		||||
 | 
			
		||||
        Raises:
 | 
			
		||||
            forms.ValidationError: When the validation fails.
 | 
			
		||||
        """
 | 
			
		||||
        if "password" not in self.data:
 | 
			
		||||
            return
 | 
			
		||||
        if "password2" in self.data:
 | 
			
		||||
            return
 | 
			
		||||
        error = forms.ValidationError(
 | 
			
		||||
            _("Please enter the password again to verify it."),
 | 
			
		||||
            code="password2_required")
 | 
			
		||||
        self.add_error("password2", error)
 | 
			
		||||
        raise error
 | 
			
		||||
 | 
			
		||||
    def _validate_passwords_equal(self) -> None:
 | 
			
		||||
        """Validates whether the two passwords are equal.
 | 
			
		||||
 | 
			
		||||
        Raises:
 | 
			
		||||
            forms.ValidationError: When the validation fails.
 | 
			
		||||
        """
 | 
			
		||||
        if "password" not in self.data:
 | 
			
		||||
            return
 | 
			
		||||
        if "password2" not in self.data:
 | 
			
		||||
            return
 | 
			
		||||
        if self.data["password"] == self.data["password2"]:
 | 
			
		||||
            return
 | 
			
		||||
        error = forms.ValidationError(_("The two passwords do not match."),
 | 
			
		||||
                                      code="passwords_equal")
 | 
			
		||||
        self.add_error("password2", error)
 | 
			
		||||
        raise error
 | 
			
		||||
 | 
			
		||||
    def _validate_is_disabled_not_oneself(self) -> None:
 | 
			
		||||
        """Validates whether the user tries to disable herself
 | 
			
		||||
 | 
			
		||||
        Raises:
 | 
			
		||||
            forms.ValidationError: When the validation fails.
 | 
			
		||||
        """
 | 
			
		||||
        if "is_disabled" not in self.data:
 | 
			
		||||
            return
 | 
			
		||||
        if self.user is None:
 | 
			
		||||
            return
 | 
			
		||||
        if self.current_user is None:
 | 
			
		||||
            return
 | 
			
		||||
        if self.user.pk != self.current_user.pk:
 | 
			
		||||
            return
 | 
			
		||||
        error = forms.ValidationError(
 | 
			
		||||
            _("You cannot disable your own account."),
 | 
			
		||||
            code="not_oneself")
 | 
			
		||||
        self.add_error("is_disabled", error)
 | 
			
		||||
        raise error
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
# Traditional Chinese PO file for the Mia Website
 | 
			
		||||
# Traditional Chinese PO file for the Mia core application
 | 
			
		||||
# Copyright (C) 2020 imacat
 | 
			
		||||
# This file is distributed under the same license as the Mia package.
 | 
			
		||||
# imacat <imacat@mail.imacat.idv.tw>, 2020.
 | 
			
		||||
@@ -7,8 +7,8 @@ msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Project-Id-Version: mia-core 3.0\n"
 | 
			
		||||
"Report-Msgid-Bugs-To: \n"
 | 
			
		||||
"POT-Creation-Date: 2020-08-11 21:41+0800\n"
 | 
			
		||||
"PO-Revision-Date: 2020-08-11 21:44+0800\n"
 | 
			
		||||
"POT-Creation-Date: 2020-08-17 23:56+0800\n"
 | 
			
		||||
"PO-Revision-Date: 2020-08-18 00:02+0800\n"
 | 
			
		||||
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
 | 
			
		||||
"Language-Team: Traditional Chinese <imacat@mail.imacat.idv.tw>\n"
 | 
			
		||||
"Language: Traditional Chinese\n"
 | 
			
		||||
@@ -16,101 +16,57 @@ msgstr ""
 | 
			
		||||
"Content-Type: text/plain; charset=UTF-8\n"
 | 
			
		||||
"Content-Transfer-Encoding: 8bit\n"
 | 
			
		||||
 | 
			
		||||
#: mia_core/forms.py:34
 | 
			
		||||
msgid "Please fill in the log in ID."
 | 
			
		||||
msgstr "請填寫登入帳號。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/forms.py:35
 | 
			
		||||
msgid "This log in ID is too long (max 32 characters)."
 | 
			
		||||
msgstr "登入帳號太長了(最長32個字)。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/forms.py:40
 | 
			
		||||
msgid "You cannot use slash (/) in the log in ID."
 | 
			
		||||
msgstr "登入帳號不可以包含斜線 (/) 。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/forms.py:47
 | 
			
		||||
msgid "Please fill in the name."
 | 
			
		||||
msgstr "請填寫姓名。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/forms.py:48
 | 
			
		||||
msgid "This name is too long (max 32 characters)."
 | 
			
		||||
msgstr "姓名太長了(最長32個字)。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/forms.py:91
 | 
			
		||||
msgid "This log in ID is already in use."
 | 
			
		||||
msgstr "登入帳號和其他人重複。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/forms.py:106
 | 
			
		||||
msgid "Please fill in the password."
 | 
			
		||||
msgstr "請填寫密碼。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/forms.py:127
 | 
			
		||||
msgid "Please fill in the password to change the log in ID."
 | 
			
		||||
msgstr "變更登入帳號時,請填寫密碼。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/forms.py:143
 | 
			
		||||
msgid "Please enter the password again to verify it."
 | 
			
		||||
msgstr "請再次確認密碼。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/forms.py:160
 | 
			
		||||
msgid "The two passwords do not match."
 | 
			
		||||
msgstr "兩次密碼不符,請重新輸入。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/forms.py:180 mia_core/templates/mia_core/user_form.html:85
 | 
			
		||||
msgid "You cannot disable your own account."
 | 
			
		||||
msgstr "不能停用自己的帳號。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/period.py:447 mia_core/period.py:482 mia_core/period.py:500
 | 
			
		||||
#: mia_core/period.py:513 mia_core/period.py:559
 | 
			
		||||
#: mia_core/period.py:452 mia_core/period.py:487 mia_core/period.py:505
 | 
			
		||||
#: mia_core/period.py:518 mia_core/period.py:564
 | 
			
		||||
#, python-format
 | 
			
		||||
msgid "In %s"
 | 
			
		||||
msgstr "%s"
 | 
			
		||||
 | 
			
		||||
#: mia_core/period.py:457
 | 
			
		||||
#: mia_core/period.py:462
 | 
			
		||||
#, python-format
 | 
			
		||||
msgid "Since %s"
 | 
			
		||||
msgstr "%s至今"
 | 
			
		||||
 | 
			
		||||
#: mia_core/period.py:470 mia_core/period.py:491 mia_core/period.py:570
 | 
			
		||||
#: mia_core/period.py:475 mia_core/period.py:496 mia_core/period.py:575
 | 
			
		||||
#, python-format
 | 
			
		||||
msgid "Until %s"
 | 
			
		||||
msgstr "至%s前"
 | 
			
		||||
 | 
			
		||||
#: mia_core/period.py:499
 | 
			
		||||
#: mia_core/period.py:504
 | 
			
		||||
msgid "All Time"
 | 
			
		||||
msgstr "全部"
 | 
			
		||||
 | 
			
		||||
#: mia_core/period.py:583 mia_core/period.py:616
 | 
			
		||||
#: mia_core/period.py:588 mia_core/period.py:621
 | 
			
		||||
#: mia_core/templates/mia_core/include/period-chooser.html:60
 | 
			
		||||
#: mia_core/templatetags/mia_core.py:170
 | 
			
		||||
#: mia_core/templatetags/mia_core.py:173
 | 
			
		||||
msgid "This Month"
 | 
			
		||||
msgstr "這個月"
 | 
			
		||||
 | 
			
		||||
#: mia_core/period.py:624
 | 
			
		||||
#: mia_core/period.py:629
 | 
			
		||||
#: mia_core/templates/mia_core/include/period-chooser.html:63
 | 
			
		||||
#: mia_core/templatetags/mia_core.py:177
 | 
			
		||||
#: mia_core/templatetags/mia_core.py:180
 | 
			
		||||
msgid "Last Month"
 | 
			
		||||
msgstr "上個月"
 | 
			
		||||
 | 
			
		||||
#: mia_core/period.py:639
 | 
			
		||||
#: mia_core/period.py:644
 | 
			
		||||
#: mia_core/templates/mia_core/include/period-chooser.html:76
 | 
			
		||||
msgid "This Year"
 | 
			
		||||
msgstr "今年"
 | 
			
		||||
 | 
			
		||||
#: mia_core/period.py:641
 | 
			
		||||
#: mia_core/period.py:646
 | 
			
		||||
#: mia_core/templates/mia_core/include/period-chooser.html:79
 | 
			
		||||
msgid "Last Year"
 | 
			
		||||
msgstr "去年"
 | 
			
		||||
 | 
			
		||||
#: mia_core/period.py:656
 | 
			
		||||
#: mia_core/period.py:661
 | 
			
		||||
#: mia_core/templates/mia_core/include/period-chooser.html:95
 | 
			
		||||
#: mia_core/templatetags/mia_core.py:150
 | 
			
		||||
#: mia_core/templatetags/mia_core.py:153
 | 
			
		||||
msgid "Today"
 | 
			
		||||
msgstr "今天"
 | 
			
		||||
 | 
			
		||||
#: mia_core/period.py:658
 | 
			
		||||
#: mia_core/period.py:663
 | 
			
		||||
#: mia_core/templates/mia_core/include/period-chooser.html:98
 | 
			
		||||
#: mia_core/templatetags/mia_core.py:152
 | 
			
		||||
#: mia_core/templatetags/mia_core.py:155
 | 
			
		||||
msgid "Yesterday"
 | 
			
		||||
msgstr "昨天"
 | 
			
		||||
 | 
			
		||||
@@ -144,7 +100,6 @@ msgstr "日期:"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/include/period-chooser.html:107
 | 
			
		||||
#: mia_core/templates/mia_core/include/period-chooser.html:125
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:69
 | 
			
		||||
msgid "Confirm"
 | 
			
		||||
msgstr "確定"
 | 
			
		||||
 | 
			
		||||
@@ -160,198 +115,17 @@ msgstr "從:"
 | 
			
		||||
msgid "To:"
 | 
			
		||||
msgstr "到:"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:39
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:84
 | 
			
		||||
msgid "Settings"
 | 
			
		||||
msgstr "設定"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:46
 | 
			
		||||
msgid "The account is not in use."
 | 
			
		||||
msgstr "帳號未使用。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:60
 | 
			
		||||
msgid "User Deletion Confirmation"
 | 
			
		||||
msgstr "帳號刪除確認"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:65
 | 
			
		||||
msgid "Do you really want to delete this user?"
 | 
			
		||||
msgstr "您真的要刪掉這個帳號嗎?"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:70
 | 
			
		||||
msgid "Cancel"
 | 
			
		||||
msgstr "取消"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:80
 | 
			
		||||
#: mia_core/templates/mia_core/user_form.html:41
 | 
			
		||||
msgid "Back"
 | 
			
		||||
msgstr "回上頁"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:87 mia_core/views.py:163
 | 
			
		||||
msgid "You cannot delete your own account."
 | 
			
		||||
msgstr "不能刪除自己的帳號。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:89
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:94
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:99
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:104
 | 
			
		||||
msgid "Delete"
 | 
			
		||||
msgstr "刪除"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:92 mia_core/views.py:166
 | 
			
		||||
msgid "You cannot delete this account because it is in use."
 | 
			
		||||
msgstr "帳號使用中,不可刪除。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:97 mia_core/views.py:168
 | 
			
		||||
msgid "This account is already deleted."
 | 
			
		||||
msgstr "帳號已刪除。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:111
 | 
			
		||||
#: mia_core/templates/mia_core/user_form.html:50
 | 
			
		||||
msgid "Log in ID.:"
 | 
			
		||||
msgstr "登入帳號:"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:116
 | 
			
		||||
#: mia_core/templates/mia_core/user_form.html:74
 | 
			
		||||
msgid "Name:"
 | 
			
		||||
msgstr "姓名:"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:122
 | 
			
		||||
#: mia_core/templates/mia_core/user_form.html:89
 | 
			
		||||
msgid "This account is disabled."
 | 
			
		||||
msgstr "帳號停用。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:128
 | 
			
		||||
msgid "This account is deleted."
 | 
			
		||||
msgstr "帳號已刪。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:134
 | 
			
		||||
msgid "This user has not logged in yet."
 | 
			
		||||
msgstr "使用者還沒登入過。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:138
 | 
			
		||||
msgid "# of visits:"
 | 
			
		||||
msgstr "登入次數:"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:143
 | 
			
		||||
msgid "Last visit at:"
 | 
			
		||||
msgstr "上次登入時間:"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:148
 | 
			
		||||
msgid "IP:"
 | 
			
		||||
msgstr "IP:"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:153
 | 
			
		||||
msgid "Host:"
 | 
			
		||||
msgstr "主機名稱:"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:158
 | 
			
		||||
msgid "Country:"
 | 
			
		||||
msgstr "國家:"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:159
 | 
			
		||||
msgid "(Not Available)"
 | 
			
		||||
msgstr "(不可考)"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:164
 | 
			
		||||
msgid "Created at:"
 | 
			
		||||
msgstr "建檔時間:"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:169
 | 
			
		||||
msgid "Created by:"
 | 
			
		||||
msgstr "建檔人:"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:174
 | 
			
		||||
msgid "Updated at:"
 | 
			
		||||
msgstr "更新時間:"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_detail.html:179
 | 
			
		||||
msgid "Updated by:"
 | 
			
		||||
msgstr "更新人:"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_form.html:31
 | 
			
		||||
msgid "Add a New Account"
 | 
			
		||||
msgstr "建立帳號"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_form.html:58
 | 
			
		||||
msgid "Password:"
 | 
			
		||||
msgstr "密碼:"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_form.html:66
 | 
			
		||||
msgid "Confirm password:"
 | 
			
		||||
msgstr "確認密碼:"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_form.html:99
 | 
			
		||||
msgid "Submit"
 | 
			
		||||
msgstr "傳送"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_list.html:27
 | 
			
		||||
msgid "Account Management"
 | 
			
		||||
msgstr "帳號管理"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_list.html:35
 | 
			
		||||
msgid "New"
 | 
			
		||||
msgstr "新增"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_list.html:43
 | 
			
		||||
msgid "Log in ID."
 | 
			
		||||
msgstr "登入帳號"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_list.html:44
 | 
			
		||||
msgid "Name"
 | 
			
		||||
msgstr "姓名"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_list.html:45
 | 
			
		||||
#: mia_core/templates/mia_core/user_list.html:67
 | 
			
		||||
msgid "View"
 | 
			
		||||
msgstr "查閱"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_list.html:55
 | 
			
		||||
msgid "Disabled"
 | 
			
		||||
msgstr "停用"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_list.html:58
 | 
			
		||||
msgid "Deleted"
 | 
			
		||||
msgstr "已刪"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_list.html:61
 | 
			
		||||
msgid "Not In Use"
 | 
			
		||||
msgstr "未使用"
 | 
			
		||||
 | 
			
		||||
#: mia_core/templates/mia_core/user_list.html:75
 | 
			
		||||
msgid "There is currently no data."
 | 
			
		||||
msgstr "目前沒有資料。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/utils.py:343
 | 
			
		||||
#: mia_core/utils.py:347
 | 
			
		||||
msgctxt "Pagination|"
 | 
			
		||||
msgid "Previous"
 | 
			
		||||
msgstr "上一頁"
 | 
			
		||||
 | 
			
		||||
#: mia_core/utils.py:371 mia_core/utils.py:392
 | 
			
		||||
#: mia_core/utils.py:375 mia_core/utils.py:396
 | 
			
		||||
msgctxt "Pagination|"
 | 
			
		||||
msgid "..."
 | 
			
		||||
msgstr "…"
 | 
			
		||||
 | 
			
		||||
#: mia_core/utils.py:411
 | 
			
		||||
#: mia_core/utils.py:415
 | 
			
		||||
msgctxt "Pagination|"
 | 
			
		||||
msgid "Next"
 | 
			
		||||
msgstr "下一頁"
 | 
			
		||||
 | 
			
		||||
#: mia_core/views.py:141
 | 
			
		||||
msgid "This user account was not changed."
 | 
			
		||||
msgstr "帳號未異動。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/views.py:144
 | 
			
		||||
msgid "This user account was saved successfully."
 | 
			
		||||
msgstr "帳號已儲存。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/views.py:173
 | 
			
		||||
msgid "This user account was deleted successfully."
 | 
			
		||||
msgstr "帳號已刪除。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/views.py:229
 | 
			
		||||
msgid "Your user account was not changed."
 | 
			
		||||
msgstr "你的帳號未異動。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/views.py:232
 | 
			
		||||
msgid "Your user account was saved successfully."
 | 
			
		||||
msgstr "你的帳號已儲存。"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,50 +0,0 @@
 | 
			
		||||
# Traditional Chinese PO file for the JavaScript on the Mia Website
 | 
			
		||||
# Copyright (C) 2020 imacat
 | 
			
		||||
# This file is distributed under the same license as the Mia package.
 | 
			
		||||
# imacat <imacat@mail.imacat.idv.tw>, 2020.
 | 
			
		||||
#
 | 
			
		||||
msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Project-Id-Version: mia-core-js 3.0\n"
 | 
			
		||||
"Report-Msgid-Bugs-To: \n"
 | 
			
		||||
"POT-Creation-Date: 2020-08-11 21:42+0800\n"
 | 
			
		||||
"PO-Revision-Date: 2020-08-11 21:44+0800\n"
 | 
			
		||||
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
 | 
			
		||||
"Language-Team: Traditional Chinese <imacat@mail.imacat.idv.tw>\n"
 | 
			
		||||
"Language: Traditional Chinese\n"
 | 
			
		||||
"MIME-Version: 1.0\n"
 | 
			
		||||
"Content-Type: text/plain; charset=UTF-8\n"
 | 
			
		||||
"Content-Transfer-Encoding: 8bit\n"
 | 
			
		||||
"Plural-Forms: nplurals=1; plural=0;\n"
 | 
			
		||||
 | 
			
		||||
#: mia_core/static/mia_core/js/user-form.js:154
 | 
			
		||||
msgid "Please fill in the log in ID."
 | 
			
		||||
msgstr "請填寫登入帳號。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/static/mia_core/js/user-form.js:159
 | 
			
		||||
msgid "You cannot use slash (/) in the log in ID."
 | 
			
		||||
msgstr "登入帳號不可以包含斜線 (/) 。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/static/mia_core/js/user-form.js:179
 | 
			
		||||
msgid "This log in ID is already in use."
 | 
			
		||||
msgstr "登入帳號和其他人重複。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/static/mia_core/js/user-form.js:204
 | 
			
		||||
msgid "Please fill in the password to change the log in ID."
 | 
			
		||||
msgstr "變更登入帳號時,請填寫密碼。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/static/mia_core/js/user-form.js:206
 | 
			
		||||
msgid "Please fill in the password."
 | 
			
		||||
msgstr "請填寫密碼。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/static/mia_core/js/user-form.js:231
 | 
			
		||||
msgid "Please enter the password again to verify it."
 | 
			
		||||
msgstr "請再次確認密碼。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/static/mia_core/js/user-form.js:237
 | 
			
		||||
msgid "The two passwords do not match."
 | 
			
		||||
msgstr "兩次密碼不符,請重新輸入。"
 | 
			
		||||
 | 
			
		||||
#: mia_core/static/mia_core/js/user-form.js:258
 | 
			
		||||
msgid "Please fill in the name."
 | 
			
		||||
msgstr "請填寫姓名。"
 | 
			
		||||
@@ -1,189 +0,0 @@
 | 
			
		||||
# The core application of the Mia project.
 | 
			
		||||
#   by imacat <imacat@mail.imacat.idv.tw>, 2020/6/29
 | 
			
		||||
 | 
			
		||||
#  Copyright (c) 2020 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 data models of the Mia core application.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
import hashlib
 | 
			
		||||
 | 
			
		||||
from dirtyfields import DirtyFieldsMixin
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import models, connection, OperationalError, transaction, \
 | 
			
		||||
    ProgrammingError
 | 
			
		||||
from django.db.models.functions import Now
 | 
			
		||||
 | 
			
		||||
from mia_core.utils import get_multi_lingual_attr, set_multi_lingual_attr, \
 | 
			
		||||
    new_pk
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Country(DirtyFieldsMixin, models.Model):
 | 
			
		||||
    """A country."""
 | 
			
		||||
    id = models.PositiveIntegerField(primary_key=True, db_column="sn")
 | 
			
		||||
    code = models.CharField(max_length=2, unique=True, db_column="id")
 | 
			
		||||
    name_en = models.CharField(max_length=64)
 | 
			
		||||
    name_zh_hant = models.CharField(
 | 
			
		||||
        max_length=32, null=True, db_column="name_zhtw")
 | 
			
		||||
    name_zh_hans = models.CharField(
 | 
			
		||||
        max_length=32, null=True, db_column="name_zhcn")
 | 
			
		||||
    is_special = models.BooleanField(
 | 
			
		||||
        default=False, db_column="special")
 | 
			
		||||
    created_at = models.DateTimeField(
 | 
			
		||||
        auto_now_add=True, db_column="created")
 | 
			
		||||
    created_by = models.ForeignKey(
 | 
			
		||||
        "User", on_delete=models.PROTECT,
 | 
			
		||||
        db_column="createdby", related_name="created_countries")
 | 
			
		||||
    updated_at = models.DateTimeField(
 | 
			
		||||
        auto_now=True, db_column="updated")
 | 
			
		||||
    updated_by = models.ForeignKey(
 | 
			
		||||
        "User", on_delete=models.PROTECT,
 | 
			
		||||
        db_column="updatedby", related_name="updated_countries")
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        """Returns the string representation of this country."""
 | 
			
		||||
        return self.code.__str__() + " " + self.name.__str__()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def name(self) -> str:
 | 
			
		||||
        """The country name in the current language."""
 | 
			
		||||
        return get_multi_lingual_attr(self, "name", "en")
 | 
			
		||||
 | 
			
		||||
    @name.setter
 | 
			
		||||
    def name(self, value: str) -> None:
 | 
			
		||||
        set_multi_lingual_attr(self, "name", value)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        db_table = "country"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class User(DirtyFieldsMixin, models.Model):
 | 
			
		||||
    """A user."""
 | 
			
		||||
    id = models.PositiveIntegerField(primary_key=True, db_column="sn")
 | 
			
		||||
    login_id = models.CharField(max_length=32, unique=True, db_column="id")
 | 
			
		||||
    password = models.CharField(max_length=32, db_column="passwd")
 | 
			
		||||
    name = models.CharField(max_length=32)
 | 
			
		||||
    is_disabled = models.BooleanField(
 | 
			
		||||
        default=False, db_column="disabled")
 | 
			
		||||
    is_deleted = models.BooleanField(
 | 
			
		||||
        default=False, db_column="deleted")
 | 
			
		||||
    language = models.CharField(max_length=6, null=True, db_column="lang")
 | 
			
		||||
    visits = models.PositiveSmallIntegerField(default=0)
 | 
			
		||||
    visited_at = models.DateTimeField(null=True, db_column="visited")
 | 
			
		||||
    visited_ip = models.GenericIPAddressField(null=True, db_column="ip")
 | 
			
		||||
    visited_host = models.CharField(
 | 
			
		||||
        max_length=128, null=True, db_column="host")
 | 
			
		||||
    visited_country = models.ForeignKey(
 | 
			
		||||
        Country, on_delete=models.PROTECT, null=True,
 | 
			
		||||
        db_column="ct", to_field="code", related_name="users")
 | 
			
		||||
    created_at = models.DateTimeField(
 | 
			
		||||
        auto_now_add=True, db_column="created")
 | 
			
		||||
    created_by = models.ForeignKey(
 | 
			
		||||
        "self", on_delete=models.PROTECT,
 | 
			
		||||
        db_column="createdby", related_name="created_users")
 | 
			
		||||
    updated_at = models.DateTimeField(
 | 
			
		||||
        auto_now_add=True, db_column="updated")
 | 
			
		||||
    updated_by = models.ForeignKey(
 | 
			
		||||
        "self", on_delete=models.PROTECT,
 | 
			
		||||
        db_column="updatedby", related_name="updated_users")
 | 
			
		||||
    REQUIRED_FIELDS = ["id", "name"]
 | 
			
		||||
    USERNAME_FIELD = "login_id"
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.current_user = None
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_anonymous(self) -> bool:
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_authenticated(self) -> bool:
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    def set_password(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def check_password(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        """Returns the string representation of this user."""
 | 
			
		||||
        return "%s (%s)" % (
 | 
			
		||||
            self.name.__str__(), self.login_id.__str__())
 | 
			
		||||
 | 
			
		||||
    def save(self, force_insert=False, force_update=False, using=None,
 | 
			
		||||
             update_fields=None):
 | 
			
		||||
        if self.pk is None:
 | 
			
		||||
            self.pk = new_pk(User)
 | 
			
		||||
            if self.current_user is not None:
 | 
			
		||||
                self.created_by = self.current_user
 | 
			
		||||
        if self.current_user is not None:
 | 
			
		||||
            self.updated_by = self.current_user
 | 
			
		||||
        with transaction.atomic():
 | 
			
		||||
            super(User, self).save(
 | 
			
		||||
                force_insert=force_insert, force_update=force_update,
 | 
			
		||||
                using=using, update_fields=update_fields)
 | 
			
		||||
            User.objects.filter(pk=self.pk).update(updated_at=Now())
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        db_table = "users"
 | 
			
		||||
        app_label = "mia_core"
 | 
			
		||||
 | 
			
		||||
    def set_digest_password(self, login_id, password):
 | 
			
		||||
        self.password = self.md5(
 | 
			
		||||
            F"{login_id}:{settings.DIGEST_REALM}:{password}")
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def md5(value: str) -> str:
 | 
			
		||||
        m = hashlib.md5()
 | 
			
		||||
        m.update(value.encode("utf-8"))
 | 
			
		||||
        return m.hexdigest()
 | 
			
		||||
 | 
			
		||||
    def is_in_use(self) -> bool:
 | 
			
		||||
        """Returns whether this user is in use.
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            True if this user is in use, or False otherwise.
 | 
			
		||||
        """
 | 
			
		||||
        for table in connection.introspection.table_names():
 | 
			
		||||
            if self._is_in_use_with(F"SELECT * FROM {table}"
 | 
			
		||||
                                    " WHERE createdby=%s OR updatedby=%s"):
 | 
			
		||||
                return True
 | 
			
		||||
            if self._is_in_use_with(
 | 
			
		||||
                    F"SELECT * FROM {table}"
 | 
			
		||||
                    " WHERE created_by_id=%s OR updated_by_id=%s"):
 | 
			
		||||
                return True
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def _is_in_use_with(self, sql: str) -> bool:
 | 
			
		||||
        """Returns whether this user is in use with a specific SQL statement.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            sql: The SQL query statement
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            True if this user is in use, or False otherwise.
 | 
			
		||||
        """
 | 
			
		||||
        with connection.cursor() as cursor:
 | 
			
		||||
            try:
 | 
			
		||||
                cursor.execute(sql, [self.pk, self.pk])
 | 
			
		||||
            except OperationalError:
 | 
			
		||||
                return False
 | 
			
		||||
            except ProgrammingError:
 | 
			
		||||
                return False
 | 
			
		||||
            if cursor.fetchone() is None:
 | 
			
		||||
                return False
 | 
			
		||||
            return True
 | 
			
		||||
@@ -1,264 +0,0 @@
 | 
			
		||||
/* The Mia Website
 | 
			
		||||
 * edit.js: The JavaScript to edit the user data
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/*  Copyright (c) 2019-2020 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: 2020/3/26
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
// Initializes the page JavaScript.
 | 
			
		||||
$(function () {
 | 
			
		||||
    $("#user-login-id")
 | 
			
		||||
        .on("blur", function () {
 | 
			
		||||
        validateLoginId();
 | 
			
		||||
        updatePasswordRequirement();
 | 
			
		||||
    })
 | 
			
		||||
    $("#user-password")
 | 
			
		||||
        .on("blur", function () {
 | 
			
		||||
        validatePassword();
 | 
			
		||||
    });
 | 
			
		||||
    $("#user-password2")
 | 
			
		||||
        .on("blur", function () {
 | 
			
		||||
        validatePassword2();
 | 
			
		||||
    });
 | 
			
		||||
    $("#user-name")
 | 
			
		||||
        .on("blur", function () {
 | 
			
		||||
        validateName();
 | 
			
		||||
    });
 | 
			
		||||
    $("#user-form")
 | 
			
		||||
        .on("submit", function () {
 | 
			
		||||
        return validateForm();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Updates the password required when the log in ID is changed.
 | 
			
		||||
 *
 | 
			
		||||
 * The HTTP digest authentication requires both the log in ID and the
 | 
			
		||||
 * password to compose and store the hash.  When the log in ID is
 | 
			
		||||
 * changed, we will also need the password in order to update the
 | 
			
		||||
 * hash.
 | 
			
		||||
 *
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
function updatePasswordRequirement() {
 | 
			
		||||
    const originalId = $("#user-login-id-original").val();
 | 
			
		||||
    if (originalId === "") {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    $("#user-password")[0].required = ($("#user-login-id").val() !== originalId);
 | 
			
		||||
    validatePassword();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/*******************
 | 
			
		||||
 * Form Validation *
 | 
			
		||||
 *******************/
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The validation result
 | 
			
		||||
 * @type {object}
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
let isValidated;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Validates the form.
 | 
			
		||||
 *
 | 
			
		||||
 * @returns {boolean} true if the validation succeed, or false
 | 
			
		||||
 *                    otherwise
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
function validateForm() {
 | 
			
		||||
    isValidated = {
 | 
			
		||||
        "id": null,
 | 
			
		||||
        "_sync": true,
 | 
			
		||||
    };
 | 
			
		||||
    validateLoginIdAsync().then();
 | 
			
		||||
    validateSyncColumns();
 | 
			
		||||
    return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Validates the form on synchronous validations.
 | 
			
		||||
 *
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
function validateSyncColumns() {
 | 
			
		||||
    let isSyncValidated = true;
 | 
			
		||||
    isSyncValidated = isSyncValidated && validatePassword();
 | 
			
		||||
    isSyncValidated = isSyncValidated && validatePassword2();
 | 
			
		||||
    isSyncValidated = isSyncValidated && validateName();
 | 
			
		||||
    isValidated["_sync"] = isSyncValidated;
 | 
			
		||||
    validateFormAsync();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Validates the form for the asynchronous validation.
 | 
			
		||||
 *
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
function validateFormAsync() {
 | 
			
		||||
    let isFormValidated = true;
 | 
			
		||||
    const keys = Object.keys(isValidated);
 | 
			
		||||
    for (let i = 0; i < keys.length; i++) {
 | 
			
		||||
        if (isValidated[keys[i]] === null) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        isFormValidated = isFormValidated && isValidated[keys[i]];
 | 
			
		||||
    }
 | 
			
		||||
    if (isFormValidated) {
 | 
			
		||||
        $("#user-form")[0].submit();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Validates the log in ID for asynchronous form validation.
 | 
			
		||||
 *
 | 
			
		||||
 * @returns {Promise<void>}
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
async function validateLoginIdAsync() {
 | 
			
		||||
    isValidated["id"] = await validateLoginId();
 | 
			
		||||
    validateFormAsync();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Validates the log in ID.
 | 
			
		||||
 *
 | 
			
		||||
 * @returns {boolean} true if the validation succeed, or false
 | 
			
		||||
 *                    otherwise
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
async function validateLoginId() {
 | 
			
		||||
    const id = $("#user-login-id")[0];
 | 
			
		||||
    const errorMessage = $("#user-login-id-error");
 | 
			
		||||
    id.value = id.value.trim();
 | 
			
		||||
    if (id.value === "") {
 | 
			
		||||
        id.classList.add("is-invalid");
 | 
			
		||||
        errorMessage.text(gettext("Please fill in the log in ID."));
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
    if (id.value.match(/\//)) {
 | 
			
		||||
        id.classList.add("is-invalid");
 | 
			
		||||
        errorMessage.text(gettext("You cannot use slash (/) in the log in ID."));
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
    const originalId = $("#user-login-id-original").val();
 | 
			
		||||
    if (originalId === "" || id.value !== originalId) {
 | 
			
		||||
        let exists = null;
 | 
			
		||||
        const request = new XMLHttpRequest();
 | 
			
		||||
        request.onreadystatechange = function() {
 | 
			
		||||
            if (this.readyState === 4 && this.status === 200) {
 | 
			
		||||
                exists = JSON.parse(this.responseText);
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        const url = $("#exists-url").val().replace("ID", id.value);
 | 
			
		||||
        request.open("GET", url, true);
 | 
			
		||||
        request.send();
 | 
			
		||||
        while (exists === null) {
 | 
			
		||||
            await new Promise(r => setTimeout(r, 200));
 | 
			
		||||
        }
 | 
			
		||||
        if (exists) {
 | 
			
		||||
            id.classList.add("is-invalid");
 | 
			
		||||
            errorMessage.text(gettext("This log in ID is already in use."));
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    id.classList.remove("is-invalid");
 | 
			
		||||
    errorMessage.text("");
 | 
			
		||||
    return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Validates the password.
 | 
			
		||||
 *
 | 
			
		||||
 * @returns {boolean} true if the validation succeed, or false
 | 
			
		||||
 *                    otherwise
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
async function validatePassword() {
 | 
			
		||||
    const password = $("#user-password")[0];
 | 
			
		||||
    const errorMessage = $("#user-password-error");
 | 
			
		||||
    password.value = password.value.trim();
 | 
			
		||||
    if (password.required) {
 | 
			
		||||
        if (password.value === "") {
 | 
			
		||||
            password.classList.add("is-invalid");
 | 
			
		||||
            const originalId = $("#user-login-id-original").val();
 | 
			
		||||
            if (originalId === "" || $("#user-login-id").val() !== originalId) {
 | 
			
		||||
                errorMessage.text(gettext("Please fill in the password to change the log in ID."));
 | 
			
		||||
            } else {
 | 
			
		||||
                errorMessage.text(gettext("Please fill in the password."));
 | 
			
		||||
            }
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    password.classList.remove("is-invalid");
 | 
			
		||||
    errorMessage.text("");
 | 
			
		||||
    return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Validates the password verification.
 | 
			
		||||
 *
 | 
			
		||||
 * @returns {boolean} true if the validation succeed, or false
 | 
			
		||||
 *                    otherwise
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
function validatePassword2() {
 | 
			
		||||
    const password2 = $("#user-password2")[0];
 | 
			
		||||
    const errorMessage = $("#user-password2-error");
 | 
			
		||||
    password2.value = password2.value.trim();
 | 
			
		||||
    const password = $("#user-password").val();
 | 
			
		||||
    if (password !== "") {
 | 
			
		||||
        if (password2.value === "") {
 | 
			
		||||
            password2.classList.add("is-invalid");
 | 
			
		||||
            errorMessage.text(gettext("Please enter the password again to verify it."));
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if (password2.value !== password) {
 | 
			
		||||
        password2.classList.add("is-invalid");
 | 
			
		||||
        errorMessage.text(gettext("The two passwords do not match."));
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
    password2.classList.remove("is-invalid");
 | 
			
		||||
    errorMessage.text("");
 | 
			
		||||
    return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Validates the name.
 | 
			
		||||
 *
 | 
			
		||||
 * @returns {boolean} true if the validation succeed, or false
 | 
			
		||||
 *                    otherwise
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
function validateName() {
 | 
			
		||||
    const name = $("#user-name")[0];
 | 
			
		||||
    const errorMessage = $("#user-name-error");
 | 
			
		||||
    name.value = name.value.trim();
 | 
			
		||||
    if (name.value === "") {
 | 
			
		||||
        name.classList.add("is-invalid");
 | 
			
		||||
        errorMessage.text(gettext("Please fill in the name."));
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
    name.classList.remove("is-invalid");
 | 
			
		||||
    errorMessage.text("");
 | 
			
		||||
    return true;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,183 +0,0 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
{% comment %}
 | 
			
		||||
The Mia Core Application
 | 
			
		||||
user_detail.html: The template for the user details
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2020 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: 2020/8/9
 | 
			
		||||
{% endcomment %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
{% load mia_core %}
 | 
			
		||||
{% if request|is_in_section:"mia_core:my-account" %}
 | 
			
		||||
  {% setvar "user" request.user %}
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{% block settings %}
 | 
			
		||||
  {% setvar "title" user.name %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
 | 
			
		||||
{% if request|is_in_section:"mia_core:my-account" %}
 | 
			
		||||
  <div class="btn-group btn-actions">
 | 
			
		||||
    <a class="btn btn-primary" role="button" href="{% url "mia_core:my-account.update" %}">
 | 
			
		||||
      <i class="fas fa-user-cog"></i>
 | 
			
		||||
      {{ _("Settings")|force_escape }}
 | 
			
		||||
    </a>
 | 
			
		||||
  </div>
 | 
			
		||||
{% else %}
 | 
			
		||||
  {% if not user.is_in_use %}
 | 
			
		||||
    <div class="alert alert-info alert-dismissible fade show">
 | 
			
		||||
      <button type="button" class="close" data-dismiss="alert">×</button>
 | 
			
		||||
      {{ _("The account is not in use.")|force_escape }}
 | 
			
		||||
    </div>
 | 
			
		||||
  {% endif %}
 | 
			
		||||
 | 
			
		||||
  <!-- the delete confirmation dialog -->
 | 
			
		||||
  <form action="{% url "mia_core:users.delete" user %}" method="POST">
 | 
			
		||||
    {% csrf_token %}
 | 
			
		||||
    <!-- The Modal -->
 | 
			
		||||
    <div class="modal" id="del-modal">
 | 
			
		||||
      <div class="modal-dialog">
 | 
			
		||||
        <div class="modal-content">
 | 
			
		||||
 | 
			
		||||
          <!-- Modal Header -->
 | 
			
		||||
          <div class="modal-header">
 | 
			
		||||
            <h4 class="modal-title">{{ _("User Deletion Confirmation")|force_escape }}</h4>
 | 
			
		||||
            <button type="button" class="close" data-dismiss="modal">×</button>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <!-- Modal body -->
 | 
			
		||||
          <div class="modal-body">{{ _("Do you really want to delete this user?")|force_escape }}</div>
 | 
			
		||||
 | 
			
		||||
          <!-- Modal footer -->
 | 
			
		||||
          <div class="modal-footer">
 | 
			
		||||
            <button class="btn btn-danger" type="submit" name="del-confirm">{{ _("Confirm")|force_escape }}</button>
 | 
			
		||||
            <button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Cancel")|force_escape }}</button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </form>
 | 
			
		||||
 | 
			
		||||
  <div class="btn-group btn-actions">
 | 
			
		||||
    <a class="btn btn-primary" role="button" href="{% url "mia_core:users" %}">
 | 
			
		||||
      <i class="fas fa-chevron-circle-left"></i>
 | 
			
		||||
      {{ _("Back")|force_escape }}
 | 
			
		||||
    </a>
 | 
			
		||||
    <a class="btn btn-primary" role="button" href="{% url "mia_core:users.update" user %}">
 | 
			
		||||
      <i class="fas fa-user-cog"></i>
 | 
			
		||||
      {{ _("Settings")|force_escape }}
 | 
			
		||||
    </a>
 | 
			
		||||
    {% if user.login_id == request.user.login_id %}
 | 
			
		||||
      <button class="btn btn-secondary" type="button" disabled="disabled" title="{{ _("You cannot delete your own account.")|force_escape }}">
 | 
			
		||||
        <i class="fas fa-trash"></i>
 | 
			
		||||
        {{ _("Delete")|force_escape }}
 | 
			
		||||
      </button>
 | 
			
		||||
    {% elif user.is_in_use %}
 | 
			
		||||
      <button class="btn btn-secondary" type="button" disabled="disabled" title="{{ _("You cannot delete this account because it is in use.")|force_escape }}">
 | 
			
		||||
        <i class="fas fa-trash"></i>
 | 
			
		||||
        {{ _("Delete")|force_escape }}
 | 
			
		||||
      </button>
 | 
			
		||||
    {% elif user.is_deleted %}
 | 
			
		||||
      <button class="btn btn-secondary" type="button" disabled="disabled" title="{{ _("This account is already deleted.")|force_escape }}>">
 | 
			
		||||
        <i class="fas fa-trash"></i>
 | 
			
		||||
        {{ _("Delete")|force_escape }}
 | 
			
		||||
      </button>
 | 
			
		||||
    {% else %}
 | 
			
		||||
      <button class="btn btn-danger" type="button" data-toggle="modal" data-target="#del-modal">
 | 
			
		||||
        <i class="fas fa-trash"></i>
 | 
			
		||||
        {{ _("Delete")|force_escape }}
 | 
			
		||||
      </button>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
  </div>
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
<div class="row form-group">
 | 
			
		||||
<div class="col-sm-2">{{ _("Log in ID.:")|force_escape }}</div>
 | 
			
		||||
  <div class="col-sm-10">{{ user.login_id }}</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="row form-group">
 | 
			
		||||
  <div class="col-sm-2">{{ _("Name:")|force_escape }}</div>
 | 
			
		||||
  <div class="col-sm-10">{{ user.name }}</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{% if user.is_disabled %}
 | 
			
		||||
  <div class="row form-group">
 | 
			
		||||
    <div class="col-sm-12">{{ _("This account is disabled.")|force_escape }}</div>
 | 
			
		||||
  </div>
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{% if user.is_deleted %}
 | 
			
		||||
  <div class="row form-group">
 | 
			
		||||
    <div class="col-sm-12">{{ _("This account is deleted.")|force_escape }}</div>
 | 
			
		||||
  </div>
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{% if not user.visits %}
 | 
			
		||||
  <div class="row form-group">
 | 
			
		||||
    <div class="col-sm-12">{{ _("This user has not logged in yet.")|force_escape }}</div>
 | 
			
		||||
  </div>
 | 
			
		||||
{% else %}
 | 
			
		||||
  <div class="row form-group">
 | 
			
		||||
    <div class="col-sm-2">{{ _("# of visits:")|force_escape }}</div>
 | 
			
		||||
    <div class="col-sm-10">{{ user.visits }}</div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="row form-group">
 | 
			
		||||
    <div class="col-sm-2">{{ _("Last visit at:")|force_escape }}</div>
 | 
			
		||||
    <div class="col-sm-10">{{ user.visited_at }}</div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="row form-group">
 | 
			
		||||
    <div class="col-sm-2">{{ _("IP:")|force_escape }}</div>
 | 
			
		||||
    <div class="col-sm-10">{{ user.visited_ip }}</div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="row form-group">
 | 
			
		||||
    <div class="col-sm-2">{{ _("Host:")|force_escape }}</div>
 | 
			
		||||
    <div class="col-sm-10">{{ user.visited_host }}</div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="row form-group">
 | 
			
		||||
    <div class="col-sm-2">{{ _("Country:")|force_escape }}</div>
 | 
			
		||||
    <div class="col-sm-10">{{ user.visited_country.name|default:_("(Not Available)")|force_escape }}</div>
 | 
			
		||||
  </div>
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
<div class="row form-group">
 | 
			
		||||
<div class="col-sm-2">{{ _("Created at:")|force_escape }}</div>
 | 
			
		||||
  <div class="col-sm-10">{{ user.created_at }}</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="row form-group">
 | 
			
		||||
  <div class="col-sm-2">{{ _("Created by:")|force_escape }}</div>
 | 
			
		||||
  <div class="col-sm-10">{{ user.created_by }}</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="row form-group">
 | 
			
		||||
  <div class="col-sm-2">{{ _("Updated at:")|force_escape }}</div>
 | 
			
		||||
  <div class="col-sm-10">{{ user.updated_at }}</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="row form-group">
 | 
			
		||||
  <div class="col-sm-2">{{ _("Updated by:")|force_escape }}</div>
 | 
			
		||||
  <div class="col-sm-10">{{ user.updated_by }}</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -1,105 +0,0 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
{% comment %}
 | 
			
		||||
The Mia Core Application
 | 
			
		||||
user_form.html: The template for the form of a user
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2020 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: 2020/8/9
 | 
			
		||||
{% endcomment %}
 | 
			
		||||
{% load static %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
{% load mia_core %}
 | 
			
		||||
 | 
			
		||||
{% block settings %}
 | 
			
		||||
  {% if form.user %}
 | 
			
		||||
    {% setvar "title" user.name %}
 | 
			
		||||
  {% else %}
 | 
			
		||||
    {% setvar "title" _("Add a New Account") %}
 | 
			
		||||
  {% endif %}
 | 
			
		||||
  {% static "mia_core/js/user-form.js" as file %}{% add_js file %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
 | 
			
		||||
<div class="btn-group btn-actions">
 | 
			
		||||
  <a class="btn btn-primary" role="button" href="{% if request|is_in_section:"mia_core:my-account" %}{% url "mia_core:my-account" %}{% elif form.user %}{% url "mia_core:users.detail" form.user %}{% else %}{% url "mia_core:users" %}{% endif %}">
 | 
			
		||||
    <i class="fas fa-chevron-circle-left"></i>
 | 
			
		||||
    {{ _("Back")|force_escape }}
 | 
			
		||||
  </a>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<form id="user-form" action="{{ request.get_full_path }}" method="POST">
 | 
			
		||||
  {% csrf_token %}
 | 
			
		||||
  <input id="exists-url" type="hidden" value="{% url "mia_core:api.users.exists" "ID" %}" />
 | 
			
		||||
  <input id="user-login-id-original" type="hidden" value="{{ form.user.login_id }}" />
 | 
			
		||||
  <div class="row form-group">
 | 
			
		||||
    <label class="col-sm-2 col-form-label" for="user-login-id">{{ _("Log in ID.:")|force_escape }}</label>
 | 
			
		||||
    <div class="col-sm-10">
 | 
			
		||||
      <input id="user-login-id" class="form-control {% if form.login_id.errors %} is-invalid {% endif %}" type="text" name="login_id" value="{{ form.login_id.value|default:"" }}" maxlength="32" required="required" />
 | 
			
		||||
      <div id="user-login-id-error" class="invalid-feedback">{{ form.login_id.errors.0 }}</div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="row form-group">
 | 
			
		||||
    <label class="col-sm-2 col-form-label" for="user-password">{{ _("Password:")|force_escape }}</label>
 | 
			
		||||
    <div class="col-sm-10">
 | 
			
		||||
      <input id="user-password" class="form-control {% if form.password.errors %} is-invalid {% endif %}" type="password" name="password" value="" {% if not form.user %} required="required" {% endif %} />
 | 
			
		||||
      <div id="user-password-error" class="invalid-feedback">{{ form.password.errors.0 }}</div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="row form-group">
 | 
			
		||||
    <label class="col-sm-2 col-form-label" for="user-password2">{{ _("Confirm password:")|force_escape }}</label>
 | 
			
		||||
    <div class="col-sm-10">
 | 
			
		||||
      <input id="user-password2" class="form-control {% if form.password2.errors %} is-invalid {% endif %}" type="password" name="password2" value="" {% if not form.user %} required="required" {% endif %} />
 | 
			
		||||
      <div id="user-password2-error" class="invalid-feedback">{{ form.password2.errors.0 }}</div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="row form-group">
 | 
			
		||||
    <label class="col-sm-2 col-form-label" for="user-name">{{ _("Name:")|force_escape }}</label>
 | 
			
		||||
    <div class="col-sm-10">
 | 
			
		||||
      <input id="user-name" class="form-control {% if form.name.errors %} is-invalid {% endif %}" type="text" name="name" value="{{ form.name.value|default:"" }}" maxlength="32" required="required" />
 | 
			
		||||
      <div id="user-name-error" class="invalid-feedback">{{ form.name.errors.0 }}</div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  {% if not request|is_in_section:"mia_core:my-account" %}
 | 
			
		||||
    <div class="row form-group form-check">
 | 
			
		||||
      <div class="col-sm-12">
 | 
			
		||||
        {% if form.user and form.user.pk == request.user.pk %}
 | 
			
		||||
          {{ _("You cannot disable your own account.")|force_escape }}
 | 
			
		||||
        {% else %}
 | 
			
		||||
          <input id="user-is-disabled" class="form-check-input" type="checkbox" name="is_disabled" value="1" {% if form.is_disabled.value %} checked="checked" {% endif %} />
 | 
			
		||||
          <label class="form-check-label" for="user-is-disabled">
 | 
			
		||||
            {{ _("This account is disabled.")|force_escape }}
 | 
			
		||||
          </label>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  {% endif %}
 | 
			
		||||
 | 
			
		||||
  <div class="row form-group">
 | 
			
		||||
    <div class="col-sm-12">
 | 
			
		||||
      <button class="btn btn-primary" type="submit">
 | 
			
		||||
        {{ _("Submit")|force_escape }}
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</form>
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -1,78 +0,0 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
{% comment %}
 | 
			
		||||
The Mia Core Application
 | 
			
		||||
user_list.html: The template for the user list
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2020 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: 2020/8/9
 | 
			
		||||
{% endcomment %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
{% load mia_core %}
 | 
			
		||||
 | 
			
		||||
{% block settings %}
 | 
			
		||||
  {% setvar "title" _("Account Management") %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
 | 
			
		||||
<div class="btn-group btn-actions">
 | 
			
		||||
  <a class="btn btn-primary" role="button" href="{% url "mia_core:users.create" %}">
 | 
			
		||||
    <i class="fas fa-user-plus"></i>
 | 
			
		||||
    {{ _("New")|force_escape }}
 | 
			
		||||
  </a>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{% if user_list %}
 | 
			
		||||
  <table id="users" class="table table-striped table-hover">
 | 
			
		||||
    <thead>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <th scope="col">{{ _("Log in ID.")|force_escape }}</th>
 | 
			
		||||
        <th scope="col">{{ _("Name")|force_escape }}</th>
 | 
			
		||||
        <th class="actions" scope="col">{{ _("View")|force_escape }}</th>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </thead>
 | 
			
		||||
    <tbody>
 | 
			
		||||
      {% for user in user_list %}
 | 
			
		||||
        <tr>
 | 
			
		||||
          <td>{{ user.login_id }}</td>
 | 
			
		||||
          <td>
 | 
			
		||||
            {{ user.name }}
 | 
			
		||||
            {% if user.is_disabled %}
 | 
			
		||||
              <span class="badge badge-pill badge-secondary">{{ _("Disabled")|force_escape }}</span>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            {% if user.is_deleted %}
 | 
			
		||||
              <span class="badge badge-pill badge-dark">{{ _("Deleted")|force_escape }}</span>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            {% if not user.is_in_use %}
 | 
			
		||||
              <span class="badge badge-pill badge-info">{{ _("Not In Use")|force_escape }}</span>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="actions">
 | 
			
		||||
            <a href="{% url "mia_core:users.detail" user %}" class="btn btn-info" role="button">
 | 
			
		||||
              <i class="fas fa-eye"></i>
 | 
			
		||||
              <span class="d-none d-sm-inline">{{ _("View")|force_escape }}</span>
 | 
			
		||||
            </a>
 | 
			
		||||
          </td>
 | 
			
		||||
        </tr>
 | 
			
		||||
      {% endfor %}
 | 
			
		||||
    </tbody>
 | 
			
		||||
  </table>
 | 
			
		||||
{% else %}
 | 
			
		||||
  <p>{{ _("There is currently no data.")|force_escape }}</p>
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -1,44 +0,0 @@
 | 
			
		||||
# The Mia core application of the Mia project.
 | 
			
		||||
#   by imacat <imacat@mail.imacat.idv.tw>, 2020/8/9
 | 
			
		||||
 | 
			
		||||
#  Copyright (c) 2020 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 route settings of the Mia core application.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from django.urls import path, register_converter
 | 
			
		||||
from django.views.decorators.http import require_GET
 | 
			
		||||
from django.views.generic import TemplateView
 | 
			
		||||
 | 
			
		||||
from . import views, converters
 | 
			
		||||
from .digest_auth import login_required
 | 
			
		||||
 | 
			
		||||
register_converter(converters.UserConverter, "user")
 | 
			
		||||
 | 
			
		||||
app_name = "mia_core"
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path("logout", views.logout, name="logout"),
 | 
			
		||||
    path("users", views.UserListView.as_view(), name="users"),
 | 
			
		||||
    path("users/create", views.UserFormView.as_view(), name="users.create"),
 | 
			
		||||
    path("users/<user:user>", views.UserView.as_view(), name="users.detail"),
 | 
			
		||||
    path("users/<user:user>/update", views.UserFormView.as_view(), name="users.update"),
 | 
			
		||||
    path("users/<user:user>/delete", views.user_delete, name="users.delete"),
 | 
			
		||||
    path("api/users/<str:login_id>/exists", views.api_users_exists,
 | 
			
		||||
         name="api.users.exists"),
 | 
			
		||||
    path("my-account", require_GET(login_required(TemplateView.as_view(
 | 
			
		||||
        template_name="mia_core/user_detail.html"))), name="my-account"),
 | 
			
		||||
    path("my-account/update", views.MyAccountFormView.as_view(),
 | 
			
		||||
         name="my-account.update"),
 | 
			
		||||
]
 | 
			
		||||
@@ -23,24 +23,15 @@ from typing import Dict, Type, Optional, Any
 | 
			
		||||
from dirtyfields import DirtyFieldsMixin
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.contrib import messages
 | 
			
		||||
from django.contrib.auth import logout as logout_user
 | 
			
		||||
from django.contrib.messages.views import SuccessMessageMixin
 | 
			
		||||
from django.db.models import Model
 | 
			
		||||
from django.http import HttpResponse, JsonResponse, HttpRequest, \
 | 
			
		||||
from django.http import HttpResponse, HttpRequest, \
 | 
			
		||||
    HttpResponseRedirect, Http404
 | 
			
		||||
from django.shortcuts import redirect, render
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.utils.decorators import method_decorator
 | 
			
		||||
from django.utils.translation import gettext_noop
 | 
			
		||||
from django.views.decorators.http import require_POST, require_GET
 | 
			
		||||
from django.views.generic import DeleteView as CoreDeleteView, ListView, \
 | 
			
		||||
    DetailView
 | 
			
		||||
from django.views.generic import DeleteView as CoreDeleteView
 | 
			
		||||
from django.views.generic.base import View
 | 
			
		||||
 | 
			
		||||
from . import stored_post, utils
 | 
			
		||||
from .digest_auth import login_required
 | 
			
		||||
from .forms import UserForm
 | 
			
		||||
from .models import User
 | 
			
		||||
from .utils import UrlBuilder
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -232,150 +223,3 @@ class DeleteView(SuccessMessageMixin, CoreDeleteView):
 | 
			
		||||
        return response
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@require_POST
 | 
			
		||||
def logout(request: HttpRequest) -> HttpResponseRedirect:
 | 
			
		||||
    """The view to log out a user.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        request: The request.
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        The redirect response.
 | 
			
		||||
    """
 | 
			
		||||
    logout_user(request)
 | 
			
		||||
    if "next" in request.POST:
 | 
			
		||||
        request.session["logout"] = True
 | 
			
		||||
        return redirect(request.POST["next"])
 | 
			
		||||
    return redirect("home")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@method_decorator(require_GET, name="dispatch")
 | 
			
		||||
@method_decorator(login_required, name="dispatch")
 | 
			
		||||
class UserListView(ListView):
 | 
			
		||||
    """The view to list the users."""
 | 
			
		||||
    queryset = User.objects.order_by("login_id")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@method_decorator(require_GET, name="dispatch")
 | 
			
		||||
@method_decorator(login_required, name="dispatch")
 | 
			
		||||
class UserView(DetailView):
 | 
			
		||||
    """The view of a user."""
 | 
			
		||||
    def get_object(self, queryset=None):
 | 
			
		||||
        return self.kwargs["user"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@method_decorator(login_required, name="dispatch")
 | 
			
		||||
class UserFormView(FormView):
 | 
			
		||||
    """The form to create or update a user."""
 | 
			
		||||
    model = User
 | 
			
		||||
    form_class = UserForm
 | 
			
		||||
    not_modified_message = gettext_noop("This user account was not changed.")
 | 
			
		||||
    success_message = gettext_noop("This user account was saved successfully.")
 | 
			
		||||
 | 
			
		||||
    def make_form_from_post(self, post: Dict[str, str]) -> UserForm:
 | 
			
		||||
        """Creates and returns the form from the POST data."""
 | 
			
		||||
        form = UserForm(post)
 | 
			
		||||
        form.user = self.get_object()
 | 
			
		||||
        form.current_user = self.request.user
 | 
			
		||||
        return form
 | 
			
		||||
 | 
			
		||||
    def make_form_from_model(self, obj: User) -> UserForm:
 | 
			
		||||
        """Creates and returns the form from a data model."""
 | 
			
		||||
        form = UserForm({
 | 
			
		||||
            "login_id": obj.login_id,
 | 
			
		||||
            "name": obj.name,
 | 
			
		||||
            "is_disabled": obj.is_disabled,
 | 
			
		||||
        })
 | 
			
		||||
        form.user = self.get_object()
 | 
			
		||||
        form.current_user = self.request.user
 | 
			
		||||
        return form
 | 
			
		||||
 | 
			
		||||
    def fill_model_from_form(self, obj: User, form: UserForm) -> None:
 | 
			
		||||
        """Fills in the data model from the form."""
 | 
			
		||||
        obj.login_id = form["login_id"].value()
 | 
			
		||||
        if form["password"].value() is not None:
 | 
			
		||||
            obj.set_digest_password(
 | 
			
		||||
                form["login_id"].value(), form["password"].value())
 | 
			
		||||
        obj.name = form["name"].value()
 | 
			
		||||
        obj.is_disabled = form["is_disabled"].value()
 | 
			
		||||
        obj.current_user = self.request.user
 | 
			
		||||
 | 
			
		||||
    def get_success_url(self) -> str:
 | 
			
		||||
        """Returns the URL on success."""
 | 
			
		||||
        return reverse("mia_core:users.detail", args=[self.get_object()],
 | 
			
		||||
                       current_app=self.request.resolver_match.namespace)
 | 
			
		||||
 | 
			
		||||
    def get_object(self) -> Optional[Model]:
 | 
			
		||||
        """Returns the current object, or None on a create form."""
 | 
			
		||||
        return self.kwargs.get("user")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@require_POST
 | 
			
		||||
@login_required
 | 
			
		||||
def user_delete(request: HttpRequest, user: User) -> HttpResponseRedirect:
 | 
			
		||||
    """The view to delete an user.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        request: The request.
 | 
			
		||||
        user: The user.
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        The response.
 | 
			
		||||
    """
 | 
			
		||||
    message = None
 | 
			
		||||
    if user.pk == request.user.pk:
 | 
			
		||||
        message = gettext_noop("You cannot delete your own account.")
 | 
			
		||||
    elif user.is_in_use():
 | 
			
		||||
        message = gettext_noop(
 | 
			
		||||
            "You cannot delete this account because it is in use.")
 | 
			
		||||
    elif user.is_deleted:
 | 
			
		||||
        message = gettext_noop("This account is already deleted.")
 | 
			
		||||
    if message is not None:
 | 
			
		||||
        messages.error(request, message)
 | 
			
		||||
        return redirect("mia_core:users.detail", user)
 | 
			
		||||
    user.delete()
 | 
			
		||||
    message = gettext_noop("This user account was deleted successfully.")
 | 
			
		||||
    messages.success(request, message)
 | 
			
		||||
    return redirect("mia_core:users")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@method_decorator(login_required, name="dispatch")
 | 
			
		||||
class MyAccountFormView(UserFormView):
 | 
			
		||||
    """The form to update the information of the currently logged-in user."""
 | 
			
		||||
    not_modified_message = gettext_noop("Your user account was not changed.")
 | 
			
		||||
    success_message = gettext_noop("Your user account was saved successfully.")
 | 
			
		||||
 | 
			
		||||
    def fill_model_from_form(self, obj: User, form: UserForm) -> None:
 | 
			
		||||
        """Fills in the data model from the form."""
 | 
			
		||||
        obj.login_id = form["login_id"].value()
 | 
			
		||||
        if form["password"].value() is not None:
 | 
			
		||||
            obj.set_digest_password(
 | 
			
		||||
                form["login_id"].value(), form["password"].value())
 | 
			
		||||
        obj.name = form["name"].value()
 | 
			
		||||
        obj.current_user = self.request.user
 | 
			
		||||
 | 
			
		||||
    def get_success_url(self) -> str:
 | 
			
		||||
        """Returns the URL on success."""
 | 
			
		||||
        return reverse("mia_core:my-account",
 | 
			
		||||
                       current_app=self.request.resolver_match.namespace)
 | 
			
		||||
 | 
			
		||||
    def get_object(self) -> Optional[Model]:
 | 
			
		||||
        """Finds and returns the current object, or None on a create form."""
 | 
			
		||||
        return self.request.user
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def api_users_exists(request: HttpRequest, login_id: str) -> JsonResponse:
 | 
			
		||||
    """The view to check whether a user with a log in ID exists.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        request: The request.
 | 
			
		||||
        login_id: The log in ID.
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        The response.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        User.objects.get(login_id=login_id)
 | 
			
		||||
    except User.DoesNotExist:
 | 
			
		||||
        return JsonResponse(False, safe=False)
 | 
			
		||||
    return JsonResponse(True, safe=False)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user