sapiopycommons 2024.8.15a304__tar.gz → 2024.8.19a305__tar.gz
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 sapiopycommons might be problematic. Click here for more details.
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/PKG-INFO +1 -1
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/pyproject.toml +1 -1
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/callbacks/callback_util.py +122 -25
- sapiopycommons-2024.8.19a305/src/sapiopycommons/customreport/column_builder.py +60 -0
- sapiopycommons-2024.8.19a305/src/sapiopycommons/customreport/custom_report_builder.py +125 -0
- sapiopycommons-2024.8.19a305/src/sapiopycommons/customreport/term_builder.py +296 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/datatype/attachment_util.py +15 -6
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/eln/experiment_handler.py +193 -39
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/files/complex_data_loader.py +1 -1
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/files/file_bridge.py +1 -1
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/files/file_bridge_handler.py +21 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/files/file_util.py +38 -5
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/files/file_validator.py +21 -6
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/files/file_writer.py +44 -15
- sapiopycommons-2024.8.19a305/src/sapiopycommons/general/aliases.py +173 -0
- sapiopycommons-2024.8.19a305/src/sapiopycommons/general/audit_log.py +200 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/general/popup_util.py +17 -0
- sapiopycommons-2024.8.19a305/src/sapiopycommons/general/sapio_links.py +48 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/general/time_util.py +40 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/recordmodel/record_handler.py +114 -17
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/rules/eln_rule_handler.py +29 -22
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/rules/on_save_rule_handler.py +29 -28
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/webhook/webhook_handlers.py +90 -26
- sapiopycommons-2024.8.19a305/src/sapiopycommons/webhook/webservice_handlers.py +67 -0
- sapiopycommons-2024.8.19a305/tests/_do_not_add_init_py_here +0 -0
- sapiopycommons-2024.8.15a304/src/sapiopycommons/general/aliases.py +0 -82
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/.gitignore +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/LICENSE +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/README.md +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/__init__.py +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/callbacks/__init__.py +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/chem/IndigoMolecules.py +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/chem/Molecules.py +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/chem/__init__.py +0 -0
- {sapiopycommons-2024.8.15a304/src/sapiopycommons/datatype → sapiopycommons-2024.8.19a305/src/sapiopycommons/customreport}/__init__.py +0 -0
- {sapiopycommons-2024.8.15a304/src/sapiopycommons/eln → sapiopycommons-2024.8.19a305/src/sapiopycommons/datatype}/__init__.py +0 -0
- {sapiopycommons-2024.8.15a304/src/sapiopycommons/files → sapiopycommons-2024.8.19a305/src/sapiopycommons/eln}/__init__.py +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/eln/experiment_report_util.py +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/eln/plate_designer.py +0 -0
- {sapiopycommons-2024.8.15a304/src/sapiopycommons/general → sapiopycommons-2024.8.19a305/src/sapiopycommons/files}/__init__.py +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/files/file_data_handler.py +0 -0
- {sapiopycommons-2024.8.15a304/src/sapiopycommons/processtracking → sapiopycommons-2024.8.19a305/src/sapiopycommons/general}/__init__.py +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/general/accession_service.py +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/general/custom_report_util.py +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/general/exceptions.py +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/general/storage_util.py +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/multimodal/multimodal.py +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/multimodal/multimodal_data.py +0 -0
- {sapiopycommons-2024.8.15a304/src/sapiopycommons/recordmodel → sapiopycommons-2024.8.19a305/src/sapiopycommons/processtracking}/__init__.py +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/processtracking/endpoints.py +0 -0
- {sapiopycommons-2024.8.15a304/src/sapiopycommons/rules → sapiopycommons-2024.8.19a305/src/sapiopycommons/recordmodel}/__init__.py +0 -0
- {sapiopycommons-2024.8.15a304/src/sapiopycommons/webhook → sapiopycommons-2024.8.19a305/src/sapiopycommons/rules}/__init__.py +0 -0
- /sapiopycommons-2024.8.15a304/tests/_do_not_add_init_py_here → /sapiopycommons-2024.8.19a305/src/sapiopycommons/webhook/__init__.py +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/tests/accession_test.py +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/tests/bio_reg_test.py +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/tests/chem_test.py +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/tests/data_type_models.py +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/tests/kappa.chains.fasta +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/tests/mafft_test.py +0 -0
- {sapiopycommons-2024.8.15a304 → sapiopycommons-2024.8.19a305}/tests/test.gb +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: sapiopycommons
|
|
3
|
-
Version: 2024.8.
|
|
3
|
+
Version: 2024.8.19a305
|
|
4
4
|
Summary: Official Sapio Python API Utilities Package
|
|
5
5
|
Project-URL: Homepage, https://github.com/sapiosciences
|
|
6
6
|
Author-email: Jonathan Steck <jsteck@sapiosciences.com>, Yechen Qiao <yqiao@sapiosciences.com>
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import io
|
|
4
4
|
from typing import Any
|
|
5
|
+
from weakref import WeakValueDictionary
|
|
5
6
|
|
|
6
7
|
from sapiopylib.rest.ClientCallbackService import ClientCallback
|
|
7
8
|
from sapiopylib.rest.DataMgmtService import DataMgmtServer
|
|
@@ -15,7 +16,7 @@ from sapiopylib.rest.pojo.datatype.FieldDefinition import AbstractVeloxFieldDefi
|
|
|
15
16
|
from sapiopylib.rest.pojo.webhook.ClientCallbackRequest import OptionDialogRequest, ListDialogRequest, \
|
|
16
17
|
FormEntryDialogRequest, InputDialogCriteria, TableEntryDialogRequest, ESigningRequestPojo, \
|
|
17
18
|
DataRecordDialogRequest, InputSelectionRequest, FilePromptRequest, MultiFilePromptRequest, \
|
|
18
|
-
TempTableSelectionRequest
|
|
19
|
+
TempTableSelectionRequest, DisplayPopupRequest, PopupType
|
|
19
20
|
from sapiopylib.rest.pojo.webhook.ClientCallbackResult import ESigningResponsePojo
|
|
20
21
|
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
21
22
|
from sapiopylib.rest.pojo.webhook.WebhookEnums import FormAccessLevel, ScanToSelectCriteria, SearchType
|
|
@@ -24,6 +25,7 @@ from sapiopylib.rest.utils.FormBuilder import FormBuilder
|
|
|
24
25
|
from sapiopylib.rest.utils.recorddatasinks import InMemoryRecordDataSink
|
|
25
26
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
|
|
26
27
|
|
|
28
|
+
from sapiopycommons.files.file_util import FileUtil
|
|
27
29
|
from sapiopycommons.general.aliases import FieldMap, SapioRecord, AliasUtil, RecordIdentifier
|
|
28
30
|
from sapiopycommons.general.custom_report_util import CustomReportUtil
|
|
29
31
|
from sapiopycommons.general.exceptions import SapioUserCancelledException, SapioException, SapioUserErrorException
|
|
@@ -37,26 +39,86 @@ class CallbackUtil:
|
|
|
37
39
|
width_pixels: int | None
|
|
38
40
|
width_percent: float | None
|
|
39
41
|
|
|
42
|
+
__instances: WeakValueDictionary[SapioUser, CallbackUtil] = WeakValueDictionary()
|
|
43
|
+
__initialized: bool
|
|
44
|
+
|
|
45
|
+
def __new__(cls, context: SapioWebhookContext | SapioUser):
|
|
46
|
+
"""
|
|
47
|
+
:param context: The current webhook context or a user object to send requests from.
|
|
48
|
+
"""
|
|
49
|
+
user = context if isinstance(context, SapioUser) else context.user
|
|
50
|
+
obj = cls.__instances.get(user)
|
|
51
|
+
if not obj:
|
|
52
|
+
obj = object.__new__(cls)
|
|
53
|
+
obj.__initialized = False
|
|
54
|
+
cls.__instances[user] = obj
|
|
55
|
+
return obj
|
|
56
|
+
|
|
40
57
|
def __init__(self, context: SapioWebhookContext | SapioUser):
|
|
41
58
|
"""
|
|
42
59
|
:param context: The current webhook context or a user object to send requests from.
|
|
43
60
|
"""
|
|
61
|
+
if self.__initialized:
|
|
62
|
+
return
|
|
63
|
+
self.__initialized = True
|
|
64
|
+
|
|
44
65
|
self.user = context if isinstance(context, SapioUser) else context.user
|
|
45
66
|
self.callback = DataMgmtServer.get_client_callback(self.user)
|
|
46
67
|
self.dt_cache = DataTypeCacheManager(self.user)
|
|
47
68
|
self.width_pixels = None
|
|
48
69
|
self.width_percent = None
|
|
49
70
|
|
|
50
|
-
def set_dialog_width(self, width_pixels: int | None, width_percent: float | None):
|
|
71
|
+
def set_dialog_width(self, width_pixels: int | None = None, width_percent: float | None = None):
|
|
51
72
|
"""
|
|
52
73
|
Set the width that dialogs will appear as for those dialogs that support specifying their width.
|
|
53
74
|
|
|
54
75
|
:param width_pixels: The number of pixels wide that dialogs will appear as.
|
|
55
|
-
:param width_percent: The percentage of the client's screen width that dialogs
|
|
76
|
+
:param width_percent: The percentage (as a value between 0 and 1) of the client's screen width that dialogs
|
|
77
|
+
will appear as.
|
|
56
78
|
"""
|
|
79
|
+
if width_pixels is not None and width_percent is not None:
|
|
80
|
+
raise SapioException("Cannot set both width_pixels and width_percent at once.")
|
|
57
81
|
self.width_pixels = width_pixels
|
|
58
82
|
self.width_percent = width_percent
|
|
59
|
-
|
|
83
|
+
|
|
84
|
+
def toaster_popup(self, message: str, title: str = "", popup_type: PopupType = PopupType.Info) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Display a toaster popup in the bottom right corner of the user's screen.
|
|
87
|
+
|
|
88
|
+
:param message: The message to display in the toaster.
|
|
89
|
+
:param title: The title to display at the top of the toaster.
|
|
90
|
+
:param popup_type: The popup type to use for the toaster. This controls the color that the toaster appears with.
|
|
91
|
+
Info is blue, Success is green, Warning is yellow, and Error is red
|
|
92
|
+
"""
|
|
93
|
+
self.callback.display_popup(DisplayPopupRequest(title, message, popup_type))
|
|
94
|
+
|
|
95
|
+
def display_info(self, message: str) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Display an info message to the user in a dialog. Repeated calls to this function will append the new messages
|
|
98
|
+
to the same dialog if it is still opened by the user.
|
|
99
|
+
|
|
100
|
+
:param message: The message to display to the user.
|
|
101
|
+
"""
|
|
102
|
+
self.callback.display_info(message)
|
|
103
|
+
|
|
104
|
+
def display_warning(self, message: str) -> None:
|
|
105
|
+
"""
|
|
106
|
+
Display a warning message to the user in a dialog. Repeated calls to this function will append the new messages
|
|
107
|
+
to the same dialog if it is still opened by the user.
|
|
108
|
+
|
|
109
|
+
:param message: The message to display to the user.
|
|
110
|
+
"""
|
|
111
|
+
self.callback.display_warning(message)
|
|
112
|
+
|
|
113
|
+
def display_error(self, message: str) -> None:
|
|
114
|
+
"""
|
|
115
|
+
Display an error message to the user in a dialog. Repeated calls to this function will append the new messages
|
|
116
|
+
to the same dialog if it is still opened by the user.
|
|
117
|
+
|
|
118
|
+
:param message: The message to display to the user.
|
|
119
|
+
"""
|
|
120
|
+
self.callback.display_error(message)
|
|
121
|
+
|
|
60
122
|
def option_dialog(self, title: str, msg: str, options: list[str], default_option: int = 0,
|
|
61
123
|
user_can_cancel: bool = False) -> str:
|
|
62
124
|
"""
|
|
@@ -71,7 +133,8 @@ class CallbackUtil:
|
|
|
71
133
|
SapioUserCancelledException is thrown.
|
|
72
134
|
:return: The name of the button that the user selected.
|
|
73
135
|
"""
|
|
74
|
-
request = OptionDialogRequest(title, msg, options, default_option, user_can_cancel
|
|
136
|
+
request = OptionDialogRequest(title, msg, options, default_option, user_can_cancel,
|
|
137
|
+
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
75
138
|
response: int | None = self.callback.show_option_dialog(request)
|
|
76
139
|
if response is None:
|
|
77
140
|
raise SapioUserCancelledException()
|
|
@@ -111,16 +174,20 @@ class CallbackUtil:
|
|
|
111
174
|
"""
|
|
112
175
|
return self.option_dialog(title, msg, ["Yes", "No"], 0 if default_yes else 1, False) == "Yes"
|
|
113
176
|
|
|
114
|
-
def list_dialog(self, title: str, options: list[str], multi_select: bool = False
|
|
177
|
+
def list_dialog(self, title: str, options: list[str], multi_select: bool = False,
|
|
178
|
+
preselected_values: list[str] | None = None) -> list[str]:
|
|
115
179
|
"""
|
|
116
180
|
Create a list dialog with the given options for the user to choose from.
|
|
117
181
|
|
|
118
182
|
:param title: The title of the dialog.
|
|
119
183
|
:param options: The list options that the user has to choose from.
|
|
120
184
|
:param multi_select: Whether the user is able to select multiple options from the list.
|
|
185
|
+
:param preselected_values: A list of values that will already be selected when the list dialog is created. The
|
|
186
|
+
user can unselect these values if they want to.
|
|
121
187
|
:return: The list of options that the user selected.
|
|
122
188
|
"""
|
|
123
|
-
request = ListDialogRequest(title, multi_select, options
|
|
189
|
+
request = ListDialogRequest(title, multi_select, options, preselected_values,
|
|
190
|
+
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
124
191
|
response: list[str] | None = self.callback.show_list_dialog(request)
|
|
125
192
|
if response is None:
|
|
126
193
|
raise SapioUserCancelledException()
|
|
@@ -163,8 +230,6 @@ class CallbackUtil:
|
|
|
163
230
|
builder = FormBuilder(data_type, display_name, plural_display_name)
|
|
164
231
|
for field_def in fields:
|
|
165
232
|
field_name = field_def.data_field_name
|
|
166
|
-
if values and hasattr(field_def, "default_value"):
|
|
167
|
-
field_def.default_value = values.get(field_name)
|
|
168
233
|
column: int = 0
|
|
169
234
|
span: int = 4
|
|
170
235
|
if column_positions and field_name in column_positions:
|
|
@@ -173,7 +238,8 @@ class CallbackUtil:
|
|
|
173
238
|
span = position[1]
|
|
174
239
|
builder.add_field(field_def, column, span)
|
|
175
240
|
|
|
176
|
-
request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type()
|
|
241
|
+
request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
|
|
242
|
+
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
177
243
|
response: FieldMap | None = self.callback.show_form_entry_dialog(request)
|
|
178
244
|
if response is None:
|
|
179
245
|
raise SapioUserCancelledException()
|
|
@@ -215,13 +281,13 @@ class CallbackUtil:
|
|
|
215
281
|
modifier = FieldModifier(visible=True, editable=editable)
|
|
216
282
|
|
|
217
283
|
# Build the form using only those fields that are desired.
|
|
284
|
+
values: dict[str, Any] = {}
|
|
218
285
|
builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
|
|
219
286
|
for field_name in fields:
|
|
220
287
|
field_def = field_defs.get(field_name)
|
|
221
288
|
if field_def is None:
|
|
222
289
|
raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
|
|
223
|
-
|
|
224
|
-
field_def.default_value = record.get_field_value(field_name)
|
|
290
|
+
values[field_name] = record.get_field_value(field_name)
|
|
225
291
|
column: int = 0
|
|
226
292
|
span: int = 4
|
|
227
293
|
if column_positions and field_name in column_positions:
|
|
@@ -230,7 +296,8 @@ class CallbackUtil:
|
|
|
230
296
|
span = position[1]
|
|
231
297
|
builder.add_field(modifier.modify_field(field_def), column, span)
|
|
232
298
|
|
|
233
|
-
request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type()
|
|
299
|
+
request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
|
|
300
|
+
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
234
301
|
response: FieldMap | None = self.callback.show_form_entry_dialog(request)
|
|
235
302
|
if response is None:
|
|
236
303
|
raise SapioUserCancelledException()
|
|
@@ -245,7 +312,8 @@ class CallbackUtil:
|
|
|
245
312
|
:param field: The definition for a field that the user must provide input to.
|
|
246
313
|
:return: The response value from the user for the given field.
|
|
247
314
|
"""
|
|
248
|
-
request = InputDialogCriteria(title, msg, field,
|
|
315
|
+
request = InputDialogCriteria(title, msg, field,
|
|
316
|
+
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
249
317
|
response: Any | None = self.callback.show_input_dialog(request)
|
|
250
318
|
if response is None:
|
|
251
319
|
raise SapioUserCancelledException()
|
|
@@ -322,6 +390,8 @@ class CallbackUtil:
|
|
|
322
390
|
msg: str,
|
|
323
391
|
fields: list[AbstractVeloxFieldDefinition],
|
|
324
392
|
values: list[FieldMap],
|
|
393
|
+
group_by: str | None = None,
|
|
394
|
+
image_data: list[bytes] | None = None,
|
|
325
395
|
*,
|
|
326
396
|
data_type: str = "Default",
|
|
327
397
|
display_name: str | None = None,
|
|
@@ -335,6 +405,10 @@ class CallbackUtil:
|
|
|
335
405
|
:param fields: The definitions of the fields to display as table columns. Fields will be displayed in the order
|
|
336
406
|
they are provided in this list.
|
|
337
407
|
:param values: The values to set for each row of the table.
|
|
408
|
+
:param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
|
|
409
|
+
The user may remove this grouping if they want to.
|
|
410
|
+
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
411
|
+
the image data list corresponds to the element at the same index in the values list.
|
|
338
412
|
:param data_type: The data type name for the temporary data type that will be created for this table.
|
|
339
413
|
:param display_name: The display name for the temporary data type. If not provided, defaults to the data type
|
|
340
414
|
name.
|
|
@@ -356,7 +430,9 @@ class CallbackUtil:
|
|
|
356
430
|
for field in fields:
|
|
357
431
|
builder.add_field(modifier.modify_field(field))
|
|
358
432
|
|
|
359
|
-
request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values
|
|
433
|
+
request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
|
|
434
|
+
record_image_data_list=image_data, group_by_field=group_by,
|
|
435
|
+
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
360
436
|
response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
|
|
361
437
|
if response is None:
|
|
362
438
|
raise SapioUserCancelledException()
|
|
@@ -367,7 +443,9 @@ class CallbackUtil:
|
|
|
367
443
|
msg: str,
|
|
368
444
|
fields: list[str],
|
|
369
445
|
records: list[SapioRecord],
|
|
370
|
-
editable: bool | None = True
|
|
446
|
+
editable: bool | None = True,
|
|
447
|
+
group_by: str | None = None,
|
|
448
|
+
image_data: list[bytes] | None = None) -> list[FieldMap]:
|
|
371
449
|
"""
|
|
372
450
|
Create a table dialog where the user may input data into the fields of the table. The table is constructed from
|
|
373
451
|
a given list of records of a singular type. Provided field names must match fields on the definition of the data
|
|
@@ -384,6 +462,10 @@ class CallbackUtil:
|
|
|
384
462
|
they are provided in this list.
|
|
385
463
|
:param editable: If true, all fields are displayed as editable. If false, all fields are displayed as
|
|
386
464
|
uneditable. If none, only those fields that are defined as editable by the data designer will be editable.
|
|
465
|
+
:param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
|
|
466
|
+
The user may remove this grouping if they want to.
|
|
467
|
+
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
468
|
+
the image data list corresponds to the element at the same index in the records list.
|
|
387
469
|
:return: A list of dictionaries mapping the data field names of the given field definitions to the response
|
|
388
470
|
value from the user for that field for each row.
|
|
389
471
|
"""
|
|
@@ -410,7 +492,9 @@ class CallbackUtil:
|
|
|
410
492
|
raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
|
|
411
493
|
builder.add_field(modifier.modify_field(field_def))
|
|
412
494
|
|
|
413
|
-
request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), field_map_list
|
|
495
|
+
request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), field_map_list,
|
|
496
|
+
record_image_data_list=image_data, group_by_field=group_by,
|
|
497
|
+
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
414
498
|
response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
|
|
415
499
|
if response is None:
|
|
416
500
|
raise SapioUserCancelledException()
|
|
@@ -567,7 +651,8 @@ class CallbackUtil:
|
|
|
567
651
|
for field in final_fields:
|
|
568
652
|
builder.add_field(field)
|
|
569
653
|
|
|
570
|
-
request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values
|
|
654
|
+
request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
|
|
655
|
+
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
571
656
|
response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
|
|
572
657
|
if response is None:
|
|
573
658
|
raise SapioUserCancelledException()
|
|
@@ -616,7 +701,8 @@ class CallbackUtil:
|
|
|
616
701
|
raise SapioException(f"The data type \"{data_type}\" does not have a layout by the name "
|
|
617
702
|
f"\"{layout_name}\" in the system.")
|
|
618
703
|
|
|
619
|
-
request = DataRecordDialogRequest(title, record, layout, minimized, access_level, plugin_path_list
|
|
704
|
+
request = DataRecordDialogRequest(title, record, layout, minimized, access_level, plugin_path_list,
|
|
705
|
+
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
620
706
|
response: bool = self.callback.data_record_form_view_dialog(request)
|
|
621
707
|
if not response:
|
|
622
708
|
raise SapioUserCancelledException()
|
|
@@ -663,7 +749,7 @@ class CallbackUtil:
|
|
|
663
749
|
return response
|
|
664
750
|
|
|
665
751
|
def record_selection_dialog(self, msg: str, fields: list[str], records: list[SapioRecord],
|
|
666
|
-
multi_select: bool = True) -> list[
|
|
752
|
+
multi_select: bool = True) -> list[SapioRecord]:
|
|
667
753
|
"""
|
|
668
754
|
Create a record selection dialog for a list of records for the user to choose from. Provided field names must
|
|
669
755
|
match fields on the definition of the data type of the given records.
|
|
@@ -676,7 +762,7 @@ class CallbackUtil:
|
|
|
676
762
|
they are provided in this list.
|
|
677
763
|
:param records: The records to display as rows in the table.
|
|
678
764
|
:param multi_select: Whether the user is able to select multiple records from the list.
|
|
679
|
-
:return: A list of
|
|
765
|
+
:return: A list of the selected records.
|
|
680
766
|
"""
|
|
681
767
|
data_types: set[str] = {x.data_type_name for x in records}
|
|
682
768
|
if len(data_types) > 1:
|
|
@@ -774,7 +860,7 @@ class CallbackUtil:
|
|
|
774
860
|
|
|
775
861
|
# If CustomReportCriteria was provided, it must be wrapped as a CustomReport.
|
|
776
862
|
if isinstance(custom_search, CustomReportCriteria):
|
|
777
|
-
custom_search: CustomReport = CustomReport(False,
|
|
863
|
+
custom_search: CustomReport = CustomReport(False, [], custom_search)
|
|
778
864
|
# If a string was provided, locate the report criteria for the predefined search in the system matching this
|
|
779
865
|
# name.
|
|
780
866
|
if isinstance(custom_search, str):
|
|
@@ -807,7 +893,8 @@ class CallbackUtil:
|
|
|
807
893
|
for field in additional_fields:
|
|
808
894
|
builder.add_field(field)
|
|
809
895
|
temp_dt = builder.get_temporary_data_type()
|
|
810
|
-
request = ESigningRequestPojo(title, msg, show_comment, temp_dt
|
|
896
|
+
request = ESigningRequestPojo(title, msg, show_comment, temp_dt,
|
|
897
|
+
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
811
898
|
response: ESigningResponsePojo | None = self.callback.show_esign_dialog(request)
|
|
812
899
|
if response is None:
|
|
813
900
|
raise SapioUserCancelledException()
|
|
@@ -892,7 +979,7 @@ class CallbackUtil:
|
|
|
892
979
|
if len(allowed_extensions) != 0:
|
|
893
980
|
matches: bool = False
|
|
894
981
|
for ext in allowed_extensions:
|
|
895
|
-
if file_path.endswith("." + ext):
|
|
982
|
+
if file_path.endswith("." + ext.lstrip(".")):
|
|
896
983
|
matches = True
|
|
897
984
|
break
|
|
898
985
|
if matches is False:
|
|
@@ -906,9 +993,19 @@ class CallbackUtil:
|
|
|
906
993
|
:param file_name: The name of the file.
|
|
907
994
|
:param file_data: The data of the file, provided as either a string or as a bytes array.
|
|
908
995
|
"""
|
|
909
|
-
data = io.
|
|
996
|
+
data = io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data)
|
|
910
997
|
self.callback.send_file(file_name, False, data)
|
|
911
998
|
|
|
999
|
+
def write_zip_file(self, zip_name: str, files: dict[str, str | bytes]) -> None:
|
|
1000
|
+
"""
|
|
1001
|
+
Send a collection of files to the user in a zip file.
|
|
1002
|
+
|
|
1003
|
+
:param zip_name: The name of the zip file.
|
|
1004
|
+
:param files: A dictionary of the files to add to the zip file.
|
|
1005
|
+
"""
|
|
1006
|
+
data = io.BytesIO(FileUtil.zip_files(files))
|
|
1007
|
+
self.callback.send_file(zip_name, False, data)
|
|
1008
|
+
|
|
912
1009
|
|
|
913
1010
|
class FieldModifier:
|
|
914
1011
|
"""
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from sapiopylib.rest.pojo.CustomReport import ReportColumn
|
|
2
|
+
from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
|
|
3
|
+
|
|
4
|
+
from sapiopycommons.general.aliases import DataTypeIdentifier, FieldIdentifier, AliasUtil
|
|
5
|
+
from sapiopycommons.general.exceptions import SapioException
|
|
6
|
+
|
|
7
|
+
# The system fields that every record has and their field types. System fields aren't generated as record model fields
|
|
8
|
+
# for all platform version, hence the need to create a dict for them in the off chance that they're not present on
|
|
9
|
+
# the model wrapper.
|
|
10
|
+
SYSTEM_FIELDS: dict[str, FieldType] = {
|
|
11
|
+
"DataRecordName": FieldType.IDENTIFIER,
|
|
12
|
+
"RecordId": FieldType.LONG,
|
|
13
|
+
"DateCreated": FieldType.DATE,
|
|
14
|
+
"CreatedBy": FieldType.STRING,
|
|
15
|
+
"VeloxLastModifiedDate": FieldType.DATE,
|
|
16
|
+
"VeloxLastModifiedBy": FieldType.STRING
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ColumnBuilder:
|
|
21
|
+
"""
|
|
22
|
+
A class for building report columns for custom reports.
|
|
23
|
+
"""
|
|
24
|
+
@staticmethod
|
|
25
|
+
def build_column(data_type: DataTypeIdentifier, field: FieldIdentifier, field_type: FieldType | None = None) \
|
|
26
|
+
-> ReportColumn:
|
|
27
|
+
"""
|
|
28
|
+
Build a ReportColumn from a variety of possible inputs.
|
|
29
|
+
|
|
30
|
+
:param data_type: An object that can be used to identify a data type.
|
|
31
|
+
:param field: An object that can be used to identify a data field.
|
|
32
|
+
:param field_type: The field type of the provided field. This is only required if the field type cannot be
|
|
33
|
+
determined from the given data type and field, which occurs when the given field is a string and the
|
|
34
|
+
given data type is not a wrapped record model or record model wrapper.
|
|
35
|
+
:return: A ReportColumn for the inputs.
|
|
36
|
+
"""
|
|
37
|
+
# Get the data type and field names from the inputs.
|
|
38
|
+
data_type_name = AliasUtil.to_data_type_name(data_type)
|
|
39
|
+
field_name = AliasUtil.to_data_field_name(field)
|
|
40
|
+
if field_type is None:
|
|
41
|
+
field_type = ColumnBuilder.__field_type(data_type, field)
|
|
42
|
+
if field_type is None:
|
|
43
|
+
raise SapioException("The field_type parameter is required for the provided data_type and field inputs.")
|
|
44
|
+
return ReportColumn(data_type_name, field_name, field_type)
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def __field_type(data_type: DataTypeIdentifier, field: FieldIdentifier) -> FieldType | None:
|
|
48
|
+
"""
|
|
49
|
+
Given a record model wrapper and a field name, return the field type for that field. Accounts for system fields.
|
|
50
|
+
|
|
51
|
+
:param data_type: The record model wrapper that the field is on.
|
|
52
|
+
:param field: The field name to return the type of.
|
|
53
|
+
:return: The field type of the given field name.
|
|
54
|
+
"""
|
|
55
|
+
# Check if the field name is a system field. If it us, use the field type defined in this file.
|
|
56
|
+
field_name: str = AliasUtil.to_data_field_name(field)
|
|
57
|
+
if field_name in SYSTEM_FIELDS:
|
|
58
|
+
return SYSTEM_FIELDS.get(field_name)
|
|
59
|
+
# Otherwise, check if the field type can be found from the wrapper.
|
|
60
|
+
return AliasUtil.to_field_type(field_name, data_type)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from sapiopylib.rest.pojo.CustomReport import ReportColumn, CustomReportCriteria, AbstractReportTerm, \
|
|
2
|
+
ExplicitJoinDefinition, RelatedRecordCriteria, QueryRestriction, FieldCompareReportTerm
|
|
3
|
+
from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
|
|
4
|
+
|
|
5
|
+
from sapiopycommons.customreport.column_builder import ColumnBuilder
|
|
6
|
+
from sapiopycommons.general.aliases import DataTypeIdentifier, FieldIdentifier, AliasUtil, SapioRecord
|
|
7
|
+
from sapiopycommons.general.exceptions import SapioException
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CustomReportBuilder:
|
|
11
|
+
"""
|
|
12
|
+
A class used for building custom reports. Look into using the TermBuilder and ColumnBuilder classes for building
|
|
13
|
+
parts of a custom report.
|
|
14
|
+
"""
|
|
15
|
+
root_data_type: DataTypeIdentifier
|
|
16
|
+
data_type_name: str
|
|
17
|
+
root_term: AbstractReportTerm | None
|
|
18
|
+
record_criteria: RelatedRecordCriteria
|
|
19
|
+
column_list: list[ReportColumn]
|
|
20
|
+
join_list: list[ExplicitJoinDefinition]
|
|
21
|
+
|
|
22
|
+
def __init__(self, root_data_type: DataTypeIdentifier):
|
|
23
|
+
"""
|
|
24
|
+
:param root_data_type: An object that can be used to identify a data type name. Used as the root data type name
|
|
25
|
+
of this search.
|
|
26
|
+
"""
|
|
27
|
+
self.root_data_type = root_data_type
|
|
28
|
+
self.data_type_name = AliasUtil.to_data_type_name(root_data_type)
|
|
29
|
+
self.root_term = None
|
|
30
|
+
self.record_criteria = RelatedRecordCriteria(QueryRestriction.QUERY_ALL)
|
|
31
|
+
self.column_list = []
|
|
32
|
+
self.join_list = []
|
|
33
|
+
|
|
34
|
+
def has_root_term(self) -> bool:
|
|
35
|
+
"""
|
|
36
|
+
:return: Whether this report builder has had its root term set.
|
|
37
|
+
"""
|
|
38
|
+
return self.root_term is not None
|
|
39
|
+
|
|
40
|
+
def set_root_term(self, term: AbstractReportTerm) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Set the root term of the report. Use the TermBuilder class to construct the report terms.
|
|
43
|
+
|
|
44
|
+
:param term: The term to set as the root term.
|
|
45
|
+
"""
|
|
46
|
+
self.root_term = term
|
|
47
|
+
|
|
48
|
+
def has_columns(self) -> bool:
|
|
49
|
+
"""
|
|
50
|
+
:return: Whether this report builder has any report columns.
|
|
51
|
+
"""
|
|
52
|
+
return bool(self.column_list)
|
|
53
|
+
|
|
54
|
+
def add_column(self, field: FieldIdentifier, field_type: FieldType = None,
|
|
55
|
+
*, data_type: DataTypeIdentifier | None = None) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Add a column to this report builder.
|
|
58
|
+
|
|
59
|
+
:param field: An object that can be used to identify a data field.
|
|
60
|
+
:param field_type: The field type of the provided field. This is only required if the field type cannot be
|
|
61
|
+
determined from the given data type and field, which occurs when the given field is a string and the
|
|
62
|
+
given data type is not a wrapped record model or record model wrapper.
|
|
63
|
+
:param data_type: An object that can be used to identify a data type. If not provided, uses the root data type
|
|
64
|
+
provided when this builder was initialized. You'll only want to specify this value when adding a column
|
|
65
|
+
that is from a different data type than the root data type.
|
|
66
|
+
"""
|
|
67
|
+
if data_type is None:
|
|
68
|
+
data_type = self.root_data_type
|
|
69
|
+
self.column_list.append(ColumnBuilder.build_column(data_type, field, field_type))
|
|
70
|
+
|
|
71
|
+
def add_columns(self, fields: list[FieldIdentifier], *, data_type: DataTypeIdentifier | None = None) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Add columns to this report builder.
|
|
74
|
+
|
|
75
|
+
:param fields: A list of objects that can be used to identify data fields.
|
|
76
|
+
:param data_type: An object that can be used to identify a data type. If not provided, uses the root data type
|
|
77
|
+
provided when this builder was initialized. You'll only want to specify this value when adding a column
|
|
78
|
+
that is from a different data type than the root data type.
|
|
79
|
+
"""
|
|
80
|
+
for field in fields:
|
|
81
|
+
self.add_column(field, data_type=data_type)
|
|
82
|
+
|
|
83
|
+
def set_query_restriction(self, base_record: SapioRecord, search_related: QueryRestriction) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Set a restriction on the report for this report builder such that the returned results must be related in
|
|
86
|
+
some way to the provided base record. Without this, the report searches all records in the system that match the
|
|
87
|
+
root term.
|
|
88
|
+
|
|
89
|
+
:param base_record: The base record to run the search from.
|
|
90
|
+
:param search_related: Determine the relationship of the related records that can appear in the search, be those
|
|
91
|
+
children, parents, descendants, or ancestors.
|
|
92
|
+
"""
|
|
93
|
+
if search_related == QueryRestriction.QUERY_ALL:
|
|
94
|
+
raise SapioException("The search_related must be something other than QUERY_ALL when setting a query restriction.")
|
|
95
|
+
self.record_criteria = RelatedRecordCriteria(search_related,
|
|
96
|
+
AliasUtil.to_record_id(base_record),
|
|
97
|
+
AliasUtil.to_data_type_name(base_record))
|
|
98
|
+
|
|
99
|
+
def add_join(self, comparison_term: FieldCompareReportTerm) -> None:
|
|
100
|
+
"""
|
|
101
|
+
Add a join statement to this report builder.
|
|
102
|
+
|
|
103
|
+
:param comparison_term: The field comparison term to join with. The left side data type name of this term will
|
|
104
|
+
be the data type that is joined against.
|
|
105
|
+
"""
|
|
106
|
+
self.join_list.append(ExplicitJoinDefinition(comparison_term.left_data_type_name, comparison_term))
|
|
107
|
+
|
|
108
|
+
def build_report_criteria(self, page_size: int = 0, page_number: int = -1, case_sensitive: bool = False,
|
|
109
|
+
owner_restriction_set: list[str] = None) -> CustomReportCriteria:
|
|
110
|
+
"""
|
|
111
|
+
Generate a CustomReportCriteria using the column list, root term, and root data type from this report builder.
|
|
112
|
+
You can use the CustomReportManager or CustomReportUtil to run the constructed report.
|
|
113
|
+
|
|
114
|
+
:param page_size: The page size of the custom report.
|
|
115
|
+
:param page_number: The page number of the current report.
|
|
116
|
+
:param case_sensitive: When searching texts, should the search be case-sensitive?
|
|
117
|
+
:param owner_restriction_set: Specifies to only return records if the record is owned by this list of usernames.
|
|
118
|
+
:return: A CustomReportCriteria from this report builder.
|
|
119
|
+
"""
|
|
120
|
+
if not self.has_root_term():
|
|
121
|
+
raise SapioException("Cannot build a report with no root term.")
|
|
122
|
+
if not self.has_columns():
|
|
123
|
+
raise SapioException("Cannot build a report with no columns.")
|
|
124
|
+
return CustomReportCriteria(self.column_list, self.root_term, self.record_criteria, self.data_type_name,
|
|
125
|
+
case_sensitive, page_size, page_number, owner_restriction_set, self.join_list)
|