diff --git a/mia_core/forms.py b/mia_core/forms.py new file mode 100644 index 0000000..ffbaaf9 --- /dev/null +++ b/mia_core/forms.py @@ -0,0 +1,161 @@ +# The core application of the Mia project. +# by imacat , 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() + + def __init__(self, *args, **kwargs): + super(UserForm, self).__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_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): + """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_required(self): + """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_password2_required(self): + """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): + """Validates whether the two passwords are equa. + + 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): + """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 diff --git a/mia_core/locale/zh_Hant/LC_MESSAGES/django.po b/mia_core/locale/zh_Hant/LC_MESSAGES/django.po index 660e8e6..8b566fb 100644 --- a/mia_core/locale/zh_Hant/LC_MESSAGES/django.po +++ b/mia_core/locale/zh_Hant/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: mia 1.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-08-09 20:48+0800\n" -"PO-Revision-Date: 2020-08-09 20:48+0800\n" +"POT-Creation-Date: 2020-08-09 22:03+0800\n" +"PO-Revision-Date: 2020-08-09 22:05+0800\n" "Last-Translator: imacat \n" "Language-Team: Traditional Chinese \n" "Language: Traditional Chinese\n" @@ -16,6 +16,46 @@ 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:90 +msgid "This log in ID is already in use." +msgstr "登入帳號和其他人重複。" + +#: mia_core/forms.py:105 +msgid "Please fill in the password." +msgstr "請填寫密碼。" + +#: mia_core/forms.py:121 +msgid "Please enter the password again to verify it." +msgstr "請再次確認密碼。" + +#: mia_core/forms.py:138 +msgid "The two passwords do not match." +msgstr "兩次密碼不符,請重新輸入。" + +#: mia_core/forms.py:158 mia_core/templates/mia_core/user_form.html:84 +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 @@ -133,6 +173,7 @@ msgid "Cancel" msgstr "取消" #: mia_core/templates/mia_core/user_detail.html:69 +#: mia_core/templates/mia_core/user_form.html:41 msgid "Back" msgstr "回上頁" @@ -160,14 +201,16 @@ msgid "This account is already deleted." msgstr "帳號已刪除。" #: mia_core/templates/mia_core/user_detail.html:99 -msgid "Login ID.:" -msgstr "帳號" +msgid "Log in ID.:" +msgstr "登入帳號:" #: mia_core/templates/mia_core/user_detail.html:104 +#: mia_core/templates/mia_core/user_form.html:74 msgid "Name:" -msgstr "姓名" +msgstr "姓名:" #: mia_core/templates/mia_core/user_detail.html:110 +#: mia_core/templates/mia_core/user_form.html:88 msgid "This account is disabled." msgstr "帳號停用。" @@ -219,6 +262,26 @@ msgstr "更新時間:" 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:50 +msgid "Log in ID:" +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:97 +msgid "Submit" +msgstr "傳送" + #: mia_core/templates/mia_core/user_list.html:27 msgid "Account Management" msgstr "帳號管理" @@ -228,8 +291,8 @@ msgid "New" msgstr "新增" #: mia_core/templates/mia_core/user_list.html:43 -msgid "Login ID" -msgstr "帳號" +msgid "Log in ID" +msgstr "登入帳號" #: mia_core/templates/mia_core/user_list.html:44 msgid "Name" diff --git a/mia_core/locale/zh_Hant/LC_MESSAGES/djangojs.po b/mia_core/locale/zh_Hant/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..2c9a93a --- /dev/null +++ b/mia_core/locale/zh_Hant/LC_MESSAGES/djangojs.po @@ -0,0 +1,46 @@ +# 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 , 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: core 1.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-08-09 21:54+0800\n" +"PO-Revision-Date: 2020-08-09 22:05+0800\n" +"Last-Translator: imacat \n" +"Language-Team: Traditional Chinese \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:129 +msgid "Please fill in the log in ID." +msgstr "請填寫登入帳號。" + +#: mia_core/static/mia_core/js/user-form.js:134 +msgid "You cannot use slash (/) in the log in ID." +msgstr "登入帳號不可以包含斜線 (/) 。" + +#: mia_core/static/mia_core/js/user-form.js:154 +msgid "This log in ID is already in use." +msgstr "登入帳號和其他人重複。" + +#: mia_core/static/mia_core/js/user-form.js:177 +msgid "Please fill in the password." +msgstr "請填寫密碼。" + +#: mia_core/static/mia_core/js/user-form.js:201 +msgid "Please enter the password again to verify it." +msgstr "請再次確認密碼。" + +#: mia_core/static/mia_core/js/user-form.js:207 +msgid "The two passwords do not match." +msgstr "兩次密碼不符,請重新輸入。" + +#: mia_core/static/mia_core/js/user-form.js:228 +msgid "Please fill in the name." +msgstr "請填寫姓名。" diff --git a/mia_core/static/mia_core/js/user-form.js b/mia_core/static/mia_core/js/user-form.js new file mode 100644 index 0000000..e8272c3 --- /dev/null +++ b/mia_core/static/mia_core/js/user-form.js @@ -0,0 +1,234 @@ +/* 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(); + }); + $("#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(); + }); +}); + + +/******************* + * 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} + * @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"); + 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; +} diff --git a/mia_core/templates/mia_core/user_detail.html b/mia_core/templates/mia_core/user_detail.html index ff173eb..cb9e644 100644 --- a/mia_core/templates/mia_core/user_detail.html +++ b/mia_core/templates/mia_core/user_detail.html @@ -96,7 +96,7 @@ First written: 2020/8/9
-
{{ _("Login ID.:")|force_escape }}
+
{{ _("Log in ID.:")|force_escape }}
{{ user.login_id }}
diff --git a/mia_core/templates/mia_core/user_form.html b/mia_core/templates/mia_core/user_form.html new file mode 100644 index 0000000..7ea9514 --- /dev/null +++ b/mia_core/templates/mia_core/user_form.html @@ -0,0 +1,103 @@ +{% 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 %} + + + +
+ {% csrf_token %} + + +
+ +
+ +
{{ form.login_id.errors.0 }}
+
+
+ +
+ +
+ +
{{ form.password.errors.0 }}
+
+
+ +
+ +
+ +
{{ form.password2.errors.0 }}
+
+
+ +
+ +
+ +
{{ form.name.errors.0 }}
+
+
+ +
+
+ {% if form.user and form.user.pk == request.user.pk %} + {{ _("You cannot disable your own account.")|force_escape }} + {% else %} + + + {% endif %} +
+
+ +
+
+ +
+
+
+ +{% endblock %} diff --git a/mia_core/templates/mia_core/user_list.html b/mia_core/templates/mia_core/user_list.html index 573136d..1863790 100644 --- a/mia_core/templates/mia_core/user_list.html +++ b/mia_core/templates/mia_core/user_list.html @@ -40,7 +40,7 @@ First written: 2020/8/9 - + diff --git a/mia_core/urls.py b/mia_core/urls.py index 5c80e7b..bdd1773 100644 --- a/mia_core/urls.py +++ b/mia_core/urls.py @@ -27,19 +27,17 @@ register_converter(converters.UserConverter, "user") app_name = "mia_core" urlpatterns = [ path("users", views.UserListView.as_view(), name="users"), - # TODO: To be done. - path("users/create", views.todo, name="users.create"), + path("users/create", views.user_form, name="users.create"), # TODO: To be done. path("users/store", views.todo, name="users.store"), path("users/", views.UserView.as_view(), name="users.detail"), - # TODO: To be done. - path("users//edit", views.todo, name="users.edit"), + path("users//edit", views.user_form, name="users.edit"), # TODO: To be done. path("users//update", views.todo, name="users.update"), # TODO: To be done. path("users//delete", views.todo, name="users.delete"), # TODO: To be done. - path("api/users//exists", views.todo, + path("api/users//exists", views.api_users_exists, name="api.users.exists"), # TODO: To be done. path("my-account", views.todo, name="my-account"), diff --git a/mia_core/views.py b/mia_core/views.py index 0f2074a..8556aba 100644 --- a/mia_core/views.py +++ b/mia_core/views.py @@ -21,13 +21,16 @@ from django.contrib import messages from django.contrib.auth import logout as logout_user from django.contrib.messages.views import SuccessMessageMixin -from django.http import HttpResponse -from django.shortcuts import redirect -from django.views.decorators.http import require_POST +from django.http import HttpResponse, JsonResponse +from django.shortcuts import redirect, render +from django.views.decorators.http import require_POST, require_GET from django.views.generic import DeleteView as CoreDeleteView, ListView, \ DetailView -from mia_core.models import User +from . import stored_post +from .digest_auth import login_required +from .forms import UserForm +from .models import User class DeleteView(SuccessMessageMixin, CoreDeleteView): @@ -67,6 +70,54 @@ class UserView(DetailView): return self.request.resolver_match.kwargs["user"] +@require_GET +@login_required +def user_form(request, user=None): + """The view to edit an accounting transaction. + + Args: + request (HttpRequest): The request. + user (User): The account. + + Returns: + HttpResponse: The response. + """ + previous_post = stored_post.get_previous_post(request) + if previous_post is not None: + form = UserForm(previous_post) + elif user is not None: + form = UserForm({ + "login_id": user.login_id, + "name": user.name, + "is_disabled": user.is_disabled, + }) + else: + form = UserForm() + form.user = user + form.current_user = request.user + return render(request, "mia_core/user_form.html", { + "form": form, + }) + + +def api_users_exists(request, login_id): + """The view to check whether a user with a log in ID exists. + + Args: + request (HttpRequest): The request. + login_id (str): The log in ID. + + Returns: + JsonResponse: The response. + """ + try: + User.objects.get(login_id=login_id) + except User.DoesNotExist: + return JsonResponse(False, safe=False) + return JsonResponse(True, safe=False) + + + # TODO: To be removed. def todo(request, **kwargs): """A dummy placeholder view for the URL settings that are not
{{ _("Login ID")|force_escape }}{{ _("Log in ID")|force_escape }} {{ _("Name")|force_escape }} {{ _("View")|force_escape }}