Moved the local part of the application from the Mia core application to the Mia Womb local application.

This commit is contained in:
依瑪貓 2020-08-18 00:37:04 +08:00
parent edeaaef00c
commit d4961f9e25
15 changed files with 26 additions and 1737 deletions

View File

@ -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):

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 "你的帳號已儲存。"

View File

@ -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 "請填寫姓名。"

View File

@ -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

View File

@ -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;
}

View File

@ -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">&times;</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">&times;</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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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"),
]

View File

@ -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)