Basic app, mainly copied from tmpl-flask
This commit is contained in:
parent
354a473f77
commit
bfea5eeb06
20 changed files with 446 additions and 49 deletions
5
.env
Normal file
5
.env
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
MYSQL_HOST=${REPO_NAME_SNAKE}_db
|
||||||
|
MUSQL_PORT=3306
|
||||||
|
MYSQL_USER=${REPO_NAME_SNAKE}
|
||||||
|
MYSQL_PASSWORD=
|
||||||
|
MYSQL_DATABASE=${REPO_NAME_SNAKE}
|
5
.env_debug
Normal file
5
.env_debug
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
MYSQL_HOST=localhost
|
||||||
|
MUSQL_PORT=3306
|
||||||
|
MYSQL_USER=darkcat09
|
||||||
|
MYSQL_PASSWORD=
|
||||||
|
MYSQL_DATABASE=apptest
|
6
.gitea/template
Normal file
6
.gitea/template
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
app/*.py
|
||||||
|
templates/base.html
|
||||||
|
docker-compose.yml
|
||||||
|
Makefile
|
||||||
|
.env
|
||||||
|
README.md
|
52
.gitignore
vendored
52
.gitignore
vendored
|
@ -72,56 +72,23 @@ instance/
|
||||||
# Sphinx documentation
|
# Sphinx documentation
|
||||||
docs/_build/
|
docs/_build/
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
.pybuilder/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
# pyenv
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
|
||||||
#.python-version
|
#.python-version
|
||||||
|
|
||||||
# pipenv
|
# pipenv
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
||||||
# install all needed dependencies.
|
|
||||||
#Pipfile.lock
|
#Pipfile.lock
|
||||||
|
|
||||||
# poetry
|
# poetry
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
||||||
#poetry.lock
|
#poetry.lock
|
||||||
|
|
||||||
# pdm
|
# pdm
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
||||||
#pdm.lock
|
#pdm.lock
|
||||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
||||||
# in version control.
|
|
||||||
# https://pdm.fming.dev/#use-with-ide
|
|
||||||
.pdm.toml
|
.pdm.toml
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
# PEP 582
|
||||||
__pypackages__/
|
__pypackages__/
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
# Environments
|
||||||
.env
|
|
||||||
.venv
|
.venv
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
|
@ -129,13 +96,6 @@ ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs documentation
|
# mkdocs documentation
|
||||||
/site
|
/site
|
||||||
|
|
||||||
|
@ -153,10 +113,6 @@ dmypy.json
|
||||||
# Cython debug symbols
|
# Cython debug symbols
|
||||||
cython_debug/
|
cython_debug/
|
||||||
|
|
||||||
# PyCharm
|
# IDE
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
.vscode/
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
.idea/
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
#.idea/
|
|
||||||
|
|
||||||
|
|
7
Dockerfile
Normal file
7
Dockerfile
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
FROM python:3-alpine
|
||||||
|
RUN apk update && apk upgrade && apk add py-pip make
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
CMD make prod
|
||||||
|
EXPOSE 8000
|
20
Makefile
Normal file
20
Makefile
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
dev:
|
||||||
|
DEBUG="true" python3 -m uvicorn main:app --reload
|
||||||
|
|
||||||
|
prod:
|
||||||
|
python3 -m uvicorn main:app
|
||||||
|
|
||||||
|
format:
|
||||||
|
python3 -m autopep8 -r --in-place app/
|
||||||
|
|
||||||
|
check:
|
||||||
|
python3 -m mypy app/
|
||||||
|
python3 -m pylint app/
|
||||||
|
|
||||||
|
docker:
|
||||||
|
docker build -t ${REPO_OWNER_LOWER}/${REPO_NAME_SNAKE} .
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf app/__pycache__
|
||||||
|
rm -rf __pycache__
|
||||||
|
rm -rf .mypy_cache
|
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
19
app/main.py
Normal file
19
app/main.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
|
from pydantic import BaseSettings
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
secret_key: str = secrets.token_hex(32)
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
app = FastAPI()
|
||||||
|
app.mount(
|
||||||
|
'/static',
|
||||||
|
StaticFiles(directory='../static'),
|
||||||
|
name='static',
|
||||||
|
)
|
19
docker-compose.yml
Normal file
19
docker-compose.yml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
${REPO_NAME_SNAKE}:
|
||||||
|
image: ${REPO_OWNER}/${REPO_NAME_SNAKE}:latest
|
||||||
|
container_name: ${REPO_NAME_SNAKE}
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8080:8000"
|
||||||
|
links:
|
||||||
|
- ${REPO_NAME_SNAKE}_db
|
||||||
|
env_file: .env
|
||||||
|
${REPO_NAME_SNAKE}_db:
|
||||||
|
image: mariadb:latest
|
||||||
|
container_name: ${REPO_NAME_SNAKE}_db
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- "./database:/var/lib/mysql"
|
||||||
|
env_file: .env
|
9
main.py
Executable file
9
main.py
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
uvicorn.run(app)
|
187
pylintrc
Normal file
187
pylintrc
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
[MAIN]
|
||||||
|
analyse-fallback-blocks=no
|
||||||
|
extension-pkg-allow-list=
|
||||||
|
extension-pkg-whitelist=
|
||||||
|
fail-on=
|
||||||
|
fail-under=10
|
||||||
|
ignore=CVS
|
||||||
|
ignore-paths=
|
||||||
|
ignore-patterns=^\.#
|
||||||
|
ignored-modules=
|
||||||
|
jobs=4
|
||||||
|
limit-inference-results=100
|
||||||
|
load-plugins=
|
||||||
|
persistent=yes
|
||||||
|
py-version=3.10
|
||||||
|
recursive=no
|
||||||
|
suggestion-mode=yes
|
||||||
|
unsafe-load-any-extension=no
|
||||||
|
|
||||||
|
[REPORTS]
|
||||||
|
evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
|
||||||
|
msg-template=
|
||||||
|
reports=no
|
||||||
|
score=yes
|
||||||
|
|
||||||
|
[MESSAGES CONTROL]
|
||||||
|
confidence=HIGH,
|
||||||
|
CONTROL_FLOW,
|
||||||
|
INFERENCE,
|
||||||
|
INFERENCE_FAILURE,
|
||||||
|
UNDEFINED
|
||||||
|
disable=raw-checker-failed,
|
||||||
|
bad-inline-option,
|
||||||
|
locally-disabled,
|
||||||
|
file-ignored,
|
||||||
|
suppressed-message,
|
||||||
|
useless-suppression,
|
||||||
|
deprecated-pragma,
|
||||||
|
use-symbolic-message-instead
|
||||||
|
enable=c-extension-no-member
|
||||||
|
|
||||||
|
[SIMILARITIES]
|
||||||
|
ignore-comments=yes
|
||||||
|
ignore-docstrings=yes
|
||||||
|
ignore-imports=yes
|
||||||
|
ignore-signatures=yes
|
||||||
|
min-similarity-lines=4
|
||||||
|
|
||||||
|
[MISCELLANEOUS]
|
||||||
|
notes=FIXME,
|
||||||
|
XXX,
|
||||||
|
TODO
|
||||||
|
notes-rgx=
|
||||||
|
|
||||||
|
[DESIGN]
|
||||||
|
exclude-too-few-public-methods=
|
||||||
|
ignored-parents=
|
||||||
|
max-args=5
|
||||||
|
max-attributes=7
|
||||||
|
max-bool-expr=5
|
||||||
|
max-branches=12
|
||||||
|
max-locals=15
|
||||||
|
max-parents=7
|
||||||
|
max-public-methods=20
|
||||||
|
max-returns=6
|
||||||
|
max-statements=50
|
||||||
|
min-public-methods=1
|
||||||
|
|
||||||
|
[STRING]
|
||||||
|
check-quote-consistency=no
|
||||||
|
check-str-concat-over-line-jumps=no
|
||||||
|
|
||||||
|
[CLASSES]
|
||||||
|
check-protected-access-in-special-methods=no
|
||||||
|
defining-attr-methods=__init__,
|
||||||
|
__new__,
|
||||||
|
setUp,
|
||||||
|
__post_init__
|
||||||
|
exclude-protected=_asdict,
|
||||||
|
_fields,
|
||||||
|
_replace,
|
||||||
|
_source,
|
||||||
|
_make
|
||||||
|
valid-classmethod-first-arg=cls
|
||||||
|
valid-metaclass-classmethod-first-arg=cls
|
||||||
|
|
||||||
|
[FORMAT]
|
||||||
|
expected-line-ending-format=
|
||||||
|
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||||
|
indent-after-paren=4
|
||||||
|
indent-string=' '
|
||||||
|
max-line-length=100
|
||||||
|
max-module-lines=1000
|
||||||
|
single-line-class-stmt=no
|
||||||
|
single-line-if-stmt=no
|
||||||
|
|
||||||
|
[IMPORTS]
|
||||||
|
allow-any-import-level=
|
||||||
|
allow-wildcard-with-all=no
|
||||||
|
deprecated-modules=
|
||||||
|
ext-import-graph=
|
||||||
|
import-graph=
|
||||||
|
int-import-graph=
|
||||||
|
known-standard-library=
|
||||||
|
known-third-party=enchant
|
||||||
|
preferred-modules=
|
||||||
|
|
||||||
|
[VARIABLES]
|
||||||
|
additional-builtins=
|
||||||
|
allow-global-unused-variables=yes
|
||||||
|
allowed-redefined-builtins=
|
||||||
|
callbacks=cb_,
|
||||||
|
_cb
|
||||||
|
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||||
|
ignored-argument-names=_.*|^ignored_|^unused_
|
||||||
|
init-import=no
|
||||||
|
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
||||||
|
|
||||||
|
[LOGGING]
|
||||||
|
logging-format-style=old
|
||||||
|
logging-modules=logging
|
||||||
|
|
||||||
|
[EXCEPTIONS]
|
||||||
|
overgeneral-exceptions=BaseException,
|
||||||
|
Exception
|
||||||
|
|
||||||
|
[BASIC]
|
||||||
|
argument-naming-style=snake_case
|
||||||
|
attr-naming-style=snake_case
|
||||||
|
bad-names=foo,
|
||||||
|
bar,
|
||||||
|
baz,
|
||||||
|
toto,
|
||||||
|
tutu,
|
||||||
|
tata
|
||||||
|
bad-names-rgxs=
|
||||||
|
class-attribute-naming-style=any
|
||||||
|
class-const-naming-style=UPPER_CASE
|
||||||
|
class-naming-style=PascalCase
|
||||||
|
const-naming-style=any
|
||||||
|
docstring-min-length=-1
|
||||||
|
function-naming-style=snake_case
|
||||||
|
good-names=i,
|
||||||
|
j,
|
||||||
|
k,
|
||||||
|
f,
|
||||||
|
db,
|
||||||
|
ex,
|
||||||
|
Run,
|
||||||
|
_
|
||||||
|
good-names-rgxs=
|
||||||
|
include-naming-hint=no
|
||||||
|
inlinevar-naming-style=any
|
||||||
|
method-naming-style=snake_case
|
||||||
|
module-naming-style=snake_case
|
||||||
|
name-group=
|
||||||
|
no-docstring-rgx=^_
|
||||||
|
property-classes=abc.abstractproperty
|
||||||
|
variable-naming-style=snake_case
|
||||||
|
|
||||||
|
[SPELLING]
|
||||||
|
max-spelling-suggestions=4
|
||||||
|
spelling-dict=
|
||||||
|
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
|
||||||
|
spelling-ignore-words=
|
||||||
|
spelling-private-dict-file=
|
||||||
|
spelling-store-unknown-words=no
|
||||||
|
|
||||||
|
[TYPECHECK]
|
||||||
|
contextmanager-decorators=contextlib.contextmanager
|
||||||
|
generated-members=
|
||||||
|
ignore-none=yes
|
||||||
|
ignore-on-opaque-inference=yes
|
||||||
|
ignored-checks-for-mixins=no-member,
|
||||||
|
not-async-context-manager,
|
||||||
|
not-context-manager,
|
||||||
|
attribute-defined-outside-init
|
||||||
|
ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
|
||||||
|
missing-member-hint=yes
|
||||||
|
missing-member-hint-distance=1
|
||||||
|
missing-member-max-choices=1
|
||||||
|
mixin-class-rgx=.*[Mm]ixin
|
||||||
|
signature-mutators=
|
||||||
|
|
||||||
|
[REFACTORING]
|
||||||
|
max-nested-blocks=5
|
||||||
|
never-returning-functions=sys.exit,argparse.parse_error
|
5
requirements.txt
Normal file
5
requirements.txt
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
fastapi==0.92.0
|
||||||
|
uvicorn[standard]==0.20.0
|
||||||
|
jinja2==3.1.2
|
||||||
|
starlette-wtf==0.4.3
|
||||||
|
python-dotenv==0.21.1
|
60
static/css/style.css
Normal file
60
static/css/style.css
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
body {
|
||||||
|
height: 100vh;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
--bg: #fff;
|
||||||
|
--fg: #000;
|
||||||
|
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
--bg: #202023;
|
||||||
|
--fg: #eee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header { margin-top: 5px; }
|
||||||
|
footer { margin-bottom: 5px; }
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #5b8a55;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
filter: brightness(120%);
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
form > div {
|
||||||
|
margin-top: 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
form > div > label {
|
||||||
|
width: 50%;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
form > div > input {
|
||||||
|
width: 50%;
|
||||||
|
border: 1px solid var(--fg);
|
||||||
|
outline: none;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
form > div > input:hover,
|
||||||
|
form > div > input:focus {
|
||||||
|
filter: brightness(130%);
|
||||||
|
}
|
4
static/js/script.js
Normal file
4
static/js/script.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
addEventListener('load', () => {
|
||||||
|
document.getElementById('js')
|
||||||
|
.innerText = new Date().toLocaleString()
|
||||||
|
})
|
11
templates/404.html
Normal file
11
templates/404.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}404{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>404: Not Found</h1>
|
||||||
|
<p>
|
||||||
|
Go to the
|
||||||
|
<a href="/">main page</a>
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
15
templates/500.html
Normal file
15
templates/500.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}500{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>500: ISE</h1>
|
||||||
|
<p>
|
||||||
|
An error occured while
|
||||||
|
processing your request.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Please, try again later
|
||||||
|
or contact web site admin.
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
21
templates/admin.html
Normal file
21
templates/admin.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Admin{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Add a person to DB</h1>
|
||||||
|
<form action="/add" method="post">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{% for field in form %}
|
||||||
|
{% if field.name != 'csrf_token' %}
|
||||||
|
<div>
|
||||||
|
{{ field.label }}
|
||||||
|
{{ field }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<div>
|
||||||
|
<input type="submit" value="Add">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
18
templates/base.html
Normal file
18
templates/base.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="${REPO_DESCRIPTION}">
|
||||||
|
<title>{% block title %}{% endblock %} | ${REPO_NAME}</title>
|
||||||
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
<script src="/static/js/script.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>${REPO_NAME}</header>
|
||||||
|
<article>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</article>
|
||||||
|
<footer id="js"></footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
12
templates/index.html
Normal file
12
templates/index.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Main page{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>
|
||||||
|
This is the default main page of
|
||||||
|
<a href="https://git.dc09.ru/DarkCat09/tmpl-flask">
|
||||||
|
Flask app template
|
||||||
|
</a>
|
||||||
|
</h1>
|
||||||
|
{% endblock %}
|
18
templates/table.html
Normal file
18
templates/table.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Database{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Sample database</h1>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{% for row in rows %}
|
||||||
|
<tr>
|
||||||
|
{% for cell in row %}
|
||||||
|
<td>{{ cell }}</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
Loading…
Add table
Reference in a new issue