ul-api-utils 7.7.9__py3-none-any.whl → 7.8.0__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.
Potentially problematic release.
This version of ul-api-utils might be problematic. Click here for more details.
- ul_api_utils/commands/cmd_generate_api_docs.py +112 -81
- ul_api_utils/resources/web_forms/__init__.py +0 -0
- ul_api_utils/resources/web_forms/custom_fields/__init__.py +0 -0
- ul_api_utils/resources/web_forms/custom_fields/custom_checkbox_select.py +5 -0
- ul_api_utils/resources/web_forms/custom_widgets/__init__.py +0 -0
- ul_api_utils/resources/web_forms/custom_widgets/custom_select_widget.py +86 -0
- ul_api_utils/resources/web_forms/custom_widgets/custom_text_input_widget.py +42 -0
- ul_api_utils/resources/web_forms/uni_form.py +75 -0
- {ul_api_utils-7.7.9.dist-info → ul_api_utils-7.8.0.dist-info}/METADATA +1 -1
- {ul_api_utils-7.7.9.dist-info → ul_api_utils-7.8.0.dist-info}/RECORD +14 -7
- {ul_api_utils-7.7.9.dist-info → ul_api_utils-7.8.0.dist-info}/LICENSE +0 -0
- {ul_api_utils-7.7.9.dist-info → ul_api_utils-7.8.0.dist-info}/WHEEL +0 -0
- {ul_api_utils-7.7.9.dist-info → ul_api_utils-7.8.0.dist-info}/entry_points.txt +0 -0
- {ul_api_utils-7.7.9.dist-info → ul_api_utils-7.8.0.dist-info}/top_level.txt +0 -0
|
@@ -5,7 +5,7 @@ import inspect
|
|
|
5
5
|
import os
|
|
6
6
|
import logging
|
|
7
7
|
from datetime import datetime
|
|
8
|
-
from typing import Dict, Callable, Any
|
|
8
|
+
from typing import Dict, Callable, Any, List, TextIO
|
|
9
9
|
|
|
10
10
|
from flask import url_for, Flask
|
|
11
11
|
from ul_py_tool.commands.cmd import Cmd
|
|
@@ -33,102 +33,133 @@ class CmdGenApiFunctionDocumentation(Cmd):
|
|
|
33
33
|
return self.api_dir.replace('/', '.')
|
|
34
34
|
|
|
35
35
|
@property
|
|
36
|
-
def
|
|
36
|
+
def api_main_module(self) -> str:
|
|
37
37
|
return self.api_dir.replace('/', '.') + ".main"
|
|
38
38
|
|
|
39
|
+
def run(self) -> None:
|
|
40
|
+
root_folder = os.getcwd()
|
|
41
|
+
conf.APPLICATION_DIR = os.path.join(root_folder, self.api_dir) # because sdk uses this variable to load routes
|
|
42
|
+
current_app = self.load_flask_app(self.api_main_module)
|
|
43
|
+
api_utils_functions = self.load_functions(f'{self.api_dir}/utils')
|
|
44
|
+
conf.APPLICATION_DIR = os.path.join(root_folder, self.db_dir)
|
|
45
|
+
db_helper_functions = self.load_functions(f'{self.db_dir}/models_manager')
|
|
46
|
+
db_utils_functions = self.load_functions(f"{self.db_dir}/utils")
|
|
47
|
+
with current_app.app_context():
|
|
48
|
+
current_app.config['SERVER_NAME'] = '{base_url}'
|
|
49
|
+
with current_app.app_context(), open(f'.tmp/api_doc_{datetime.now().isoformat()}.md', 'w') as file:
|
|
50
|
+
for api_route_id, flask_api_rule in enumerate(current_app.url_map.iter_rules()):
|
|
51
|
+
options = {}
|
|
52
|
+
for arg in flask_api_rule.arguments:
|
|
53
|
+
options[arg] = "[{0}]".format(arg)
|
|
54
|
+
api_route_methods = ','.join([method for method in flask_api_rule.methods if method not in ('HEAD', 'OPTIONS')]) # type: ignore
|
|
55
|
+
api_route_path = url_for(flask_api_rule.endpoint, **options).replace('%5B', '[').replace('%5D', ']')
|
|
56
|
+
func_object = current_app.view_functions[flask_api_rule.endpoint]
|
|
57
|
+
if not func_object.__module__.startswith(self.api_module):
|
|
58
|
+
continue
|
|
59
|
+
self.generate_documentation(
|
|
60
|
+
func_object,
|
|
61
|
+
file,
|
|
62
|
+
api_route_id=api_route_id,
|
|
63
|
+
api_route_path=api_route_path,
|
|
64
|
+
api_route_methods=api_route_methods,
|
|
65
|
+
loaded_db_helper_functions=db_helper_functions,
|
|
66
|
+
loaded_api_utils_functions=api_utils_functions,
|
|
67
|
+
loaded_db_utils_functions=db_utils_functions,
|
|
68
|
+
)
|
|
69
|
+
|
|
39
70
|
@staticmethod
|
|
40
|
-
def
|
|
41
|
-
|
|
71
|
+
def load_functions(directory: str) -> Dict[str, Callable[..., Any]]:
|
|
72
|
+
function_name_object__map = {}
|
|
42
73
|
for root, _dirs, files in os.walk(directory):
|
|
43
74
|
for file in files:
|
|
44
|
-
|
|
45
|
-
|
|
75
|
+
py_postfix = '.py'
|
|
76
|
+
if file.endswith(py_postfix):
|
|
77
|
+
module_name = file[:-len(py_postfix)]
|
|
46
78
|
module_path = os.path.join(root, file)
|
|
47
79
|
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
|
48
|
-
|
|
49
|
-
spec.loader
|
|
50
|
-
|
|
80
|
+
assert spec is not None # only for mypy
|
|
81
|
+
assert spec.loader is not None # only for mypy
|
|
82
|
+
module = importlib.util.module_from_spec(spec)
|
|
83
|
+
spec.loader.exec_module(module)
|
|
51
84
|
functions = inspect.getmembers(module, inspect.isfunction)
|
|
52
85
|
for name, func in functions:
|
|
53
|
-
|
|
54
|
-
return
|
|
86
|
+
function_name_object__map[name] = func
|
|
87
|
+
return function_name_object__map
|
|
55
88
|
|
|
56
89
|
@staticmethod
|
|
57
|
-
def
|
|
90
|
+
def load_flask_app(api_sdk_module: str) -> Flask:
|
|
58
91
|
module = importlib.import_module(api_sdk_module)
|
|
59
92
|
return module.flask_app
|
|
60
93
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
options[arg] = "[{0}]".format(arg)
|
|
74
|
-
methods = ','.join([method for method in rule.methods if method not in ('HEAD', 'OPTIONS')]) # type: ignore
|
|
75
|
-
url = url_for(rule.endpoint, **options).replace('%5B', '[').replace('%5D', ']')
|
|
76
|
-
func_obj = current_app.view_functions[rule.endpoint]
|
|
77
|
-
if not func_obj.__module__.startswith(self.api_module):
|
|
94
|
+
@staticmethod
|
|
95
|
+
def find_called_functions_in_api(api_function_object: Callable[..., Any]) -> List[Any]:
|
|
96
|
+
calls = []
|
|
97
|
+
source = inspect.getsource(api_function_object)
|
|
98
|
+
tree = ast.parse(source)
|
|
99
|
+
for node in ast.walk(tree):
|
|
100
|
+
if isinstance(node, ast.Call):
|
|
101
|
+
if isinstance(node.func, ast.Name):
|
|
102
|
+
calls.append(node.func.id)
|
|
103
|
+
elif isinstance(node.func, ast.Attribute):
|
|
104
|
+
calls.append(node.func.attr)
|
|
105
|
+
else:
|
|
78
106
|
continue
|
|
79
|
-
|
|
80
|
-
source = inspect.getsource(func_obj)
|
|
81
|
-
tree = ast.parse(source)
|
|
82
|
-
calls = []
|
|
83
|
-
for node in ast.walk(tree):
|
|
84
|
-
if isinstance(node, ast.Call):
|
|
85
|
-
if isinstance(node.func, ast.Name):
|
|
86
|
-
calls.append(node.func.id)
|
|
87
|
-
elif isinstance(node.func, ast.Attribute):
|
|
88
|
-
calls.append(node.func.attr)
|
|
89
|
-
else:
|
|
90
|
-
pass
|
|
107
|
+
return calls
|
|
91
108
|
|
|
92
|
-
|
|
93
|
-
|
|
109
|
+
def generate_documentation(
|
|
110
|
+
self,
|
|
111
|
+
func_object: Callable[..., Any],
|
|
112
|
+
file_object: TextIO,
|
|
113
|
+
*,
|
|
114
|
+
api_route_id: int,
|
|
115
|
+
api_route_path: str,
|
|
116
|
+
api_route_methods: str,
|
|
117
|
+
loaded_db_helper_functions: Dict[str, Callable[..., Any]],
|
|
118
|
+
loaded_db_utils_functions: Dict[str, Callable[..., Any]],
|
|
119
|
+
loaded_api_utils_functions: Dict[str, Callable[..., Any]],
|
|
120
|
+
) -> None:
|
|
121
|
+
func_name = func_object.__name__
|
|
122
|
+
functions_called_in_api_route = self.find_called_functions_in_api(func_object)
|
|
123
|
+
docstring = inspect.getdoc(func_object)
|
|
124
|
+
api_docstring = 'None' if docstring is None else docstring
|
|
94
125
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
126
|
+
file_object.write(f"## {api_route_id} Путь апи {api_route_path}\n\n")
|
|
127
|
+
file_object.write(f"#### Имя функции апи: {func_name}\n")
|
|
128
|
+
file_object.write(f"### Апи методы: {api_route_methods}\n\n")
|
|
129
|
+
file_object.write("**Описание апи метода:** \n\n")
|
|
130
|
+
file_object.write(f"```python\n{api_docstring}\n```\n")
|
|
131
|
+
helper_call = 1
|
|
132
|
+
for function_called_in_api_route in functions_called_in_api_route:
|
|
133
|
+
if function_called_in_api_route not in ('transaction_commit', 'and_', 'or_', 'foreign', 'query_soft_delete', 'ensure_db_object_exists', 'db_search'):
|
|
134
|
+
if function_called_in_api_route in loaded_db_helper_functions:
|
|
135
|
+
helper_func_obj = loaded_db_helper_functions[function_called_in_api_route]
|
|
136
|
+
helper_docstring = inspect.getdoc(helper_func_obj)
|
|
137
|
+
helper_docstring = 'None' if helper_docstring is None else helper_docstring
|
|
107
138
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
139
|
+
file_object.write(f"### {api_route_id}.{helper_call} Вызвана функция работы с БД : {function_called_in_api_route}\n")
|
|
140
|
+
file_object.write(f"**Описание функции {function_called_in_api_route}:**\n\n")
|
|
141
|
+
file_object.write(f"```python\n{helper_docstring}\n```\n")
|
|
142
|
+
helper_call += 1
|
|
143
|
+
elif function_called_in_api_route in loaded_api_utils_functions:
|
|
144
|
+
if self.include_api_utils_doc:
|
|
145
|
+
util_func_obj = loaded_api_utils_functions[function_called_in_api_route]
|
|
146
|
+
util_docstring = inspect.getdoc(util_func_obj)
|
|
147
|
+
util_docstring = 'None' if util_docstring is None else util_docstring
|
|
148
|
+
if 'db_tables_used' in util_docstring or 'db_table_used' in util_docstring:
|
|
149
|
+
file_object.write(f"### {api_route_id}.{helper_call} Вызвана функция работы с БД : {function_called_in_api_route}\n")
|
|
150
|
+
file_object.write(f"**Описание функции {function_called_in_api_route}:**\n\n")
|
|
151
|
+
file_object.write(f"```python\n{util_docstring}\n```\n")
|
|
152
|
+
helper_call += 1
|
|
153
|
+
elif function_called_in_api_route in loaded_db_utils_functions:
|
|
154
|
+
if self.include_db_utils_doc:
|
|
155
|
+
util_func_obj = loaded_db_utils_functions[function_called_in_api_route]
|
|
156
|
+
db_util_docstring = inspect.getdoc(util_func_obj)
|
|
157
|
+
db_util_docstring = 'None' if db_util_docstring is None else db_util_docstring
|
|
158
|
+
if 'db_tables_used' in db_util_docstring or 'db_table_used' in db_util_docstring:
|
|
159
|
+
file_object.write(f"### {api_route_id}.{helper_call} Вызвана функция работы с БД : {function_called_in_api_route}\n")
|
|
160
|
+
file_object.write(f"**Описание функции {function_called_in_api_route}:**\n\n")
|
|
161
|
+
file_object.write(f"```python\n{db_util_docstring}\n```\n")
|
|
111
162
|
helper_call += 1
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
util_docstring = inspect.getdoc(util_func_obj)
|
|
116
|
-
util_docstring = 'None' if util_docstring is None else util_docstring
|
|
117
|
-
if 'db_tables_used' in util_docstring or 'db_table_used' in util_docstring:
|
|
118
|
-
file.write(f"### {api_num}.{helper_call} Вызвана функция работы с БД : {call}\n")
|
|
119
|
-
file.write(f"**Описание функции {call}:**\n\n")
|
|
120
|
-
file.write(f"```python\n{util_docstring}\n```\n")
|
|
121
|
-
helper_call += 1
|
|
122
|
-
elif call in db_utils:
|
|
123
|
-
if self.include_db_utils_doc:
|
|
124
|
-
util_func_obj = db_utils[call]
|
|
125
|
-
db_util_docstring = inspect.getdoc(util_func_obj)
|
|
126
|
-
db_util_docstring = 'None' if db_util_docstring is None else db_util_docstring
|
|
127
|
-
if 'db_tables_used' in db_util_docstring or 'db_table_used' in db_util_docstring:
|
|
128
|
-
file.write(f"### {api_num}.{helper_call} Вызвана функция работы с БД : {call}\n")
|
|
129
|
-
file.write(f"**Описание функции {call}:**\n\n")
|
|
130
|
-
file.write(f"```python\n{db_util_docstring}\n```\n")
|
|
131
|
-
helper_call += 1
|
|
132
|
-
file.write('-' * 20)
|
|
133
|
-
file.write('\n\n')
|
|
134
|
-
file.write('\n\n')
|
|
163
|
+
file_object.write('-' * 20)
|
|
164
|
+
file_object.write('\n\n')
|
|
165
|
+
file_object.write('\n\n')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from markupsafe import Markup
|
|
4
|
+
from wtforms import SelectField # type: ignore
|
|
5
|
+
from wtforms.widgets.core import html_params, Select # type: ignore
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CustomLiveSearchPlaceholderSelect(Select):
|
|
9
|
+
"""
|
|
10
|
+
Renders a CUSTOM select field.
|
|
11
|
+
|
|
12
|
+
If `multiple` is True, then the `size` property should be specified on
|
|
13
|
+
rendering to make the field useful.
|
|
14
|
+
|
|
15
|
+
The field must provide an `iter_choices()` method which the widget will
|
|
16
|
+
call on rendering; this method must yield tuples of
|
|
17
|
+
`(value, label, selected)`.
|
|
18
|
+
It also must provide a `has_groups()` method which tells whether choices
|
|
19
|
+
are divided into groups, and if they do, the field must have an
|
|
20
|
+
`iter_groups()` method that yields tuples of `(label, choices)`, where
|
|
21
|
+
`choices` is an iterable of `(value, label, selected)` tuples.
|
|
22
|
+
Otherwise, `selected` is False for any option field in select item group.
|
|
23
|
+
"""
|
|
24
|
+
def __call__(self, field: SelectField, **kwargs: Any) -> Markup:
|
|
25
|
+
kwargs.setdefault("id", field.id)
|
|
26
|
+
if self.multiple:
|
|
27
|
+
kwargs["multiple"] = True
|
|
28
|
+
flags = getattr(field, "flags", {})
|
|
29
|
+
for k in dir(flags):
|
|
30
|
+
if k in self.validation_attrs and k not in kwargs:
|
|
31
|
+
kwargs[k] = getattr(flags, k)
|
|
32
|
+
|
|
33
|
+
html = ["<select data-live-search='true' data-show-subtext='true' %s>" % html_params(name=field.name, **kwargs),
|
|
34
|
+
"<option value='' disabled selected>Select something...</option>"]
|
|
35
|
+
|
|
36
|
+
if field.has_groups():
|
|
37
|
+
for group, choices in field.iter_groups():
|
|
38
|
+
html.append("<optgroup %s>" % html_params(label=group))
|
|
39
|
+
for val, label, selected in choices:
|
|
40
|
+
html.append(self.render_option(val, label, selected))
|
|
41
|
+
html.append("</optgroup>")
|
|
42
|
+
else:
|
|
43
|
+
for val, label, _ in field.iter_choices():
|
|
44
|
+
html.append(self.render_option(val, label, False))
|
|
45
|
+
html.append("</select>")
|
|
46
|
+
return Markup("".join(html))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CustomLiveSearchSelect(Select):
|
|
50
|
+
"""
|
|
51
|
+
Renders a CUSTOM select field.
|
|
52
|
+
|
|
53
|
+
If `multiple` is True, then the `size` property should be specified on
|
|
54
|
+
rendering to make the field useful.
|
|
55
|
+
|
|
56
|
+
The field must provide an `iter_choices()` method which the widget will
|
|
57
|
+
call on rendering; this method must yield tuples of
|
|
58
|
+
`(value, label, selected)`.
|
|
59
|
+
It also must provide a `has_groups()` method which tells whether choices
|
|
60
|
+
are divided into groups, and if they do, the field must have an
|
|
61
|
+
`iter_groups()` method that yields tuples of `(label, choices)`, where
|
|
62
|
+
`choices` is an iterable of `(value, label, selected)` tuples.
|
|
63
|
+
Otherwise, `selected` is False for any option field in select item group.
|
|
64
|
+
"""
|
|
65
|
+
def __call__(self, field: SelectField, **kwargs: Any) -> Markup:
|
|
66
|
+
kwargs.setdefault("id", field.id)
|
|
67
|
+
if self.multiple:
|
|
68
|
+
kwargs["multiple"] = True
|
|
69
|
+
flags = getattr(field, "flags", {})
|
|
70
|
+
for k in dir(flags):
|
|
71
|
+
if k in self.validation_attrs and k not in kwargs:
|
|
72
|
+
kwargs[k] = getattr(flags, k)
|
|
73
|
+
|
|
74
|
+
html = ["<select data-live-search='true' data-show-subtext='true' %s>" % html_params(name=field.name, **kwargs)]
|
|
75
|
+
|
|
76
|
+
if field.has_groups():
|
|
77
|
+
for group, choices in field.iter_groups():
|
|
78
|
+
html.append("<optgroup %s>" % html_params(label=group))
|
|
79
|
+
for val, label, selected in choices:
|
|
80
|
+
html.append(self.render_option(val, label, selected))
|
|
81
|
+
html.append("</optgroup>")
|
|
82
|
+
else:
|
|
83
|
+
for val, label, selected in field.iter_choices():
|
|
84
|
+
html.append(self.render_option(val, label, selected))
|
|
85
|
+
html.append("</select>")
|
|
86
|
+
return Markup("".join(html))
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Optional, Any
|
|
2
|
+
|
|
3
|
+
from wtforms import StringField # type: ignore
|
|
4
|
+
from wtforms.widgets import TextInput # type: ignore
|
|
5
|
+
from markupsafe import Markup
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CustomTextInput(TextInput):
|
|
9
|
+
"""
|
|
10
|
+
Render a single-line text input with optional input attributes ("required", "maxlength", "minlength", "pattern").
|
|
11
|
+
|
|
12
|
+
examples: TextInputCustom(required=True, min_length=5, max_length=255, pattern=r"^(d+(,d+)*)?$")
|
|
13
|
+
TextInputCustom(max_length=255, pattern=r"^(d+(,d+)*)?$")
|
|
14
|
+
TextInputCustom(required=True, pattern=r"^(d+(,d+)*)?$")
|
|
15
|
+
TextInputCustom(max_length=255)
|
|
16
|
+
In model usage: ... info={"label": "LABEL:", "widget": TextInputCustom(pattern=r"^(d+(,d+)*)?$")}
|
|
17
|
+
"""
|
|
18
|
+
validation_attrs = ["required", "maxlength", "minlength", "pattern"]
|
|
19
|
+
|
|
20
|
+
def __init__(self, required: Optional[bool] = None, max_length: Optional[int] = None, min_length: Optional[int] = None, pattern: Optional[str] = None):
|
|
21
|
+
super().__init__()
|
|
22
|
+
self.required = required
|
|
23
|
+
self.max_length = max_length
|
|
24
|
+
self.min_length = min_length
|
|
25
|
+
self.pattern = pattern
|
|
26
|
+
|
|
27
|
+
def __call__(self, field: StringField, **kwargs: Any) -> Markup:
|
|
28
|
+
kwargs.setdefault("id", field.id)
|
|
29
|
+
kwargs.setdefault("type", self.input_type)
|
|
30
|
+
if self.min_length:
|
|
31
|
+
kwargs.setdefault("minlength", self.min_length)
|
|
32
|
+
if self.max_length:
|
|
33
|
+
kwargs.setdefault("maxlength", self.max_length)
|
|
34
|
+
if self.pattern:
|
|
35
|
+
kwargs.setdefault("pattern", self.pattern)
|
|
36
|
+
if "value" not in kwargs:
|
|
37
|
+
kwargs["value"] = field._value()
|
|
38
|
+
flags = getattr(field, "flags", {})
|
|
39
|
+
for k in dir(flags):
|
|
40
|
+
if k in self.validation_attrs and k not in kwargs:
|
|
41
|
+
kwargs[k] = getattr(flags, k)
|
|
42
|
+
return Markup("<input %s>" % self.html_params(name=field.name, **kwargs))
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from typing import Type, Any, Dict, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
from ul_db_utils.modules.db import db
|
|
4
|
+
from sqlalchemy.dialects import postgresql
|
|
5
|
+
from sqlalchemy.dialects.postgresql import ARRAY
|
|
6
|
+
from wtforms_alchemy import ModelForm, ClassMap, FormGenerator # type: ignore
|
|
7
|
+
from wtforms.fields import SelectField # type: ignore
|
|
8
|
+
|
|
9
|
+
from ul_db_utils.model.base_model import BaseModel
|
|
10
|
+
|
|
11
|
+
from ul_api_utils.resources.web_forms.custom_fields.custom_checkbox_select import MultiCheckboxField
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def form_factory(model_obj: Type[BaseModel], edition: bool = False, *, extra_fields: Optional[Dict[str, Any]] = None) -> Type[ModelForm]:
|
|
15
|
+
"""
|
|
16
|
+
Returns generated model form.
|
|
17
|
+
|
|
18
|
+
Parameters:
|
|
19
|
+
model_obj (type_of(BaseModel)): model object which will use for form generation
|
|
20
|
+
edition (bool): flag to indicate creation or edition form will be generated
|
|
21
|
+
extra_fields (optional(dict(str, any))): additional fields for generated form
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Form (type_of(ModelForm)): web model form
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
class ExtraFieldsFormGenerator(FormGenerator):
|
|
28
|
+
def create_fields(self, form: Any, properties: Dict[str, Any]) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Creates fields for given form based on given model attributes.
|
|
31
|
+
|
|
32
|
+
:param form: form to attach the generated fields into
|
|
33
|
+
:param properties: model attributes to generate the form fields from
|
|
34
|
+
"""
|
|
35
|
+
super(ExtraFieldsFormGenerator, self).create_fields(form, properties)
|
|
36
|
+
|
|
37
|
+
if extra_fields:
|
|
38
|
+
for field_name, field in extra_fields.items():
|
|
39
|
+
setattr(form, field_name, field)
|
|
40
|
+
|
|
41
|
+
class Form(ModelForm):
|
|
42
|
+
"""
|
|
43
|
+
A class for representing a web form.
|
|
44
|
+
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
Attributes
|
|
48
|
+
----------
|
|
49
|
+
property_columns : dict(str, any)
|
|
50
|
+
Model property columns such as ID, USER_CREATED etc.
|
|
51
|
+
|
|
52
|
+
Methods
|
|
53
|
+
-------
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
property_columns: Dict[str, Any] = {}
|
|
58
|
+
|
|
59
|
+
def __init__(self, *args: Tuple[Any], **kwargs: Dict[str, Any]) -> None:
|
|
60
|
+
super(Form, self).__init__(*args, **kwargs)
|
|
61
|
+
self.property_columns = model_obj.get_property_columns(self._obj) if self._obj else {}
|
|
62
|
+
|
|
63
|
+
class Meta:
|
|
64
|
+
model = model_obj
|
|
65
|
+
type_map = ClassMap({postgresql.UUID: SelectField, ARRAY: MultiCheckboxField})
|
|
66
|
+
only = model_obj.get_edit_columns() if edition else model_obj.get_create_columns()
|
|
67
|
+
form_generator = ExtraFieldsFormGenerator
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def get_session(cls) -> Any:
|
|
71
|
+
return db.session
|
|
72
|
+
|
|
73
|
+
Form.__name__ = f"{model_obj.__name__}Form"
|
|
74
|
+
|
|
75
|
+
return Form
|
|
@@ -32,7 +32,7 @@ ul_api_utils/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
|
|
|
32
32
|
ul_api_utils/commands/cmd_enc_keys.py,sha256=-Tblh6lI7G6M5YVwbVQqZmXhBMiIpB3a7b0Lv1MFufk,8453
|
|
33
33
|
ul_api_utils/commands/cmd_gen_api_user_token.py,sha256=Vg7oEYHvof7DSLat9yJ_k5AYL9ZOC4Jvd38DBn5U-R0,2730
|
|
34
34
|
ul_api_utils/commands/cmd_gen_new_api_user.py,sha256=ICZbKqz2D6DRvjwtNM08rNjIlWN3qClcUQw5L8FxRBY,4549
|
|
35
|
-
ul_api_utils/commands/cmd_generate_api_docs.py,sha256=
|
|
35
|
+
ul_api_utils/commands/cmd_generate_api_docs.py,sha256=t4wIVN6cdQLN7FX7_RWxGy0D8tUSe4bs2_HNVdLrwKk,8788
|
|
36
36
|
ul_api_utils/commands/cmd_start.py,sha256=QH5hEMJCoUpI2xKqWMtcp_9CU5YgmIlizT_s9lVJGUM,3441
|
|
37
37
|
ul_api_utils/commands/cmd_worker_start.py,sha256=1tt4_mL8T8_q7i1bqnfjPSkSYlRNNNp8eJ-5rTYj36w,2593
|
|
38
38
|
ul_api_utils/commands/start/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -77,6 +77,13 @@ ul_api_utils/resources/health_check/const.py,sha256=QzVZP_ZKmVKleUACiOGjzP-v54FD
|
|
|
77
77
|
ul_api_utils/resources/health_check/health_check.py,sha256=bb_ONn3akNXxNFELIUcW6foqwiKqeinmr-Wl0fQ7Q4c,18449
|
|
78
78
|
ul_api_utils/resources/health_check/health_check_template.py,sha256=Qih-sVoFVoVxfmDYBTzwlNSicCr7zNelUJLJMnM-C_Q,2572
|
|
79
79
|
ul_api_utils/resources/health_check/resource.py,sha256=SPd9kMzBOVhFZgMVfV26bDpZya3BmwxTfOR4MZ2dL4o,4199
|
|
80
|
+
ul_api_utils/resources/web_forms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
81
|
+
ul_api_utils/resources/web_forms/uni_form.py,sha256=x71Ti_Rkb0WxXq_SkwltHMYX0BYKsoV-8DPivra0HHw,2699
|
|
82
|
+
ul_api_utils/resources/web_forms/custom_fields/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
83
|
+
ul_api_utils/resources/web_forms/custom_fields/custom_checkbox_select.py,sha256=M2k9qqSXSXjd3ajGMjK3dQwCFefNKf-AL89PSOU0GxU,158
|
|
84
|
+
ul_api_utils/resources/web_forms/custom_widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
85
|
+
ul_api_utils/resources/web_forms/custom_widgets/custom_select_widget.py,sha256=691DZl8znlcKvl03SflpZWcp_9Rs_4LvPLfjsqf2Tn4,3777
|
|
86
|
+
ul_api_utils/resources/web_forms/custom_widgets/custom_text_input_widget.py,sha256=Mn6MpFrZIHPjM-l1ytWC6yGiHsIdcfbJoZiTF9XIENc,1888
|
|
80
87
|
ul_api_utils/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
81
88
|
ul_api_utils/utils/api_encoding.py,sha256=dqXknOKgc3HBOk48zDoVOLLwc1vRFATkBY8JW_RVLCo,1450
|
|
82
89
|
ul_api_utils/utils/api_format.py,sha256=wGuk5QP6b7oyl9SJ1ebNVirzCipmyTxqjoEPh2cJzAY,2025
|
|
@@ -126,9 +133,9 @@ ul_api_utils/validators/validate_empty_object.py,sha256=3Ck_iwyJE_M5e7l6s1i88aqb
|
|
|
126
133
|
ul_api_utils/validators/validate_uuid.py,sha256=EfvlRirv2EW0Z6w3s8E8rUa9GaI8qXZkBWhnPs8NFrA,257
|
|
127
134
|
ul_api_utils/validators/__tests__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
128
135
|
ul_api_utils/validators/__tests__/test_custom_fields.py,sha256=QLZ7DFta01Z7DOK9Z5Iq4uf_CmvDkVReis-GAl_QN48,1447
|
|
129
|
-
ul_api_utils-7.
|
|
130
|
-
ul_api_utils-7.
|
|
131
|
-
ul_api_utils-7.
|
|
132
|
-
ul_api_utils-7.
|
|
133
|
-
ul_api_utils-7.
|
|
134
|
-
ul_api_utils-7.
|
|
136
|
+
ul_api_utils-7.8.0.dist-info/LICENSE,sha256=6Qo8OdcqI8aGrswJKJYhST-bYqxVQBQ3ujKdTSdq-80,1062
|
|
137
|
+
ul_api_utils-7.8.0.dist-info/METADATA,sha256=ye4_rHtf-C06Ltam7yrnIsVqoJ5IqMFZxpIQEh4xOfg,14454
|
|
138
|
+
ul_api_utils-7.8.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
|
139
|
+
ul_api_utils-7.8.0.dist-info/entry_points.txt,sha256=8tL3ySHWTyJMuV1hx1fHfN8zumDVOCOm63w3StphkXg,53
|
|
140
|
+
ul_api_utils-7.8.0.dist-info/top_level.txt,sha256=1XsW8iOSFaH4LOzDcnNyxHpHrbKU3fSn-aIAxe04jmw,21
|
|
141
|
+
ul_api_utils-7.8.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|