bafser 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bafser/__init__.py +48 -0
- bafser/app.py +237 -0
- bafser/bfs_config.example.py +16 -0
- bafser/cli.py +30 -0
- bafser/data/__init__.py +0 -0
- bafser/data/_roles.py +39 -0
- bafser/data/_tables.py +5 -0
- bafser/data/image.py +109 -0
- bafser/data/log.py +154 -0
- bafser/data/operation.py +38 -0
- bafser/data/permission.py +18 -0
- bafser/data/role.py +128 -0
- bafser/data/user.py +181 -0
- bafser/data/user_role.py +50 -0
- bafser/db_session.py +69 -0
- bafser/logger.py +213 -0
- bafser/readme.md +184 -0
- bafser/scripts/add_user.py +29 -0
- bafser/scripts/add_user_role.py +33 -0
- bafser/scripts/change_user_password.py +22 -0
- bafser/scripts/init_db_values.py +20 -0
- bafser/scripts/remove_user_role.py +33 -0
- bafser/scripts/update_roles_permissions.py +13 -0
- bafser/table_base.py +69 -0
- bafser/utils/__init__.py +24 -0
- bafser/utils/create_file_response.py +13 -0
- bafser/utils/create_folder_for_file.py +7 -0
- bafser/utils/get_all_vars.py +10 -0
- bafser/utils/get_datetime_now.py +5 -0
- bafser/utils/get_json.py +13 -0
- bafser/utils/get_json_list_from_req.py +14 -0
- bafser/utils/get_json_values.py +28 -0
- bafser/utils/get_json_values_from_req.py +20 -0
- bafser/utils/get_secret_key.py +21 -0
- bafser/utils/import_all_tables.py +18 -0
- bafser/utils/ip_to_emoji.py +26 -0
- bafser/utils/jsonify_list.py +5 -0
- bafser/utils/parse_date.py +10 -0
- bafser/utils/permission_required.py +52 -0
- bafser/utils/randstr.py +8 -0
- bafser/utils/register_blueprints.py +12 -0
- bafser/utils/response_msg.py +7 -0
- bafser/utils/response_not_found.py +5 -0
- bafser/utils/use_db_session.py +17 -0
- bafser/utils/use_user.py +37 -0
- bafser/utils/use_userId.py +43 -0
- bafser/utils/use_user_optional.py +35 -0
- bafser-1.0.2.dist-info/METADATA +205 -0
- bafser-1.0.2.dist-info/RECORD +52 -0
- bafser-1.0.2.dist-info/WHEEL +4 -0
- bafser-1.0.2.dist-info/entry_points.txt +2 -0
- bafser-1.0.2.dist-info/licenses/LICENSE +21 -0
bafser/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# base for flask server by Mixel Te
|
|
2
|
+
|
|
3
|
+
from .utils.response_msg import response_msg
|
|
4
|
+
from .utils.get_json_values import get_json_values
|
|
5
|
+
|
|
6
|
+
from .utils.create_file_response import create_file_response
|
|
7
|
+
from .utils.create_folder_for_file import create_folder_for_file
|
|
8
|
+
from .utils.get_datetime_now import get_datetime_now
|
|
9
|
+
from .utils.get_json import get_json
|
|
10
|
+
from .utils.get_json_list_from_req import get_json_list_from_req
|
|
11
|
+
from .utils.get_json_values_from_req import get_json_values_from_req
|
|
12
|
+
from .utils.ip_to_emoji import ip_to_emoji, emoji_to_ip
|
|
13
|
+
from .utils.jsonify_list import jsonify_list
|
|
14
|
+
from .utils.parse_date import parse_date
|
|
15
|
+
from .utils.permission_required import create_permission_required_decorator
|
|
16
|
+
from .utils.permission_required import permission_required, permission_required_any
|
|
17
|
+
from .utils.randstr import randstr
|
|
18
|
+
from .utils.response_not_found import response_not_found
|
|
19
|
+
from .utils.use_db_session import use_db_session
|
|
20
|
+
from .utils.use_userId import use_userId, use_userId_optional
|
|
21
|
+
from .utils.use_user import use_user
|
|
22
|
+
from .utils.use_user_optional import use_user_optional
|
|
23
|
+
|
|
24
|
+
from .app import AppConfig, create_app
|
|
25
|
+
from .logger import get_logger_frontend, log_frontend_error, get_log_fpath, add_file_logger, ParametrizedLogger
|
|
26
|
+
|
|
27
|
+
from .db_session import SqlAlchemyBase
|
|
28
|
+
from .table_base import TableBase, IdMixin, ObjMixin
|
|
29
|
+
from .data._tables import TablesBase
|
|
30
|
+
from .data._roles import RolesBase
|
|
31
|
+
from .data.operation import OperationsBase
|
|
32
|
+
from .data.user_role import UserRole
|
|
33
|
+
from .data.user import UserBase
|
|
34
|
+
from .data.log import Log
|
|
35
|
+
from .data.role import Role
|
|
36
|
+
from .data.image import Image
|
|
37
|
+
|
|
38
|
+
from .scripts.init_db_values import init_db_values
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class M:
|
|
42
|
+
DELETE = "DELETE"
|
|
43
|
+
GET = "GET"
|
|
44
|
+
HEAD = "HEAD"
|
|
45
|
+
OPTIONS = "OPTIONS"
|
|
46
|
+
PATCH = "PATCH"
|
|
47
|
+
POST = "POST"
|
|
48
|
+
PUT = "PUT"
|
bafser/app.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from datetime import datetime, timedelta, timezone
|
|
2
|
+
from typing import Callable, Literal, Union
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
import traceback
|
|
8
|
+
|
|
9
|
+
from flask import Flask, Response, abort, g, make_response, redirect, request, send_from_directory
|
|
10
|
+
from flask_jwt_extended import JWTManager, create_access_token, get_jwt, get_jwt_identity, set_access_cookies, verify_jwt_in_request
|
|
11
|
+
from urllib.parse import quote
|
|
12
|
+
|
|
13
|
+
from bafser.scripts.init_db_values import init_db_values
|
|
14
|
+
|
|
15
|
+
from . import db_session
|
|
16
|
+
from .logger import get_logger_requests, setLogging
|
|
17
|
+
from .utils import get_json, get_secret_key, get_secret_key_rnd, randstr, register_blueprints, response_msg
|
|
18
|
+
import bfs_config
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AppConfig():
|
|
22
|
+
is_admin_default = False
|
|
23
|
+
data_folders: list[tuple[str, str]] = []
|
|
24
|
+
config: list[tuple[str, str]] = []
|
|
25
|
+
|
|
26
|
+
def __init__(self,
|
|
27
|
+
FRONTEND_FOLDER="build",
|
|
28
|
+
IMAGES_FOLDER="images",
|
|
29
|
+
JWT_ACCESS_TOKEN_EXPIRES: Union[Literal[False], timedelta] = False,
|
|
30
|
+
JWT_ACCESS_TOKEN_REFRESH: Union[Literal[False], timedelta] = timedelta(minutes=30),
|
|
31
|
+
CACHE_MAX_AGE=31536000,
|
|
32
|
+
MESSAGE_TO_FRONTEND="",
|
|
33
|
+
STATIC_FOLDERS: list[str] = ["/static/", "/fonts/"],
|
|
34
|
+
DEV_MODE=False,
|
|
35
|
+
DELAY_MODE=False,
|
|
36
|
+
):
|
|
37
|
+
self.FRONTEND_FOLDER = FRONTEND_FOLDER
|
|
38
|
+
self.IMAGES_FOLDER = IMAGES_FOLDER
|
|
39
|
+
self.JWT_ACCESS_TOKEN_EXPIRES = JWT_ACCESS_TOKEN_EXPIRES
|
|
40
|
+
self.JWT_ACCESS_TOKEN_REFRESH = JWT_ACCESS_TOKEN_REFRESH
|
|
41
|
+
self.CACHE_MAX_AGE = CACHE_MAX_AGE
|
|
42
|
+
self.MESSAGE_TO_FRONTEND = MESSAGE_TO_FRONTEND
|
|
43
|
+
self.STATIC_FOLDERS = STATIC_FOLDERS
|
|
44
|
+
self.DEV_MODE = DEV_MODE
|
|
45
|
+
self.DELAY_MODE = DELAY_MODE
|
|
46
|
+
self.add_data_folder("IMAGES_FOLDER", IMAGES_FOLDER)
|
|
47
|
+
self.add("CACHE_MAX_AGE", CACHE_MAX_AGE)
|
|
48
|
+
|
|
49
|
+
def add(self, key: str, value: str):
|
|
50
|
+
self.config.append((key, value))
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
def add_data_folder(self, key: str, path: str):
|
|
54
|
+
self.add(key, path)
|
|
55
|
+
self.data_folders.append((key, path))
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
def add_secret_key(self, key: str, path: str):
|
|
59
|
+
self.add(key, get_secret_key(path))
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def add_secret_key_rnd(self, key: str, path: str):
|
|
63
|
+
self.add(key, get_secret_key_rnd(path))
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def create_app(import_name: str, config: AppConfig):
|
|
68
|
+
setLogging()
|
|
69
|
+
logreq = get_logger_requests()
|
|
70
|
+
app = Flask(import_name, static_folder=None)
|
|
71
|
+
app.config["JWT_TOKEN_LOCATION"] = ["cookies"]
|
|
72
|
+
app.config["JWT_SECRET_KEY"] = get_secret_key_rnd(bfs_config.jwt_key_file_path)
|
|
73
|
+
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = config.JWT_ACCESS_TOKEN_EXPIRES
|
|
74
|
+
app.config["JWT_COOKIE_CSRF_PROTECT"] = False
|
|
75
|
+
app.config["JWT_SESSION_COOKIE"] = False
|
|
76
|
+
for (key, path) in config.config:
|
|
77
|
+
app.config[key] = path
|
|
78
|
+
|
|
79
|
+
jwt_manager = JWTManager(app)
|
|
80
|
+
|
|
81
|
+
def run(run_app: bool, init_dev_values: Callable[[], None] = None, port=5000):
|
|
82
|
+
for (_, path) in config.data_folders:
|
|
83
|
+
if not os.path.exists(path):
|
|
84
|
+
os.makedirs(path)
|
|
85
|
+
|
|
86
|
+
if config.DEV_MODE:
|
|
87
|
+
if not os.path.exists(bfs_config.db_dev_path):
|
|
88
|
+
os.makedirs(os.path.dirname(bfs_config.db_dev_path), exist_ok=True)
|
|
89
|
+
init_db_values(True)
|
|
90
|
+
if init_dev_values is not None:
|
|
91
|
+
init_dev_values()
|
|
92
|
+
|
|
93
|
+
db_session.global_init(config.DEV_MODE)
|
|
94
|
+
|
|
95
|
+
if not config.DEV_MODE:
|
|
96
|
+
check_is_admin_default()
|
|
97
|
+
|
|
98
|
+
register_blueprints(app)
|
|
99
|
+
if run_app:
|
|
100
|
+
print("Starting")
|
|
101
|
+
if config.DELAY_MODE:
|
|
102
|
+
print("Delay for requests is enabled")
|
|
103
|
+
app.run(debug=True, port=port)
|
|
104
|
+
|
|
105
|
+
def check_is_admin_default():
|
|
106
|
+
from . import UserBase
|
|
107
|
+
db_sess = db_session.create_session()
|
|
108
|
+
admin = UserBase.get_by_login(db_sess, "admin", includeDeleted=True)
|
|
109
|
+
if admin is not None:
|
|
110
|
+
config.is_admin_default = admin.check_password("admin")
|
|
111
|
+
db_sess.close()
|
|
112
|
+
|
|
113
|
+
@app.before_request
|
|
114
|
+
def before_request():
|
|
115
|
+
g.json = get_json(request)
|
|
116
|
+
g.req_id = randstr(4)
|
|
117
|
+
try:
|
|
118
|
+
verify_jwt_in_request()
|
|
119
|
+
jwt_identity = get_jwt_identity()
|
|
120
|
+
except Exception:
|
|
121
|
+
jwt_identity = None
|
|
122
|
+
if jwt_identity and isinstance(jwt_identity, (list, tuple)) and len(jwt_identity) == 2:
|
|
123
|
+
g.userId = jwt_identity[0]
|
|
124
|
+
if request.path.startswith(bfs_config.api_url):
|
|
125
|
+
try:
|
|
126
|
+
if g.json[1]:
|
|
127
|
+
if "password" in g.json[0]:
|
|
128
|
+
password = g.json[0]["password"]
|
|
129
|
+
g.json[0]["password"] = "***"
|
|
130
|
+
data = json.dumps(g.json[0])[:512]
|
|
131
|
+
g.json[0]["password"] = password
|
|
132
|
+
else:
|
|
133
|
+
data = json.dumps(g.json[0])[:512]
|
|
134
|
+
logreq.info("Request;;%(data)s", {"data": data})
|
|
135
|
+
else:
|
|
136
|
+
logreq.info("Request")
|
|
137
|
+
except Exception as x:
|
|
138
|
+
logging.error("Request logging error: %s", x)
|
|
139
|
+
|
|
140
|
+
if config.DELAY_MODE:
|
|
141
|
+
time.sleep(0.5)
|
|
142
|
+
if config.is_admin_default:
|
|
143
|
+
check_is_admin_default()
|
|
144
|
+
if config.is_admin_default:
|
|
145
|
+
# Admin password must be changed
|
|
146
|
+
return response_msg("Security error")
|
|
147
|
+
|
|
148
|
+
@app.after_request
|
|
149
|
+
def after_request(response: Response):
|
|
150
|
+
if request.path.startswith(bfs_config.api_url):
|
|
151
|
+
try:
|
|
152
|
+
if response.content_type == "application/json":
|
|
153
|
+
logreq.info("Response;%s;%s", response.status_code, str(response.data)[:512])
|
|
154
|
+
else:
|
|
155
|
+
logreq.info("Response;%s", response.status_code)
|
|
156
|
+
except Exception as x:
|
|
157
|
+
logging.error("Request logging error: %s", x)
|
|
158
|
+
|
|
159
|
+
response.set_cookie("MESSAGE_TO_FRONTEND", quote(config.MESSAGE_TO_FRONTEND))
|
|
160
|
+
|
|
161
|
+
if config.JWT_ACCESS_TOKEN_REFRESH:
|
|
162
|
+
try:
|
|
163
|
+
exp_timestamp = get_jwt()["exp"]
|
|
164
|
+
now = datetime.now(timezone.utc)
|
|
165
|
+
target_timestamp = datetime.timestamp(now + config.JWT_ACCESS_TOKEN_REFRESH)
|
|
166
|
+
if target_timestamp > exp_timestamp:
|
|
167
|
+
access_token = create_access_token(identity=get_jwt_identity())
|
|
168
|
+
set_access_cookies(response, access_token)
|
|
169
|
+
except (RuntimeError, KeyError):
|
|
170
|
+
# Case where there is not a valid JWT
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
return response
|
|
174
|
+
|
|
175
|
+
@app.route("/", defaults={"path": ""})
|
|
176
|
+
@app.route("/<path:path>")
|
|
177
|
+
def frontend(path):
|
|
178
|
+
if request.path.startswith(bfs_config.api_url):
|
|
179
|
+
abort(404)
|
|
180
|
+
if path != "" and os.path.exists(config.FRONTEND_FOLDER + "/" + path):
|
|
181
|
+
res = send_from_directory(config.FRONTEND_FOLDER, path)
|
|
182
|
+
if any(request.path.startswith(path) for path in config.STATIC_FOLDERS):
|
|
183
|
+
res.headers.set("Cache-Control", f"public,max-age={config.CACHE_MAX_AGE},immutable")
|
|
184
|
+
else:
|
|
185
|
+
res.headers.set("Cache-Control", "no_cache")
|
|
186
|
+
return res
|
|
187
|
+
else:
|
|
188
|
+
res = send_from_directory(config.FRONTEND_FOLDER, "index.html")
|
|
189
|
+
res.headers.set("Cache-Control", "no_cache")
|
|
190
|
+
return res
|
|
191
|
+
|
|
192
|
+
@app.errorhandler(404)
|
|
193
|
+
def not_found(error):
|
|
194
|
+
if request.path.startswith(bfs_config.api_url):
|
|
195
|
+
return response_msg("Not found", 404)
|
|
196
|
+
return make_response("Страница не найдена", 404)
|
|
197
|
+
|
|
198
|
+
@app.errorhandler(405)
|
|
199
|
+
def method_not_allowed(error):
|
|
200
|
+
return response_msg("Method Not Allowed", 405)
|
|
201
|
+
|
|
202
|
+
@app.errorhandler(415)
|
|
203
|
+
def unsupported_media_type(error):
|
|
204
|
+
return response_msg("Unsupported Media Type", 415)
|
|
205
|
+
|
|
206
|
+
@app.errorhandler(403)
|
|
207
|
+
def no_permission(error):
|
|
208
|
+
return response_msg("No permission", 403)
|
|
209
|
+
|
|
210
|
+
@app.errorhandler(500)
|
|
211
|
+
@app.errorhandler(Exception)
|
|
212
|
+
def internal_server_error(error):
|
|
213
|
+
print(error)
|
|
214
|
+
logging.error("%s\n%s", error, traceback.format_exc())
|
|
215
|
+
if request.path.startswith(bfs_config.api_url):
|
|
216
|
+
return response_msg("Internal Server Error", 500)
|
|
217
|
+
return make_response("Произошла ошибка", 500)
|
|
218
|
+
|
|
219
|
+
@app.errorhandler(401)
|
|
220
|
+
def unauthorized(error):
|
|
221
|
+
if request.path.startswith(bfs_config.api_url):
|
|
222
|
+
return response_msg("Unauthorized", 401)
|
|
223
|
+
return redirect(bfs_config.login_page_url)
|
|
224
|
+
|
|
225
|
+
@jwt_manager.expired_token_loader
|
|
226
|
+
def expired_token_loader(jwt_header, jwt_data):
|
|
227
|
+
return response_msg("The JWT has expired", 401)
|
|
228
|
+
|
|
229
|
+
@jwt_manager.invalid_token_loader
|
|
230
|
+
def invalid_token_loader(error):
|
|
231
|
+
return response_msg("Invalid JWT", 401)
|
|
232
|
+
|
|
233
|
+
@jwt_manager.unauthorized_loader
|
|
234
|
+
def unauthorized_loader(error):
|
|
235
|
+
return response_msg("Unauthorized", 401)
|
|
236
|
+
|
|
237
|
+
return app, run
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
db_dev_path = "db/dev.db"
|
|
2
|
+
db_path = "ticketsystem:UR2hqJDbSfQ@ticketsystem.mysql.pythonanywhere-services.com/ticketsystem$default"
|
|
3
|
+
sql_echo = False
|
|
4
|
+
|
|
5
|
+
log_info_path = "logs/log_info.csv"
|
|
6
|
+
log_requests_path = "logs/log_requests.csv"
|
|
7
|
+
log_errors_path = "logs/log_errors.log"
|
|
8
|
+
log_frontend_path = "logs/log_frontend.log"
|
|
9
|
+
|
|
10
|
+
jwt_key_file_path = "secret_key_jwt.txt"
|
|
11
|
+
|
|
12
|
+
login_page_url = "/login"
|
|
13
|
+
api_url = "/api/"
|
|
14
|
+
|
|
15
|
+
blueprints_folder = "blueprints"
|
|
16
|
+
data_tables_folder = "data"
|
bafser/cli.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def cli():
|
|
6
|
+
scripts = [
|
|
7
|
+
("add_user_role", "userId roleId [dev]"),
|
|
8
|
+
("add_user", "login password name roleId [dev]"),
|
|
9
|
+
("change_user_password", "login new_password [dev]"),
|
|
10
|
+
("init_db_values", "[dev]"),
|
|
11
|
+
("remove_user_role", "userId roleId [dev]"),
|
|
12
|
+
("update_roles_permissions", "[dev]"),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
if len(sys.argv) < 2 or sys.argv[1] not in map(lambda v: v[0], scripts):
|
|
16
|
+
ml = max(map(lambda v: len(v[0]), scripts))
|
|
17
|
+
ml2 = max(map(lambda v: len(v[1]), scripts))
|
|
18
|
+
l = ml + ml2 + 5
|
|
19
|
+
t = " Scripts "
|
|
20
|
+
l2 = (l - len(t))
|
|
21
|
+
print("-" * (l2 // 2) + t + "-" * (l2 // 2 + l2 % 2))
|
|
22
|
+
print("\n".join(map(lambda v: f"{' ' * (ml - len(v[0]))} {v[0]} : {v[1]}", scripts)))
|
|
23
|
+
print("-" * l)
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
importlib.import_module("scripts." + sys.argv[1], "bafser").run(sys.argv[2:])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
cli()
|
bafser/data/__init__.py
ADDED
|
File without changes
|
bafser/data/_roles.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import Type, TypedDict
|
|
2
|
+
|
|
3
|
+
from ..utils.get_all_vars import get_all_fields
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Roles: "Type[RolesBase]" = None
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_roles():
|
|
10
|
+
if Roles is None:
|
|
11
|
+
raise Exception("[BFS] No class inherited from RolesBase")
|
|
12
|
+
return Roles
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RoleDesc(TypedDict):
|
|
16
|
+
name: str
|
|
17
|
+
operations: list[tuple[str, str]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
TRole = int
|
|
21
|
+
TRoles = dict[TRole, RoleDesc]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RolesBase:
|
|
25
|
+
admin = 1
|
|
26
|
+
|
|
27
|
+
ROLES: TRoles = {}
|
|
28
|
+
|
|
29
|
+
def __init_subclass__(cls, **kwargs):
|
|
30
|
+
global Roles
|
|
31
|
+
Roles = cls
|
|
32
|
+
fields = list(get_all_fields(cls()))
|
|
33
|
+
same = []
|
|
34
|
+
for i in range(len(fields)):
|
|
35
|
+
for k in range(i + 1, len(fields)):
|
|
36
|
+
if fields[i][1] == fields[k][1]:
|
|
37
|
+
same.append((fields[i][0], fields[k][0], fields[i][1]))
|
|
38
|
+
if len(same) > 0:
|
|
39
|
+
raise Exception(f"[BFS] Same ids for Role: {'; '.join(f'{n1} and {n2} is {v}' for (n1, n2, v) in same)}")
|
bafser/data/_tables.py
ADDED
bafser/data/image.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any, TypedDict, Union
|
|
3
|
+
import base64
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from flask import current_app
|
|
7
|
+
from sqlalchemy import Column, String, orm, ForeignKey, Integer, DateTime
|
|
8
|
+
from sqlalchemy.orm import Session
|
|
9
|
+
|
|
10
|
+
from .. import SqlAlchemyBase, ObjMixin, UserBase, Log, get_json_values, get_datetime_now, create_file_response
|
|
11
|
+
from ._tables import TablesBase
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ImageJson(TypedDict):
|
|
15
|
+
data: str
|
|
16
|
+
name: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
TError = str
|
|
20
|
+
TFieldName = str
|
|
21
|
+
TValue = Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Image(SqlAlchemyBase, ObjMixin):
|
|
25
|
+
__tablename__ = TablesBase.Image
|
|
26
|
+
|
|
27
|
+
name = Column(String(128), nullable=False)
|
|
28
|
+
type = Column(String(16), nullable=False)
|
|
29
|
+
creationDate = Column(DateTime, nullable=False)
|
|
30
|
+
deletionDate = Column(DateTime, nullable=True)
|
|
31
|
+
createdById = Column(Integer, ForeignKey("User.id"), nullable=False)
|
|
32
|
+
|
|
33
|
+
creator = orm.relationship("User")
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def new(cls, creator: UserBase, json: ImageJson) -> Union[tuple[None, TError], tuple["Image", None]]:
|
|
37
|
+
(data, name), values_error = get_json_values(json, "data", "name")
|
|
38
|
+
if values_error:
|
|
39
|
+
return None, values_error
|
|
40
|
+
|
|
41
|
+
data_splited = data.split(',')
|
|
42
|
+
if len(data_splited) != 2:
|
|
43
|
+
return None, "img data is not base64"
|
|
44
|
+
|
|
45
|
+
img_header, img_data = data_splited
|
|
46
|
+
img_header_splited = img_header.split(";")
|
|
47
|
+
if len(img_header_splited) != 2 or img_header_splited[1] != "base64":
|
|
48
|
+
return None, "img data is not base64"
|
|
49
|
+
|
|
50
|
+
img_header_splited_splited = img_header_splited[0].split(":")
|
|
51
|
+
if len(img_header_splited_splited) != 2:
|
|
52
|
+
return None, "img data is not base64"
|
|
53
|
+
mimetype = img_header_splited_splited[1]
|
|
54
|
+
|
|
55
|
+
if mimetype not in ["image/png", "image/jpeg", "image/gif"]:
|
|
56
|
+
return None, "img mimetype is not in [image/png, image/jpeg, image/gif]"
|
|
57
|
+
|
|
58
|
+
type = mimetype.split("/")[1]
|
|
59
|
+
|
|
60
|
+
db_sess = Session.object_session(creator)
|
|
61
|
+
now = get_datetime_now()
|
|
62
|
+
img, add_changes, err = cls._new(creator, json, {"name": name, "type": type, "createdById": creator.id, "creationDate": now})
|
|
63
|
+
if err:
|
|
64
|
+
return None, err
|
|
65
|
+
db_sess.add(img)
|
|
66
|
+
db_sess.commit()
|
|
67
|
+
|
|
68
|
+
path = img.get_path()
|
|
69
|
+
with open(path, "wb") as f:
|
|
70
|
+
f.write(base64.b64decode(img_data + '=='))
|
|
71
|
+
|
|
72
|
+
Log.added(img, creator, [
|
|
73
|
+
("name", img.name),
|
|
74
|
+
("type", img.type),
|
|
75
|
+
("creationDate", img.creationDate.isoformat()),
|
|
76
|
+
("createdById", img.createdById),
|
|
77
|
+
*add_changes,
|
|
78
|
+
], now)
|
|
79
|
+
|
|
80
|
+
return img, None
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def _new(creator: UserBase, json: ImageJson, image_kwargs: dict) -> \
|
|
84
|
+
Union[tuple[None, None, TError], tuple["Image", list[tuple[TFieldName, TValue]], None]]:
|
|
85
|
+
img = Image(**image_kwargs)
|
|
86
|
+
return img, [], None
|
|
87
|
+
|
|
88
|
+
def create_file_response(self):
|
|
89
|
+
return create_file_response(self.get_path(), f"image/{self.type}", self.get_filename())
|
|
90
|
+
|
|
91
|
+
def delete(self, actor: UserBase, commit=True, now: datetime = None, db_sess: Session = None):
|
|
92
|
+
now = get_datetime_now() if now is None else now
|
|
93
|
+
self.deletionDate = now
|
|
94
|
+
super().delete(actor, commit, now, db_sess)
|
|
95
|
+
|
|
96
|
+
def restore(self, actor: UserBase, commit=True, now: datetime = None, db_sess: Session = None):
|
|
97
|
+
if not os.path.exists(self.get_path()):
|
|
98
|
+
return False
|
|
99
|
+
super().restore(actor, commit, now, db_sess)
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
def get_path(self):
|
|
103
|
+
return os.path.join(current_app.config["IMAGES_FOLDER"], f"{self.id}.{self.type}")
|
|
104
|
+
|
|
105
|
+
def get_filename(self):
|
|
106
|
+
return self.name + "." + self.type
|
|
107
|
+
|
|
108
|
+
def get_dict(self):
|
|
109
|
+
return self.to_dict(only=("name", "type", "creationDate", "deletionDate", "createdById"))
|
bafser/data/log.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any, Union
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import Column, DateTime, orm, Integer, String, JSON
|
|
5
|
+
from sqlalchemy.orm import Session
|
|
6
|
+
|
|
7
|
+
from .. import SqlAlchemyBase, TableBase, UserBase, IdMixin, get_datetime_now
|
|
8
|
+
|
|
9
|
+
FieldName = str
|
|
10
|
+
NewValue = Any
|
|
11
|
+
OldValue = Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Log(SqlAlchemyBase, IdMixin):
|
|
15
|
+
__tablename__ = "Log"
|
|
16
|
+
|
|
17
|
+
date = Column(DateTime, nullable=False)
|
|
18
|
+
actionCode = Column(String(16), nullable=False)
|
|
19
|
+
userId = Column(Integer, nullable=False)
|
|
20
|
+
userName = Column(String(64), nullable=False)
|
|
21
|
+
tableName = Column(String(16), nullable=False)
|
|
22
|
+
recordId = Column(Integer, nullable=False)
|
|
23
|
+
changes = Column(JSON, nullable=False)
|
|
24
|
+
|
|
25
|
+
def __repr__(self):
|
|
26
|
+
return f"<Log> [{self.id}] {self.date} {self.actionCode}"
|
|
27
|
+
|
|
28
|
+
def get_dict(self):
|
|
29
|
+
return self.to_dict(only=("id", "date", "actionCode", "userId", "userName", "tableName", "recordId", "changes"))
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def added(
|
|
33
|
+
record: TableBase,
|
|
34
|
+
actor: Union[UserBase, None],
|
|
35
|
+
changes: list[tuple[FieldName, NewValue]],
|
|
36
|
+
now: datetime = None,
|
|
37
|
+
commit=True,
|
|
38
|
+
db_sess: Session = None,
|
|
39
|
+
):
|
|
40
|
+
if actor is None:
|
|
41
|
+
actor = UserBase.get_fake_system()
|
|
42
|
+
db_sess = db_sess if db_sess else Session.object_session(actor)
|
|
43
|
+
if now is None:
|
|
44
|
+
now = get_datetime_now()
|
|
45
|
+
log = Log(
|
|
46
|
+
date=now,
|
|
47
|
+
actionCode=Actions.added,
|
|
48
|
+
userId=actor.id,
|
|
49
|
+
userName=actor.name,
|
|
50
|
+
tableName=record.__tablename__,
|
|
51
|
+
recordId=-1,
|
|
52
|
+
changes=list(map(lambda v: (v[0], None, v[1]), changes))
|
|
53
|
+
)
|
|
54
|
+
db_sess.add(log)
|
|
55
|
+
if isinstance(record, IdMixin):
|
|
56
|
+
if record.id is not None:
|
|
57
|
+
log.recordId = record.id
|
|
58
|
+
elif commit:
|
|
59
|
+
db_sess.commit()
|
|
60
|
+
log.recordId = record.id
|
|
61
|
+
if commit:
|
|
62
|
+
db_sess.commit()
|
|
63
|
+
return log
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def updated(
|
|
67
|
+
record: TableBase,
|
|
68
|
+
actor: Union[UserBase, None],
|
|
69
|
+
changes: list[tuple[FieldName, OldValue, NewValue]],
|
|
70
|
+
now: datetime = None,
|
|
71
|
+
commit=True,
|
|
72
|
+
db_sess: Session = None,
|
|
73
|
+
):
|
|
74
|
+
if actor is None:
|
|
75
|
+
actor = UserBase.get_fake_system()
|
|
76
|
+
db_sess = db_sess if db_sess else Session.object_session(actor)
|
|
77
|
+
if now is None:
|
|
78
|
+
now = get_datetime_now()
|
|
79
|
+
log = Log(
|
|
80
|
+
date=now,
|
|
81
|
+
actionCode=Actions.updated,
|
|
82
|
+
userId=actor.id,
|
|
83
|
+
userName=actor.name,
|
|
84
|
+
tableName=record.__tablename__,
|
|
85
|
+
recordId=record.id if isinstance(record, IdMixin) else -1,
|
|
86
|
+
changes=changes
|
|
87
|
+
)
|
|
88
|
+
db_sess.add(log)
|
|
89
|
+
if commit:
|
|
90
|
+
db_sess.commit()
|
|
91
|
+
return log
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def deleted(
|
|
95
|
+
record: TableBase,
|
|
96
|
+
actor: Union[UserBase, None],
|
|
97
|
+
changes: list[tuple[FieldName, OldValue]] = [],
|
|
98
|
+
now: datetime = None,
|
|
99
|
+
commit=True,
|
|
100
|
+
db_sess: Session = None,
|
|
101
|
+
):
|
|
102
|
+
if actor is None:
|
|
103
|
+
actor = UserBase.get_fake_system()
|
|
104
|
+
db_sess = db_sess if db_sess else Session.object_session(actor)
|
|
105
|
+
if now is None:
|
|
106
|
+
now = get_datetime_now()
|
|
107
|
+
log = Log(
|
|
108
|
+
date=now,
|
|
109
|
+
actionCode=Actions.deleted,
|
|
110
|
+
userId=actor.id,
|
|
111
|
+
userName=actor.name,
|
|
112
|
+
tableName=record.__tablename__,
|
|
113
|
+
recordId=record.id if isinstance(record, IdMixin) else -1,
|
|
114
|
+
changes=list(map(lambda v: (v[0], v[1], None), changes))
|
|
115
|
+
)
|
|
116
|
+
db_sess.add(log)
|
|
117
|
+
if commit:
|
|
118
|
+
db_sess.commit()
|
|
119
|
+
return log
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def restored(
|
|
123
|
+
record: TableBase,
|
|
124
|
+
actor: Union[UserBase, None],
|
|
125
|
+
changes: list[tuple[FieldName, OldValue, NewValue]] = [],
|
|
126
|
+
now: datetime = None,
|
|
127
|
+
commit=True,
|
|
128
|
+
db_sess: Session = None,
|
|
129
|
+
):
|
|
130
|
+
if actor is None:
|
|
131
|
+
actor = UserBase.get_fake_system()
|
|
132
|
+
db_sess = db_sess if db_sess else Session.object_session(actor)
|
|
133
|
+
if now is None:
|
|
134
|
+
now = get_datetime_now()
|
|
135
|
+
log = Log(
|
|
136
|
+
date=now,
|
|
137
|
+
actionCode=Actions.restored,
|
|
138
|
+
userId=actor.id,
|
|
139
|
+
userName=actor.name,
|
|
140
|
+
tableName=record.__tablename__,
|
|
141
|
+
recordId=record.id if isinstance(record, IdMixin) else -1,
|
|
142
|
+
changes=changes
|
|
143
|
+
)
|
|
144
|
+
db_sess.add(log)
|
|
145
|
+
if commit:
|
|
146
|
+
db_sess.commit()
|
|
147
|
+
return log
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class Actions:
|
|
151
|
+
added = "added"
|
|
152
|
+
updated = "updated"
|
|
153
|
+
deleted = "deleted"
|
|
154
|
+
restored = "restored"
|
bafser/data/operation.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Generator, Type
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import Column, String
|
|
4
|
+
|
|
5
|
+
from .. import SqlAlchemyBase
|
|
6
|
+
from ..utils import get_all_values
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Operation(SqlAlchemyBase):
|
|
10
|
+
__tablename__ = "Operation"
|
|
11
|
+
|
|
12
|
+
id = Column(String(32), primary_key=True, unique=True)
|
|
13
|
+
name = Column(String(32), nullable=False)
|
|
14
|
+
|
|
15
|
+
def __repr__(self):
|
|
16
|
+
return f"<Operation> [{self.id}] {self.name}"
|
|
17
|
+
|
|
18
|
+
def get_dict(self):
|
|
19
|
+
return self.to_dict(only=("id", "name"))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OperationsBase:
|
|
23
|
+
@classmethod
|
|
24
|
+
def get_all(cls) -> Generator[tuple[str, str], None, None]:
|
|
25
|
+
return get_all_values(cls())
|
|
26
|
+
|
|
27
|
+
def __init_subclass__(cls, **kwargs):
|
|
28
|
+
global Operations
|
|
29
|
+
Operations = cls
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
Operations: Type[OperationsBase] = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_operations():
|
|
36
|
+
if Operations is None:
|
|
37
|
+
raise Exception("[BFS] No class inherited from OperationsBase")
|
|
38
|
+
return Operations
|