Moved the local part of the application from the Mia core application to the Mia Womb local application.
This commit is contained in:
parent
edeaaef00c
commit
d4961f9e25
@ -26,7 +26,7 @@ from django.db import transaction
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from accounting.utils import Populator
|
from accounting.utils import Populator
|
||||||
from mia_core.models import User
|
from mia_womb.models import User
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
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.decorators.http import require_GET
|
||||||
from django.views.generic import RedirectView
|
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
|
from . import converters, views
|
||||||
|
|
||||||
register_converter(converters.PeriodConverter, "period")
|
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.decorators.http import require_GET, require_POST
|
||||||
from django.views.generic import RedirectView, ListView, DetailView
|
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.period import Period
|
||||||
from mia_core.utils import Pagination, get_multi_lingual_search, \
|
from mia_core.utils import Pagination, get_multi_lingual_search, \
|
||||||
PaginationException
|
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
|
# Copyright (C) 2020 imacat
|
||||||
# This file is distributed under the same license as the Mia package.
|
# This file is distributed under the same license as the Mia package.
|
||||||
# imacat <imacat@mail.imacat.idv.tw>, 2020.
|
# imacat <imacat@mail.imacat.idv.tw>, 2020.
|
||||||
@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: mia-core 3.0\n"
|
"Project-Id-Version: mia-core 3.0\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2020-08-11 21:41+0800\n"
|
"POT-Creation-Date: 2020-08-17 23:56+0800\n"
|
||||||
"PO-Revision-Date: 2020-08-11 21:44+0800\n"
|
"PO-Revision-Date: 2020-08-18 00:02+0800\n"
|
||||||
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
|
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
|
||||||
"Language-Team: Traditional Chinese <imacat@mail.imacat.idv.tw>\n"
|
"Language-Team: Traditional Chinese <imacat@mail.imacat.idv.tw>\n"
|
||||||
"Language: Traditional Chinese\n"
|
"Language: Traditional Chinese\n"
|
||||||
@ -16,101 +16,57 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
#: mia_core/forms.py:34
|
#: mia_core/period.py:452 mia_core/period.py:487 mia_core/period.py:505
|
||||||
msgid "Please fill in the log in ID."
|
#: mia_core/period.py:518 mia_core/period.py:564
|
||||||
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
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "In %s"
|
msgid "In %s"
|
||||||
msgstr "%s"
|
msgstr "%s"
|
||||||
|
|
||||||
#: mia_core/period.py:457
|
#: mia_core/period.py:462
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Since %s"
|
msgid "Since %s"
|
||||||
msgstr "%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
|
#, python-format
|
||||||
msgid "Until %s"
|
msgid "Until %s"
|
||||||
msgstr "至%s前"
|
msgstr "至%s前"
|
||||||
|
|
||||||
#: mia_core/period.py:499
|
#: mia_core/period.py:504
|
||||||
msgid "All Time"
|
msgid "All Time"
|
||||||
msgstr "全部"
|
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/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"
|
msgid "This Month"
|
||||||
msgstr "這個月"
|
msgstr "這個月"
|
||||||
|
|
||||||
#: mia_core/period.py:624
|
#: mia_core/period.py:629
|
||||||
#: mia_core/templates/mia_core/include/period-chooser.html:63
|
#: 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"
|
msgid "Last Month"
|
||||||
msgstr "上個月"
|
msgstr "上個月"
|
||||||
|
|
||||||
#: mia_core/period.py:639
|
#: mia_core/period.py:644
|
||||||
#: mia_core/templates/mia_core/include/period-chooser.html:76
|
#: mia_core/templates/mia_core/include/period-chooser.html:76
|
||||||
msgid "This Year"
|
msgid "This Year"
|
||||||
msgstr "今年"
|
msgstr "今年"
|
||||||
|
|
||||||
#: mia_core/period.py:641
|
#: mia_core/period.py:646
|
||||||
#: mia_core/templates/mia_core/include/period-chooser.html:79
|
#: mia_core/templates/mia_core/include/period-chooser.html:79
|
||||||
msgid "Last Year"
|
msgid "Last Year"
|
||||||
msgstr "去年"
|
msgstr "去年"
|
||||||
|
|
||||||
#: mia_core/period.py:656
|
#: mia_core/period.py:661
|
||||||
#: mia_core/templates/mia_core/include/period-chooser.html:95
|
#: 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"
|
msgid "Today"
|
||||||
msgstr "今天"
|
msgstr "今天"
|
||||||
|
|
||||||
#: mia_core/period.py:658
|
#: mia_core/period.py:663
|
||||||
#: mia_core/templates/mia_core/include/period-chooser.html:98
|
#: 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"
|
msgid "Yesterday"
|
||||||
msgstr "昨天"
|
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:107
|
||||||
#: mia_core/templates/mia_core/include/period-chooser.html:125
|
#: mia_core/templates/mia_core/include/period-chooser.html:125
|
||||||
#: mia_core/templates/mia_core/user_detail.html:69
|
|
||||||
msgid "Confirm"
|
msgid "Confirm"
|
||||||
msgstr "確定"
|
msgstr "確定"
|
||||||
|
|
||||||
@ -160,198 +115,17 @@ msgstr "從:"
|
|||||||
msgid "To:"
|
msgid "To:"
|
||||||
msgstr "到:"
|
msgstr "到:"
|
||||||
|
|
||||||
#: mia_core/templates/mia_core/user_detail.html:39
|
#: mia_core/utils.py:347
|
||||||
#: 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
|
|
||||||
msgctxt "Pagination|"
|
msgctxt "Pagination|"
|
||||||
msgid "Previous"
|
msgid "Previous"
|
||||||
msgstr "上一頁"
|
msgstr "上一頁"
|
||||||
|
|
||||||
#: mia_core/utils.py:371 mia_core/utils.py:392
|
#: mia_core/utils.py:375 mia_core/utils.py:396
|
||||||
msgctxt "Pagination|"
|
msgctxt "Pagination|"
|
||||||
msgid "..."
|
msgid "..."
|
||||||
msgstr "…"
|
msgstr "…"
|
||||||
|
|
||||||
#: mia_core/utils.py:411
|
#: mia_core/utils.py:415
|
||||||
msgctxt "Pagination|"
|
msgctxt "Pagination|"
|
||||||
msgid "Next"
|
msgid "Next"
|
||||||
msgstr "下一頁"
|
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 dirtyfields import DirtyFieldsMixin
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
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.db.models import Model
|
from django.db.models import Model
|
||||||
from django.http import HttpResponse, JsonResponse, HttpRequest, \
|
from django.http import HttpResponse, HttpRequest, \
|
||||||
HttpResponseRedirect, Http404
|
HttpResponseRedirect, Http404
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.urls import reverse
|
from django.views.generic import DeleteView as CoreDeleteView
|
||||||
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.base import View
|
from django.views.generic.base import View
|
||||||
|
|
||||||
from . import stored_post, utils
|
from . import stored_post, utils
|
||||||
from .digest_auth import login_required
|
|
||||||
from .forms import UserForm
|
|
||||||
from .models import User
|
|
||||||
from .utils import UrlBuilder
|
from .utils import UrlBuilder
|
||||||
|
|
||||||
|
|
||||||
@ -232,150 +223,3 @@ class DeleteView(SuccessMessageMixin, CoreDeleteView):
|
|||||||
return response
|
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)
|
|
||||||
|
Loading…
Reference in New Issue
Block a user