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.
Files changed (52) hide show
  1. bafser/__init__.py +48 -0
  2. bafser/app.py +237 -0
  3. bafser/bfs_config.example.py +16 -0
  4. bafser/cli.py +30 -0
  5. bafser/data/__init__.py +0 -0
  6. bafser/data/_roles.py +39 -0
  7. bafser/data/_tables.py +5 -0
  8. bafser/data/image.py +109 -0
  9. bafser/data/log.py +154 -0
  10. bafser/data/operation.py +38 -0
  11. bafser/data/permission.py +18 -0
  12. bafser/data/role.py +128 -0
  13. bafser/data/user.py +181 -0
  14. bafser/data/user_role.py +50 -0
  15. bafser/db_session.py +69 -0
  16. bafser/logger.py +213 -0
  17. bafser/readme.md +184 -0
  18. bafser/scripts/add_user.py +29 -0
  19. bafser/scripts/add_user_role.py +33 -0
  20. bafser/scripts/change_user_password.py +22 -0
  21. bafser/scripts/init_db_values.py +20 -0
  22. bafser/scripts/remove_user_role.py +33 -0
  23. bafser/scripts/update_roles_permissions.py +13 -0
  24. bafser/table_base.py +69 -0
  25. bafser/utils/__init__.py +24 -0
  26. bafser/utils/create_file_response.py +13 -0
  27. bafser/utils/create_folder_for_file.py +7 -0
  28. bafser/utils/get_all_vars.py +10 -0
  29. bafser/utils/get_datetime_now.py +5 -0
  30. bafser/utils/get_json.py +13 -0
  31. bafser/utils/get_json_list_from_req.py +14 -0
  32. bafser/utils/get_json_values.py +28 -0
  33. bafser/utils/get_json_values_from_req.py +20 -0
  34. bafser/utils/get_secret_key.py +21 -0
  35. bafser/utils/import_all_tables.py +18 -0
  36. bafser/utils/ip_to_emoji.py +26 -0
  37. bafser/utils/jsonify_list.py +5 -0
  38. bafser/utils/parse_date.py +10 -0
  39. bafser/utils/permission_required.py +52 -0
  40. bafser/utils/randstr.py +8 -0
  41. bafser/utils/register_blueprints.py +12 -0
  42. bafser/utils/response_msg.py +7 -0
  43. bafser/utils/response_not_found.py +5 -0
  44. bafser/utils/use_db_session.py +17 -0
  45. bafser/utils/use_user.py +37 -0
  46. bafser/utils/use_userId.py +43 -0
  47. bafser/utils/use_user_optional.py +35 -0
  48. bafser-1.0.2.dist-info/METADATA +205 -0
  49. bafser-1.0.2.dist-info/RECORD +52 -0
  50. bafser-1.0.2.dist-info/WHEEL +4 -0
  51. bafser-1.0.2.dist-info/entry_points.txt +2 -0
  52. 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()
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
@@ -0,0 +1,5 @@
1
+ class TablesBase:
2
+ User = "User"
3
+ Role = "Role"
4
+ UserRole = "UserRole"
5
+ Image = "Image"
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"
@@ -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