Renamed the "testsite" application to "test_site".

This commit is contained in:
2023-02-02 00:39:03 +08:00
parent 6876fdf75e
commit 4aed2f6ba7
11 changed files with 14 additions and 12 deletions

122
tests/test_site/__init__.py Normal file
View File

@ -0,0 +1,122 @@
# The Mia! Accounting Flask Demonstration Website.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/27
# Copyright (c) 2023 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 Mia! Accounting Flask demonstration website.
"""
import os
import typing as t
from secrets import token_urlsafe
import click
from flask import Flask, Blueprint, render_template
from flask.cli import with_appcontext
from flask_babel_js import BabelJS
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import CSRFProtect
from sqlalchemy import Column
import accounting.utils.user
bp: Blueprint = Blueprint("home", __name__)
babel_js: BabelJS = BabelJS()
csrf: CSRFProtect = CSRFProtect()
db: SQLAlchemy = SQLAlchemy()
def create_app(is_testing: bool = False) -> Flask:
"""Create and configure the application.
:param is_testing: True if we are running for testing, or False otherwise.
:return: The application.
"""
import accounting
app: Flask = Flask(__name__)
db_uri: str = "sqlite:///" if is_testing else "sqlite:///local.sqlite"
app.config.from_mapping({
"SECRET_KEY": os.environ.get("SECRET_KEY", token_urlsafe(32)),
"SQLALCHEMY_DATABASE_URI": db_uri,
"BABEL_DEFAULT_LOCALE": "en",
"ALL_LINGUAS": "zh_Hant|正體中文,en|English,zh_Hans|简体中文",
})
if is_testing:
app.config["TESTING"] = True
babel_js.init_app(app)
csrf.init_app(app)
db.init_app(app)
app.register_blueprint(bp, url_prefix="/")
app.cli.add_command(init_db_command)
from . import locale
locale.init_app(app)
from . import auth
auth.init_app(app)
class UserUtils(accounting.utils.user.AbstractUserUtils[auth.User]):
@property
def cls(self) -> t.Type[auth.User]:
return auth.User
@property
def pk_column(self) -> Column:
return auth.User.id
@property
def current_user(self) -> auth.User:
return auth.current_user()
def get_by_username(self, username: str) -> auth.User | None:
return auth.User.query\
.filter(auth.User.username == username).first()
def get_pk(self, user: auth.User) -> int:
return user.id
can_view: t.Callable[[], bool] = lambda: auth.current_user() is not None \
and auth.current_user().username in ["viewer", "editor"]
can_edit: t.Callable[[], bool] = lambda: auth.current_user() is not None \
and auth.current_user().username == "editor"
accounting.init_app(app, user_utils=UserUtils(),
can_view_func=can_view, can_edit_func=can_edit)
return app
@click.command("init-db")
@with_appcontext
def init_db_command() -> None:
"""Initializes the database."""
db.create_all()
from .auth import User
for username in ["viewer", "editor", "nobody"]:
if User.query.filter(User.username == username).first() is None:
db.session.add(User(username=username))
db.session.commit()
click.echo("Database initialized successfully.")
@bp.get("/", endpoint="home")
def get_home() -> str:
"""Returns the home page.
:return: The home page.
"""
return render_template("home.html")

99
tests/test_site/auth.py Normal file
View File

@ -0,0 +1,99 @@
# The Mia! Accounting Flask Demonstration Website.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/27
# Copyright (c) 2023 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 authentication for the Mia! Accounting Flask demonstration website.
"""
from flask import Blueprint, render_template, Flask, redirect, url_for, \
session, request, g
from . import db
bp: Blueprint = Blueprint("auth", __name__, url_prefix="/")
class User(db.Model):
"""A user."""
__tablename__ = "users"
"""The table name."""
id = db.Column(db.Integer, nullable=False, primary_key=True,
autoincrement=True)
"""The ID"""
username = db.Column(db.String, nullable=False, unique=True)
"""The username."""
def __str__(self) -> str:
"""Returns the string representation of the user.
:return: The string representation of the user.
"""
return self.username
@bp.get("login", endpoint="login-form")
def show_login_form() -> str:
"""Shows the login form.
:return: The login form.
"""
return render_template("login.html")
@bp.post("login", endpoint="login")
def login() -> redirect:
"""Logs in the user.
:return: The redirection to the home page.
"""
if request.form.get("username") not in ["viewer", "editor", "nobody"]:
return redirect(url_for("auth.login"))
session["user"] = request.form.get("username")
return redirect(url_for("home.home"))
@bp.post("logout", endpoint="logout")
def logout() -> redirect:
"""Logs out the user.
:return: The redirection to the home page.
"""
if "user" in session:
del session["user"]
return redirect(url_for("home.home"))
def current_user() -> User | None:
"""Returns the current user.
:return: The current user, or None if the user did not log in.
"""
if not hasattr(g, "user"):
if "user" not in session:
g.user = None
else:
g.user = User.query.filter(
User.username == session["user"]).first()
return g.user
def init_app(app: Flask) -> None:
"""Initialize the localization.
:param app: The Flask application.
:return: None.
"""
app.register_blueprint(bp)
app.jinja_env.globals["current_user"] = current_user

97
tests/test_site/locale.py Normal file
View File

@ -0,0 +1,97 @@
# The Mia! Accounting Flask Demonstration Website.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/2
# Copyright (c) 2023 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 localization for the Mia! Accounting Flask demonstration website.
"""
from babel import Locale
from flask import request, session, current_app, Blueprint, Response, \
redirect, url_for, Flask
from flask_babel import Babel
from werkzeug.datastructures import LanguageAccept
bp: Blueprint = Blueprint("locale", __name__, url_prefix="/")
def get_locale():
"""Returns the locale of the user
:return: The locale of the user.
"""
all_linguas: dict[str, str] = get_all_linguas()
if "locale" in session and session["locale"] in all_linguas:
return session["locale"]
return __fix_accept_language(request.accept_languages)\
.best_match(all_linguas.keys())
def __fix_accept_language(accept: LanguageAccept) -> LanguageAccept:
"""Fixes the accept-language so that territory variants may be matched to
script variants. For example, zh_TW, zh_HK to zh_Hant, and zh_CN, zh_SG to
zh_Hans. This is to solve the issue that Flask only recognizes the script
variants, like zh_Hant and zh_Hans.
:param accept: The original HTTP accept languages.
:return: The fixed HTTP accept languages
"""
accept_list: list[tuple[str, float]] = list(accept)
to_add: list[tuple[str, float]] = []
for pair in accept_list:
locale: Locale = Locale.parse(pair[0].replace("-", "_"))
if locale.script is not None:
tag: str = f"{locale.language}-{locale.script}"
if tag not in accept:
to_add.append((tag, pair[1]))
accept_list.extend(to_add)
return LanguageAccept(accept_list)
@bp.post("/locale", endpoint="set-locale")
def set_locale() -> Response:
"""Sets the locale for the user.
:return: The response.
"""
all_linguas: dict[str, str] = get_all_linguas()
if "locale" in request.form and request.form["locale"] in all_linguas:
session["locale"] = request.form["locale"]
if "next" in request.form:
return redirect(request.form["next"])
return redirect(url_for("home.home"))
def get_all_linguas() -> dict[str, str]:
"""Returns all the available languages.
:return: All the available languages, as a dictionary of the language code
and their local names.
"""
return {y[0]: y[1] for y in
[x.split("|") for x in
current_app.config["ALL_LINGUAS"].split(",")]}
def init_app(app: Flask) -> None:
"""Initialize the localization.
:param app: The Flask application.
:return: None.
"""
babel = Babel()
babel.init_app(app, locale_selector=get_locale)
app.register_blueprint(bp)
app.jinja_env.globals["get_locale"] = get_locale
app.jinja_env.globals["get_all_linguas"] = get_all_linguas

View File

@ -0,0 +1,134 @@
{#
The Mia! Accounting Flask Demonstration Website
base.html: The side-wide layout template
Copyright (c) 2023 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: 2023/1/27
#}
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="{{ _("en") }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="{{ "imacat" }}" />
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.2.1/css/all.min.css">
{% block styles %}{% endblock %}
<script src="{{ url_for("babel_catalog") }}"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/decimal.js/9.0.0/decimal.min.js"></script>
{% block scripts %}{% endblock %}
<title>{% block title %}{% endblock %}</title>
</head>
<body>
<nav class="navbar navbar-expand-lg bg-body-tertiary bg-dark navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for("home.home") }}">
<i class="fa-solid fa-house"></i>
{{ _("Home") }}
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#collapsible-navbar" aria-controls="collapsible-navbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div id="collapsible-navbar" class="collapse navbar-collapse">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
{% include "/accounting/include/nav.html" %}
</ul>
<!-- The right side -->
<ul class="navbar-nav d-flex">
{% if current_user() is not none %}
<li class="nav-item dropdown">
<span class="nav-link dropdown-toggle" data-bs-toggle="dropdown">
<i class="fa-solid fa-user"></i>
{{ current_user().username }}
</span>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<form action="{{ url_for("auth.logout") }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn dropdown-item" type="submit">
<i class="fa-solid fa-right-from-bracket"></i>
{{ _("Log Out") }}
</button>
</form>
</li>
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for("auth.login") }}">
<i class="fa-solid fa-right-to-bracket"></i>
{{ _("Log In") }}
</a>
</li>
{% endif %}
<li class="nav-item dropdown">
<span class="nav-link dropdown-toggle" data-bs-toggle="dropdown">
<i class="fa-solid fa-language"></i>
</span>
<form action="{{ url_for("locale.set-locale") }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="next" value="{{ request.full_path if request.query_string else request.path }}">
<ul class="dropdown-menu dropdown-menu-end">
{% for locale_code, locale_name in get_all_linguas().items() %}
<li>
<button class="dropdown-item {% if locale_code == get_locale() %} active {% endif %}" type="submit" name="locale" value="{{ locale_code }}">
{{ locale_name }}
</button>
</li>
{% endfor %}
</ul>
</form>
</li>
</ul>
</div>
</div>
</nav>
<div class="container">
<h1>{% block header %}{% endblock %}</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
{% if category == "success" %}
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% elif category == "error" %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<strong>{{ _("Error:") }}</strong> {{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{% endfor %}
{% endif %}
{% endwith %}
<main class="pb-5">
{% block content %}{% endblock %}
</main>
</div>
</body>
</html>

View File

@ -0,0 +1,24 @@
{#
The Mia! Accounting Flask Demonstration Website
home.html: The home page.
Copyright (c) 2023 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: 2023/1/27
#}
{% extends "base.html" %}
{% block header %}{% block title %}{{ _("Home") }}{% endblock %}{% endblock %}

View File

@ -0,0 +1,35 @@
{#
The Mia! Accounting Flask Demonstration Website
login.html: The login page.
Copyright (c) 2023 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: 2023/1/27
#}
{% extends "base.html" %}
{% block header %}{% block title %}{{ _("Log In") }}{% endblock %}{% endblock %}
{% block content %}
<form action="{{ url_for("auth.login") }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-primary" type="submit" name="username" value="viewer">{{ _("Viewer") }}</button>
<button class="btn btn-primary" type="submit" name="username" value="editor">{{ _("Editor") }}</button>
<button class="btn btn-primary" type="submit" name="username" value="nobody">{{ _("Nobody") }}</button>
</form>
{% endblock %}

View File

@ -0,0 +1,3 @@
[python: **.py]
[jinja2: **/templates/**.html]
[javascript: **/static/js/**.js]

View File

@ -0,0 +1,56 @@
# Chinese (Traditional) translations for the Mia! Accounting Flask
# Demonstration website.
# Copyright (C) 2023 imacat
# This file is distributed under the same license as the Mia! Accounting
# Flask Demonstration project.
# imacat <imacat@mail.imacat.idv.tw>, 2023.
#
msgid ""
msgstr ""
"Project-Id-Version: Mia! Accounting Flask Demonstration 0.0.0\n"
"Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n"
"POT-Creation-Date: 2023-01-28 13:42+0800\n"
"PO-Revision-Date: 2023-01-28 13:42+0800\n"
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
"Language: zh_Hant\n"
"Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.11.0\n"
#: tests/test_site/templates/base.html:23
msgid "en"
msgstr "zh-Hant"
#: tests/test_site/templates/base.html:43
#: tests/test_site/templates/home.html:24
msgid "Home"
msgstr "首頁"
#: tests/test_site/templates/base.html:68
msgid "Log Out"
msgstr ""
#: tests/test_site/templates/base.html:78
#: tests/test_site/templates/login.html:24
msgid "Log In"
msgstr "登入"
#: tests/test_site/templates/base.html:119
msgid "Error:"
msgstr "錯誤:"
#: tests/test_site/templates/login.html:30
msgid "Viewer"
msgstr "讀報表者"
#: tests/test_site/templates/login.html:31
msgid "Editor"
msgstr "記帳者"
#: tests/test_site/templates/login.html:32
msgid "Nobody"
msgstr "沒有權限者"