Added the user form, and revised the text in the user list and user detail in the Mia core application.

This commit is contained in:
依瑪貓 2020-08-09 22:08:15 +08:00
parent 7596935ca2
commit d7ddee340b
9 changed files with 674 additions and 18 deletions

161
mia_core/forms.py Normal file
View File

@ -0,0 +1,161 @@
# 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()
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

View File

@ -7,8 +7,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: mia 1.0\n" "Project-Id-Version: mia 1.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-08-09 20:48+0800\n" "POT-Creation-Date: 2020-08-09 22:03+0800\n"
"PO-Revision-Date: 2020-08-09 20:48+0800\n" "PO-Revision-Date: 2020-08-09 22:05+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,6 +16,46 @@ 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
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: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:513 mia_core/period.py:559
#, python-format #, python-format
@ -133,6 +173,7 @@ msgid "Cancel"
msgstr "取消" msgstr "取消"
#: mia_core/templates/mia_core/user_detail.html:69 #: mia_core/templates/mia_core/user_detail.html:69
#: mia_core/templates/mia_core/user_form.html:41
msgid "Back" msgid "Back"
msgstr "回上頁" msgstr "回上頁"
@ -161,13 +202,15 @@ msgstr "帳號已刪除。"
#: mia_core/templates/mia_core/user_detail.html:99 #: mia_core/templates/mia_core/user_detail.html:99
msgid "Log in ID.:" msgid "Log in ID.:"
msgstr "帳號" msgstr "登入帳號"
#: mia_core/templates/mia_core/user_detail.html:104 #: mia_core/templates/mia_core/user_detail.html:104
#: mia_core/templates/mia_core/user_form.html:74
msgid "Name:" msgid "Name:"
msgstr "姓名" msgstr "姓名"
#: mia_core/templates/mia_core/user_detail.html:110 #: mia_core/templates/mia_core/user_detail.html:110
#: mia_core/templates/mia_core/user_form.html:88
msgid "This account is disabled." msgid "This account is disabled."
msgstr "帳號停用。" msgstr "帳號停用。"
@ -219,6 +262,26 @@ msgstr "更新時間:"
msgid "Updated by:" msgid "Updated by:"
msgstr "更新人:" 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 #: mia_core/templates/mia_core/user_list.html:27
msgid "Account Management" msgid "Account Management"
msgstr "帳號管理" msgstr "帳號管理"
@ -229,7 +292,7 @@ msgstr "新增"
#: mia_core/templates/mia_core/user_list.html:43 #: mia_core/templates/mia_core/user_list.html:43
msgid "Log in ID" msgid "Log in ID"
msgstr "帳號" msgstr "登入帳號"
#: mia_core/templates/mia_core/user_list.html:44 #: mia_core/templates/mia_core/user_list.html:44
msgid "Name" msgid "Name"

View File

@ -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 <imacat@mail.imacat.idv.tw>, 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 <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: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 "請填寫姓名。"

View File

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

@ -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 %}
<div class="btn-group btn-actions">
<a class="btn btn-primary" role="button" href="{% if form.user %}{% url "mia_core:users.detail" 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="{% if form.user %}{% url "mia_core:users.update" user %}{% else %}{% url "mia_core:users.store" %}{% endif %}" 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>
<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>
<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

@ -27,19 +27,17 @@ register_converter(converters.UserConverter, "user")
app_name = "mia_core" app_name = "mia_core"
urlpatterns = [ urlpatterns = [
path("users", views.UserListView.as_view(), name="users"), path("users", views.UserListView.as_view(), name="users"),
# TODO: To be done. path("users/create", views.user_form, name="users.create"),
path("users/create", views.todo, name="users.create"),
# TODO: To be done. # TODO: To be done.
path("users/store", views.todo, name="users.store"), path("users/store", views.todo, name="users.store"),
path("users/<user:user>", views.UserView.as_view(), name="users.detail"), path("users/<user:user>", views.UserView.as_view(), name="users.detail"),
# TODO: To be done. path("users/<user:user>/edit", views.user_form, name="users.edit"),
path("users/<user:user>/edit", views.todo, name="users.edit"),
# TODO: To be done. # TODO: To be done.
path("users/<user:user>/update", views.todo, name="users.update"), path("users/<user:user>/update", views.todo, name="users.update"),
# TODO: To be done. # TODO: To be done.
path("users/<user:user>/delete", views.todo, name="users.delete"), path("users/<user:user>/delete", views.todo, name="users.delete"),
# TODO: To be done. # TODO: To be done.
path("api/users/<str:login_id>/exists", views.todo, path("api/users/<str:login_id>/exists", views.api_users_exists,
name="api.users.exists"), name="api.users.exists"),
# TODO: To be done. # TODO: To be done.
path("my-account", views.todo, name="my-account"), path("my-account", views.todo, name="my-account"),

View File

@ -21,13 +21,16 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import logout as logout_user from django.contrib.auth import logout as logout_user
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpResponse from django.http import HttpResponse, JsonResponse
from django.shortcuts import redirect from django.shortcuts import redirect, render
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST, require_GET
from django.views.generic import DeleteView as CoreDeleteView, ListView, \ from django.views.generic import DeleteView as CoreDeleteView, ListView, \
DetailView 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): class DeleteView(SuccessMessageMixin, CoreDeleteView):
@ -67,6 +70,54 @@ class UserView(DetailView):
return self.request.resolver_match.kwargs["user"] 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. # TODO: To be removed.
def todo(request, **kwargs): def todo(request, **kwargs):
"""A dummy placeholder view for the URL settings that are not """A dummy placeholder view for the URL settings that are not