sapiopycommons 2024.11.11a364__py3-none-any.whl → 2024.11.18a366__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 sapiopycommons might be problematic. Click here for more details.
- sapiopycommons/callbacks/callback_util.py +532 -83
- sapiopycommons/callbacks/field_builder.py +537 -0
- sapiopycommons/chem/IndigoMolecules.py +2 -0
- sapiopycommons/chem/Molecules.py +77 -18
- sapiopycommons/customreport/__init__.py +0 -0
- sapiopycommons/customreport/column_builder.py +60 -0
- sapiopycommons/customreport/custom_report_builder.py +130 -0
- sapiopycommons/customreport/term_builder.py +299 -0
- sapiopycommons/datatype/attachment_util.py +11 -10
- sapiopycommons/datatype/data_fields.py +61 -0
- sapiopycommons/datatype/pseudo_data_types.py +440 -0
- sapiopycommons/eln/experiment_handler.py +272 -70
- sapiopycommons/eln/experiment_report_util.py +653 -0
- sapiopycommons/files/complex_data_loader.py +5 -4
- sapiopycommons/files/file_bridge.py +31 -24
- sapiopycommons/files/file_bridge_handler.py +340 -0
- sapiopycommons/files/file_data_handler.py +2 -5
- sapiopycommons/files/file_util.py +59 -9
- sapiopycommons/files/file_validator.py +92 -6
- sapiopycommons/files/file_writer.py +44 -15
- sapiopycommons/flowcyto/flow_cyto.py +77 -0
- sapiopycommons/flowcyto/flowcyto_data.py +75 -0
- sapiopycommons/general/accession_service.py +375 -0
- sapiopycommons/general/aliases.py +207 -6
- sapiopycommons/general/audit_log.py +189 -0
- sapiopycommons/general/custom_report_util.py +212 -37
- sapiopycommons/general/exceptions.py +21 -8
- sapiopycommons/general/popup_util.py +21 -0
- sapiopycommons/general/sapio_links.py +50 -0
- sapiopycommons/general/time_util.py +8 -2
- sapiopycommons/multimodal/multimodal.py +146 -0
- sapiopycommons/multimodal/multimodal_data.py +490 -0
- sapiopycommons/processtracking/custom_workflow_handler.py +406 -0
- sapiopycommons/processtracking/endpoints.py +22 -22
- sapiopycommons/recordmodel/record_handler.py +481 -97
- sapiopycommons/rules/eln_rule_handler.py +34 -25
- sapiopycommons/rules/on_save_rule_handler.py +34 -31
- sapiopycommons/sftpconnect/__init__.py +0 -0
- sapiopycommons/sftpconnect/sftp_builder.py +69 -0
- sapiopycommons/webhook/webhook_context.py +39 -0
- sapiopycommons/webhook/webhook_handlers.py +201 -42
- sapiopycommons/webhook/webservice_handlers.py +67 -0
- {sapiopycommons-2024.11.11a364.dist-info → sapiopycommons-2024.11.18a366.dist-info}/METADATA +5 -2
- sapiopycommons-2024.11.18a366.dist-info/RECORD +59 -0
- {sapiopycommons-2024.11.11a364.dist-info → sapiopycommons-2024.11.18a366.dist-info}/WHEEL +1 -1
- sapiopycommons-2024.11.11a364.dist-info/RECORD +0 -38
- {sapiopycommons-2024.11.11a364.dist-info → sapiopycommons-2024.11.18a366.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import io
|
|
2
|
-
from
|
|
4
|
+
from weakref import WeakValueDictionary
|
|
3
5
|
|
|
6
|
+
from requests import ReadTimeout
|
|
4
7
|
from sapiopylib.rest.ClientCallbackService import ClientCallback
|
|
5
8
|
from sapiopylib.rest.DataMgmtService import DataMgmtServer
|
|
6
9
|
from sapiopylib.rest.User import SapioUser
|
|
@@ -9,22 +12,24 @@ from sapiopylib.rest.pojo.DataRecord import DataRecord
|
|
|
9
12
|
from sapiopylib.rest.pojo.datatype.DataType import DataTypeDefinition
|
|
10
13
|
from sapiopylib.rest.pojo.datatype.DataTypeLayout import DataTypeLayout
|
|
11
14
|
from sapiopylib.rest.pojo.datatype.FieldDefinition import AbstractVeloxFieldDefinition, VeloxStringFieldDefinition, \
|
|
12
|
-
VeloxIntegerFieldDefinition, VeloxDoubleFieldDefinition
|
|
15
|
+
VeloxIntegerFieldDefinition, VeloxDoubleFieldDefinition, FieldDefinitionParser
|
|
13
16
|
from sapiopylib.rest.pojo.webhook.ClientCallbackRequest import OptionDialogRequest, ListDialogRequest, \
|
|
14
17
|
FormEntryDialogRequest, InputDialogCriteria, TableEntryDialogRequest, ESigningRequestPojo, \
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
DataRecordDialogRequest, InputSelectionRequest, FilePromptRequest, MultiFilePromptRequest, \
|
|
19
|
+
TempTableSelectionRequest, DisplayPopupRequest, PopupType
|
|
17
20
|
from sapiopylib.rest.pojo.webhook.ClientCallbackResult import ESigningResponsePojo
|
|
18
|
-
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
19
21
|
from sapiopylib.rest.pojo.webhook.WebhookEnums import FormAccessLevel, ScanToSelectCriteria, SearchType
|
|
20
22
|
from sapiopylib.rest.utils.DataTypeCacheManager import DataTypeCacheManager
|
|
21
23
|
from sapiopylib.rest.utils.FormBuilder import FormBuilder
|
|
22
24
|
from sapiopylib.rest.utils.recorddatasinks import InMemoryRecordDataSink
|
|
23
25
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
|
|
24
26
|
|
|
25
|
-
from sapiopycommons.
|
|
27
|
+
from sapiopycommons.files.file_util import FileUtil
|
|
28
|
+
from sapiopycommons.general.aliases import FieldMap, SapioRecord, AliasUtil, RecordIdentifier, FieldValue, \
|
|
29
|
+
UserIdentifier
|
|
26
30
|
from sapiopycommons.general.custom_report_util import CustomReportUtil
|
|
27
|
-
from sapiopycommons.general.exceptions import SapioUserCancelledException, SapioException, SapioUserErrorException
|
|
31
|
+
from sapiopycommons.general.exceptions import SapioUserCancelledException, SapioException, SapioUserErrorException, \
|
|
32
|
+
SapioDialogTimeoutException
|
|
28
33
|
from sapiopycommons.recordmodel.record_handler import RecordHandler
|
|
29
34
|
|
|
30
35
|
|
|
@@ -32,29 +37,104 @@ class CallbackUtil:
|
|
|
32
37
|
user: SapioUser
|
|
33
38
|
callback: ClientCallback
|
|
34
39
|
dt_cache: DataTypeCacheManager
|
|
40
|
+
_original_timeout: int
|
|
41
|
+
timeout_seconds: int
|
|
35
42
|
width_pixels: int | None
|
|
36
43
|
width_percent: float | None
|
|
37
44
|
|
|
38
|
-
|
|
45
|
+
__instances: WeakValueDictionary[SapioUser, CallbackUtil] = WeakValueDictionary()
|
|
46
|
+
__initialized: bool
|
|
47
|
+
|
|
48
|
+
def __new__(cls, context: UserIdentifier):
|
|
49
|
+
"""
|
|
50
|
+
:param context: The current webhook context or a user object to send requests from.
|
|
51
|
+
"""
|
|
52
|
+
user = AliasUtil.to_sapio_user(context)
|
|
53
|
+
obj = cls.__instances.get(user)
|
|
54
|
+
if not obj:
|
|
55
|
+
obj = object.__new__(cls)
|
|
56
|
+
obj.__initialized = False
|
|
57
|
+
cls.__instances[user] = obj
|
|
58
|
+
return obj
|
|
59
|
+
|
|
60
|
+
def __init__(self, context: UserIdentifier):
|
|
39
61
|
"""
|
|
40
62
|
:param context: The current webhook context or a user object to send requests from.
|
|
41
63
|
"""
|
|
42
|
-
|
|
64
|
+
if self.__initialized:
|
|
65
|
+
return
|
|
66
|
+
self.__initialized = True
|
|
67
|
+
|
|
68
|
+
self.user = AliasUtil.to_sapio_user(context)
|
|
43
69
|
self.callback = DataMgmtServer.get_client_callback(self.user)
|
|
44
70
|
self.dt_cache = DataTypeCacheManager(self.user)
|
|
71
|
+
self._original_timeout = self.user.timeout_seconds
|
|
72
|
+
self.timeout_seconds = self.user.timeout_seconds
|
|
45
73
|
self.width_pixels = None
|
|
46
74
|
self.width_percent = None
|
|
47
75
|
|
|
48
|
-
def set_dialog_width(self, width_pixels: int | None, width_percent: float | None):
|
|
76
|
+
def set_dialog_width(self, width_pixels: int | None = None, width_percent: float | None = None):
|
|
49
77
|
"""
|
|
50
78
|
Set the width that dialogs will appear as for those dialogs that support specifying their width.
|
|
51
79
|
|
|
52
80
|
:param width_pixels: The number of pixels wide that dialogs will appear as.
|
|
53
|
-
:param width_percent: The percentage of the client's screen width that dialogs
|
|
81
|
+
:param width_percent: The percentage (as a value between 0 and 1) of the client's screen width that dialogs
|
|
82
|
+
will appear as.
|
|
54
83
|
"""
|
|
84
|
+
if width_pixels is not None and width_percent is not None:
|
|
85
|
+
raise SapioException("Cannot set both width_pixels and width_percent at once.")
|
|
55
86
|
self.width_pixels = width_pixels
|
|
56
87
|
self.width_percent = width_percent
|
|
57
|
-
|
|
88
|
+
|
|
89
|
+
def set_dialog_timeout(self, timeout: int):
|
|
90
|
+
"""
|
|
91
|
+
Alter the timeout time used for callback requests that create dialogs for the user to interact with. By default,
|
|
92
|
+
a CallbackUtil will use the timeout time of the SapioUser provided to it. By altering this, a different timeout
|
|
93
|
+
time is used.
|
|
94
|
+
|
|
95
|
+
:param timeout: The number of seconds that must elapse before a SapioDialogTimeoutException is thrown by
|
|
96
|
+
any callback that creates a dialog for the user to interact with.
|
|
97
|
+
"""
|
|
98
|
+
self.timeout_seconds = timeout
|
|
99
|
+
|
|
100
|
+
def toaster_popup(self, message: str, title: str = "", popup_type: PopupType = PopupType.Info) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Display a toaster popup in the bottom right corner of the user's screen.
|
|
103
|
+
|
|
104
|
+
:param message: The message to display in the toaster.
|
|
105
|
+
:param title: The title to display at the top of the toaster.
|
|
106
|
+
:param popup_type: The popup type to use for the toaster. This controls the color that the toaster appears with.
|
|
107
|
+
Info is blue, Success is green, Warning is yellow, and Error is red
|
|
108
|
+
"""
|
|
109
|
+
self.callback.display_popup(DisplayPopupRequest(title, message, popup_type))
|
|
110
|
+
|
|
111
|
+
def display_info(self, message: str) -> None:
|
|
112
|
+
"""
|
|
113
|
+
Display an info message to the user in a dialog. Repeated calls to this function will append the new messages
|
|
114
|
+
to the same dialog if it is still opened by the user.
|
|
115
|
+
|
|
116
|
+
:param message: The message to display to the user.
|
|
117
|
+
"""
|
|
118
|
+
self.callback.display_info(message)
|
|
119
|
+
|
|
120
|
+
def display_warning(self, message: str) -> None:
|
|
121
|
+
"""
|
|
122
|
+
Display a warning message to the user in a dialog. Repeated calls to this function will append the new messages
|
|
123
|
+
to the same dialog if it is still opened by the user.
|
|
124
|
+
|
|
125
|
+
:param message: The message to display to the user.
|
|
126
|
+
"""
|
|
127
|
+
self.callback.display_warning(message)
|
|
128
|
+
|
|
129
|
+
def display_error(self, message: str) -> None:
|
|
130
|
+
"""
|
|
131
|
+
Display an error message to the user in a dialog. Repeated calls to this function will append the new messages
|
|
132
|
+
to the same dialog if it is still opened by the user.
|
|
133
|
+
|
|
134
|
+
:param message: The message to display to the user.
|
|
135
|
+
"""
|
|
136
|
+
self.callback.display_error(message)
|
|
137
|
+
|
|
58
138
|
def option_dialog(self, title: str, msg: str, options: list[str], default_option: int = 0,
|
|
59
139
|
user_can_cancel: bool = False) -> str:
|
|
60
140
|
"""
|
|
@@ -69,8 +149,15 @@ class CallbackUtil:
|
|
|
69
149
|
SapioUserCancelledException is thrown.
|
|
70
150
|
:return: The name of the button that the user selected.
|
|
71
151
|
"""
|
|
72
|
-
request = OptionDialogRequest(title, msg, options, default_option, user_can_cancel
|
|
73
|
-
|
|
152
|
+
request = OptionDialogRequest(title, msg, options, default_option, user_can_cancel,
|
|
153
|
+
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
154
|
+
try:
|
|
155
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
156
|
+
response: int | None = self.callback.show_option_dialog(request)
|
|
157
|
+
except ReadTimeout:
|
|
158
|
+
raise SapioDialogTimeoutException()
|
|
159
|
+
finally:
|
|
160
|
+
self.user.timeout_seconds = self._original_timeout
|
|
74
161
|
if response is None:
|
|
75
162
|
raise SapioUserCancelledException()
|
|
76
163
|
return options[response]
|
|
@@ -109,17 +196,27 @@ class CallbackUtil:
|
|
|
109
196
|
"""
|
|
110
197
|
return self.option_dialog(title, msg, ["Yes", "No"], 0 if default_yes else 1, False) == "Yes"
|
|
111
198
|
|
|
112
|
-
def list_dialog(self, title: str, options: list[str], multi_select: bool = False
|
|
199
|
+
def list_dialog(self, title: str, options: list[str], multi_select: bool = False,
|
|
200
|
+
preselected_values: list[str] | None = None) -> list[str]:
|
|
113
201
|
"""
|
|
114
202
|
Create a list dialog with the given options for the user to choose from.
|
|
115
203
|
|
|
116
204
|
:param title: The title of the dialog.
|
|
117
205
|
:param options: The list options that the user has to choose from.
|
|
118
206
|
:param multi_select: Whether the user is able to select multiple options from the list.
|
|
207
|
+
:param preselected_values: A list of values that will already be selected when the list dialog is created. The
|
|
208
|
+
user can unselect these values if they want to.
|
|
119
209
|
:return: The list of options that the user selected.
|
|
120
210
|
"""
|
|
121
|
-
request = ListDialogRequest(title, multi_select, options
|
|
122
|
-
|
|
211
|
+
request = ListDialogRequest(title, multi_select, options, preselected_values,
|
|
212
|
+
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
213
|
+
try:
|
|
214
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
215
|
+
response: list[str] | None = self.callback.show_list_dialog(request)
|
|
216
|
+
except ReadTimeout:
|
|
217
|
+
raise SapioDialogTimeoutException()
|
|
218
|
+
finally:
|
|
219
|
+
self.user.timeout_seconds = self._original_timeout
|
|
123
220
|
if response is None:
|
|
124
221
|
raise SapioUserCancelledException()
|
|
125
222
|
return response
|
|
@@ -161,8 +258,6 @@ class CallbackUtil:
|
|
|
161
258
|
builder = FormBuilder(data_type, display_name, plural_display_name)
|
|
162
259
|
for field_def in fields:
|
|
163
260
|
field_name = field_def.data_field_name
|
|
164
|
-
if values and hasattr(field_def, "default_value"):
|
|
165
|
-
field_def.default_value = values.get(field_name)
|
|
166
261
|
column: int = 0
|
|
167
262
|
span: int = 4
|
|
168
263
|
if column_positions and field_name in column_positions:
|
|
@@ -171,8 +266,15 @@ class CallbackUtil:
|
|
|
171
266
|
span = position[1]
|
|
172
267
|
builder.add_field(field_def, column, span)
|
|
173
268
|
|
|
174
|
-
request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type()
|
|
175
|
-
|
|
269
|
+
request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
|
|
270
|
+
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
271
|
+
try:
|
|
272
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
273
|
+
response: FieldMap | None = self.callback.show_form_entry_dialog(request)
|
|
274
|
+
except ReadTimeout:
|
|
275
|
+
raise SapioDialogTimeoutException()
|
|
276
|
+
finally:
|
|
277
|
+
self.user.timeout_seconds = self._original_timeout
|
|
176
278
|
if response is None:
|
|
177
279
|
raise SapioUserCancelledException()
|
|
178
280
|
return response
|
|
@@ -209,32 +311,39 @@ class CallbackUtil:
|
|
|
209
311
|
type_def: DataTypeDefinition = self.dt_cache.get_data_type(data_type)
|
|
210
312
|
field_defs: dict[str, AbstractVeloxFieldDefinition] = self.dt_cache.get_fields_for_type(data_type)
|
|
211
313
|
|
|
314
|
+
# Make everything visible, because presumably the caller gave a field name because they want it to be seen.
|
|
315
|
+
modifier = FieldModifier(visible=True, editable=editable)
|
|
316
|
+
|
|
212
317
|
# Build the form using only those fields that are desired.
|
|
318
|
+
values: dict[str, FieldValue] = {}
|
|
213
319
|
builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
|
|
214
320
|
for field_name in fields:
|
|
215
321
|
field_def = field_defs.get(field_name)
|
|
216
322
|
if field_def is None:
|
|
217
323
|
raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
|
|
218
|
-
|
|
219
|
-
field_def.editable = editable
|
|
220
|
-
field_def.visible = True
|
|
221
|
-
if hasattr(field_def, "default_value"):
|
|
222
|
-
field_def.default_value = record.get_field_value(field_name)
|
|
324
|
+
values[field_name] = record.get_field_value(field_name)
|
|
223
325
|
column: int = 0
|
|
224
326
|
span: int = 4
|
|
225
327
|
if column_positions and field_name in column_positions:
|
|
226
328
|
position = column_positions.get(field_name)
|
|
227
329
|
column = position[0]
|
|
228
330
|
span = position[1]
|
|
229
|
-
builder.add_field(field_def, column, span)
|
|
230
|
-
|
|
231
|
-
request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type()
|
|
232
|
-
|
|
331
|
+
builder.add_field(modifier.modify_field(field_def), column, span)
|
|
332
|
+
|
|
333
|
+
request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
|
|
334
|
+
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
335
|
+
try:
|
|
336
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
337
|
+
response: FieldMap | None = self.callback.show_form_entry_dialog(request)
|
|
338
|
+
except ReadTimeout:
|
|
339
|
+
raise SapioDialogTimeoutException()
|
|
340
|
+
finally:
|
|
341
|
+
self.user.timeout_seconds = self._original_timeout
|
|
233
342
|
if response is None:
|
|
234
343
|
raise SapioUserCancelledException()
|
|
235
344
|
return response
|
|
236
345
|
|
|
237
|
-
def input_dialog(self, title: str, msg: str, field: AbstractVeloxFieldDefinition) ->
|
|
346
|
+
def input_dialog(self, title: str, msg: str, field: AbstractVeloxFieldDefinition) -> FieldValue:
|
|
238
347
|
"""
|
|
239
348
|
Create an input dialog where the user must input data for a singular field.
|
|
240
349
|
|
|
@@ -243,8 +352,15 @@ class CallbackUtil:
|
|
|
243
352
|
:param field: The definition for a field that the user must provide input to.
|
|
244
353
|
:return: The response value from the user for the given field.
|
|
245
354
|
"""
|
|
246
|
-
request = InputDialogCriteria(title, msg, field,
|
|
247
|
-
|
|
355
|
+
request = InputDialogCriteria(title, msg, field,
|
|
356
|
+
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
357
|
+
try:
|
|
358
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
359
|
+
response: FieldValue | None = self.callback.show_input_dialog(request)
|
|
360
|
+
except ReadTimeout:
|
|
361
|
+
raise SapioDialogTimeoutException()
|
|
362
|
+
finally:
|
|
363
|
+
self.user.timeout_seconds = self._original_timeout
|
|
248
364
|
if response is None:
|
|
249
365
|
raise SapioUserCancelledException()
|
|
250
366
|
return response
|
|
@@ -259,7 +375,7 @@ class CallbackUtil:
|
|
|
259
375
|
:param field_name: The name and display name of the string field.
|
|
260
376
|
:param default_value: The default value to place into the string field, if any.
|
|
261
377
|
:param max_length: The max length of the string value. If not provided, uses the length of the default value.
|
|
262
|
-
If neither this
|
|
378
|
+
If neither this nor a default value are provided, defaults to 100 characters.
|
|
263
379
|
:param editable: Whether the field is editable by the user.
|
|
264
380
|
:param kwargs: Any additional keyword arguments to pass to the field definition.
|
|
265
381
|
:return: The string that the user input into the dialog.
|
|
@@ -320,6 +436,8 @@ class CallbackUtil:
|
|
|
320
436
|
msg: str,
|
|
321
437
|
fields: list[AbstractVeloxFieldDefinition],
|
|
322
438
|
values: list[FieldMap],
|
|
439
|
+
group_by: str | None = None,
|
|
440
|
+
image_data: list[bytes] | None = None,
|
|
323
441
|
*,
|
|
324
442
|
data_type: str = "Default",
|
|
325
443
|
display_name: str | None = None,
|
|
@@ -333,6 +451,10 @@ class CallbackUtil:
|
|
|
333
451
|
:param fields: The definitions of the fields to display as table columns. Fields will be displayed in the order
|
|
334
452
|
they are provided in this list.
|
|
335
453
|
:param values: The values to set for each row of the table.
|
|
454
|
+
:param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
|
|
455
|
+
The user may remove this grouping if they want to.
|
|
456
|
+
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
457
|
+
the image data list corresponds to the element at the same index in the values list.
|
|
336
458
|
:param data_type: The data type name for the temporary data type that will be created for this table.
|
|
337
459
|
:param display_name: The display name for the temporary data type. If not provided, defaults to the data type
|
|
338
460
|
name.
|
|
@@ -346,12 +468,24 @@ class CallbackUtil:
|
|
|
346
468
|
if plural_display_name is None:
|
|
347
469
|
plural_display_name = display_name + "s"
|
|
348
470
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
471
|
+
# Key fields display their columns in order before all non-key fields.
|
|
472
|
+
# Unmark key fields so that the column order is respected exactly as the caller provides it.
|
|
473
|
+
modifier = FieldModifier(key_field=False)
|
|
352
474
|
|
|
353
|
-
|
|
354
|
-
|
|
475
|
+
builder = FormBuilder(data_type, display_name, plural_display_name)
|
|
476
|
+
for field in fields:
|
|
477
|
+
builder.add_field(modifier.modify_field(field))
|
|
478
|
+
|
|
479
|
+
request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
|
|
480
|
+
record_image_data_list=image_data, group_by_field=group_by,
|
|
481
|
+
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
482
|
+
try:
|
|
483
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
484
|
+
response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
|
|
485
|
+
except ReadTimeout:
|
|
486
|
+
raise SapioDialogTimeoutException()
|
|
487
|
+
finally:
|
|
488
|
+
self.user.timeout_seconds = self._original_timeout
|
|
355
489
|
if response is None:
|
|
356
490
|
raise SapioUserCancelledException()
|
|
357
491
|
return response
|
|
@@ -361,11 +495,14 @@ class CallbackUtil:
|
|
|
361
495
|
msg: str,
|
|
362
496
|
fields: list[str],
|
|
363
497
|
records: list[SapioRecord],
|
|
364
|
-
editable: bool | None = True
|
|
498
|
+
editable: bool | None = True,
|
|
499
|
+
group_by: str | None = None,
|
|
500
|
+
image_data: list[bytes] | None = None) -> list[FieldMap]:
|
|
365
501
|
"""
|
|
366
502
|
Create a table dialog where the user may input data into the fields of the table. The table is constructed from
|
|
367
|
-
a given list of records. Provided field names must match fields on the definition of the data
|
|
368
|
-
records. The fields that are displayed will have their default value be that of the fields on
|
|
503
|
+
a given list of records of a singular type. Provided field names must match fields on the definition of the data
|
|
504
|
+
type of the given records. The fields that are displayed will have their default value be that of the fields on
|
|
505
|
+
the given records.
|
|
369
506
|
|
|
370
507
|
Makes webservice calls to get the data type definition and fields of the given records if they weren't
|
|
371
508
|
previously cached.
|
|
@@ -377,9 +514,15 @@ class CallbackUtil:
|
|
|
377
514
|
they are provided in this list.
|
|
378
515
|
:param editable: If true, all fields are displayed as editable. If false, all fields are displayed as
|
|
379
516
|
uneditable. If none, only those fields that are defined as editable by the data designer will be editable.
|
|
517
|
+
:param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
|
|
518
|
+
The user may remove this grouping if they want to.
|
|
519
|
+
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
520
|
+
the image data list corresponds to the element at the same index in the records list.
|
|
380
521
|
:return: A list of dictionaries mapping the data field names of the given field definitions to the response
|
|
381
522
|
value from the user for that field for each row.
|
|
382
523
|
"""
|
|
524
|
+
if not records:
|
|
525
|
+
raise SapioException("No records provided.")
|
|
383
526
|
data_types: set[str] = {x.data_type_name for x in records}
|
|
384
527
|
if len(data_types) > 1:
|
|
385
528
|
raise SapioException("Multiple data type names encountered in records list for record table popup.")
|
|
@@ -390,22 +533,193 @@ class CallbackUtil:
|
|
|
390
533
|
type_def: DataTypeDefinition = self.dt_cache.get_data_type(data_type)
|
|
391
534
|
field_defs: dict[str, AbstractVeloxFieldDefinition] = self.dt_cache.get_fields_for_type(data_type)
|
|
392
535
|
|
|
536
|
+
# Key fields display their columns in order before all non-key fields.
|
|
537
|
+
# Unmark key fields so that the column order is respected exactly as the caller provides it.
|
|
538
|
+
# Also make everything visible, because presumably the caller gave a field name because they want it to be seen.
|
|
539
|
+
modifier = FieldModifier(visible=True, key_field=False, editable=editable)
|
|
540
|
+
|
|
393
541
|
# Build the form using only those fields that are desired.
|
|
394
542
|
builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
|
|
395
543
|
for field_name in fields:
|
|
396
544
|
field_def = field_defs.get(field_name)
|
|
397
545
|
if field_def is None:
|
|
398
546
|
raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
547
|
+
builder.add_field(modifier.modify_field(field_def))
|
|
548
|
+
|
|
549
|
+
request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), field_map_list,
|
|
550
|
+
record_image_data_list=image_data, group_by_field=group_by,
|
|
551
|
+
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
552
|
+
try:
|
|
553
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
554
|
+
response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
|
|
555
|
+
except ReadTimeout:
|
|
556
|
+
raise SapioDialogTimeoutException()
|
|
557
|
+
finally:
|
|
558
|
+
self.user.timeout_seconds = self._original_timeout
|
|
559
|
+
if response is None:
|
|
560
|
+
raise SapioUserCancelledException()
|
|
561
|
+
return response
|
|
562
|
+
|
|
563
|
+
def multi_type_table_dialog(self,
|
|
564
|
+
title: str,
|
|
565
|
+
msg: str,
|
|
566
|
+
fields: list[(str, str) | AbstractVeloxFieldDefinition],
|
|
567
|
+
row_contents: list[list[SapioRecord | FieldMap]],
|
|
568
|
+
*,
|
|
569
|
+
default_modifier: FieldModifier | None = None,
|
|
570
|
+
field_modifiers: dict[str, FieldModifier] | None = None,
|
|
571
|
+
data_type: str = "Default",
|
|
572
|
+
display_name: str | None = None,
|
|
573
|
+
plural_display_name: str | None = None) -> list[FieldMap]:
|
|
574
|
+
"""
|
|
575
|
+
Create a table dialog where the user may input data into the fields of the table. The table is constructed from
|
|
576
|
+
a given list of records of multiple data types or field maps. Provided field names must match with field names
|
|
577
|
+
from the data type definition of the given records. The fields that are displayed will have their default value
|
|
578
|
+
be that of the fields on the given records or field maps.
|
|
579
|
+
|
|
580
|
+
Makes webservice calls to get the data type field definitions of the given records if they weren't
|
|
581
|
+
previously cached.
|
|
582
|
+
|
|
583
|
+
:param title: The title of the dialog.
|
|
584
|
+
:param msg: The message to display in the dialog.
|
|
585
|
+
:param fields: A list of objects representing the fields in the table. This could either be a two-element tuple
|
|
586
|
+
where the first element is a data type name and the second is a field name, or it could be a field
|
|
587
|
+
definition. If it is the former, a query will be made to find the field definition matching tht data type.
|
|
588
|
+
The data type names of the fields must match the data type names of the records in the row contents.
|
|
589
|
+
See the description of row_contents for what to do if you want to construct a field that pulls from a field
|
|
590
|
+
map.
|
|
591
|
+
If two fields share the same field name, an exception will be thrown. This is even true in the case where
|
|
592
|
+
the data type name of the fields is different. If you wish to display two fields from two data types with
|
|
593
|
+
the same name, then you must provide a FieldModifier for at least one of the fields where prepend_data_type
|
|
594
|
+
is True in order to make that field's name unique again. Note that if you do this for a field, the mapping
|
|
595
|
+
of record to field name will use the unedited field name, but the return results of this function will
|
|
596
|
+
use the edited field name in the results dictionary for a row.
|
|
597
|
+
:param row_contents: A list where each element is another list representing the records or a field map that will
|
|
598
|
+
be used to populate the columns of the table. If the data type of a given record doesn't match any of the
|
|
599
|
+
data type names of the given fields, then it will not be used.
|
|
600
|
+
This list can contain up to one field map, which are fields not tied to a record. This is so that you can
|
|
601
|
+
create abstract field definition not tied to a specific record in the system. If you want to define an
|
|
602
|
+
abstract field that pulls from the field map in the row contents, then you must set the data type name to
|
|
603
|
+
Default.
|
|
604
|
+
If a record of a given data type appears more than once in one of the inner-lists of the row contents, or
|
|
605
|
+
there is more than one field map, then an exception will be thrown, as there is no way of distinguishing
|
|
606
|
+
which record should be used for a field, and not all fields could have their values combined across multiple
|
|
607
|
+
records.
|
|
608
|
+
The row contents may have an inner-list which is missing a record of a data type that matches one of the
|
|
609
|
+
fields. In this case, the field value for that row under that column will be null.
|
|
610
|
+
The inner-list does not need to be sorted in any way, as this function will map the inner-list contents to
|
|
611
|
+
fields as necessary.
|
|
612
|
+
The inner-list may contain null elements; these will simply be discarded by this function.
|
|
613
|
+
:param default_modifier: A default field modifier that will be applied to the given fields. This can be used to
|
|
614
|
+
make field definitions from the system behave differently than their system values. If this value is None,
|
|
615
|
+
then a default field modifier is created that causes all specified fields to be both visible and not key
|
|
616
|
+
fields. (Key fields get displayed first before any non-key fields in tables, so the key field setting is
|
|
617
|
+
disabled by default in order to have the columns in the table respect the order of the fields as they are
|
|
618
|
+
provided to this function.)
|
|
619
|
+
:param field_modifiers: A mapping of data field name to field modifier for changes that should be applied to
|
|
620
|
+
the matching field. If a data field name is not present in the provided dict, or the provided dictionary is
|
|
621
|
+
None, then the default modifier will be used.
|
|
622
|
+
:param data_type: The data type name for the temporary data type that will be created for this table.
|
|
623
|
+
:param display_name: The display name for the temporary data type. If not provided, defaults to the data type
|
|
624
|
+
name.
|
|
625
|
+
:param plural_display_name: The plural display name for the temporary data type. If not provided, defaults to
|
|
626
|
+
the display name + "s".
|
|
627
|
+
:return: A list of dictionaries mapping the data field names of the given field definitions to the response
|
|
628
|
+
value from the user for that field for each row.
|
|
629
|
+
"""
|
|
630
|
+
# Set the default modifier to make all fields visible and not key if no default was provided.
|
|
631
|
+
if default_modifier is None:
|
|
632
|
+
default_modifier = FieldModifier(visible=True, key_field=False)
|
|
633
|
+
# To make things simpler, treat null field modifiers as an empty dict.
|
|
634
|
+
if field_modifiers is None:
|
|
635
|
+
field_modifiers = {}
|
|
636
|
+
|
|
637
|
+
# Construct the final fields list from the possible field objects.
|
|
638
|
+
final_fields: list[AbstractVeloxFieldDefinition] = []
|
|
639
|
+
# Keep track of whether any given field name appears more than once, as two fields could have the same
|
|
640
|
+
# field name but different data types. In this case, the user should provide a field modifier or field
|
|
641
|
+
# definition that changes one of the field names.
|
|
642
|
+
field_names: list[str] = []
|
|
643
|
+
for field in fields:
|
|
644
|
+
# Find the field definition for this field object.
|
|
645
|
+
if isinstance(field, tuple):
|
|
646
|
+
field_def: AbstractVeloxFieldDefinition = self.dt_cache.get_fields_for_type(field[0]).get(field[1])
|
|
647
|
+
elif isinstance(field, AbstractVeloxFieldDefinition):
|
|
648
|
+
field_def: AbstractVeloxFieldDefinition = field
|
|
649
|
+
else:
|
|
650
|
+
raise SapioException("Unrecognized field object.")
|
|
651
|
+
|
|
652
|
+
# Locate the modifier for this field and store the modified field.
|
|
653
|
+
name: str = field_def.data_field_name
|
|
654
|
+
modifier: FieldModifier = field_modifiers.get(name, default_modifier)
|
|
655
|
+
field_def: AbstractVeloxFieldDefinition = modifier.modify_field(field_def)
|
|
656
|
+
final_fields.append(field_def)
|
|
657
|
+
|
|
658
|
+
# Verify that this field name isn't a duplicate.
|
|
659
|
+
# The field name may have changed due to the modifier.
|
|
660
|
+
name: str = field_def.data_field_name
|
|
661
|
+
if name in field_names:
|
|
662
|
+
raise SapioException(f"The field name \"{name}\" appears more than once in the given fields. "
|
|
663
|
+
f"If you have provided two fields with the same name but different data types, "
|
|
664
|
+
f"consider providing a FieldModifier where prepend_data_type is true for one of "
|
|
665
|
+
f"the fields so that the field names will be different.")
|
|
666
|
+
field_names.append(name)
|
|
667
|
+
|
|
668
|
+
# Get the values for each row.
|
|
669
|
+
values: list[dict[str, FieldValue]] = []
|
|
670
|
+
for row in row_contents:
|
|
671
|
+
# The final values for this row:
|
|
672
|
+
row_values: dict[str, FieldValue] = {}
|
|
673
|
+
|
|
674
|
+
# Map the records for this row by their data type. If a field map is provided, its data type is Default.
|
|
675
|
+
row_records: dict[str, SapioRecord | FieldMap] = {}
|
|
676
|
+
for rec in row:
|
|
677
|
+
# Toss out null elements.
|
|
678
|
+
if rec is None:
|
|
679
|
+
continue
|
|
680
|
+
# Map records to their data type name. Map field maps to Default.
|
|
681
|
+
dt: str = "Default" if isinstance(rec, dict) else rec.data_type_name
|
|
682
|
+
# Warn if the same data type name appears more than once.
|
|
683
|
+
if dt in row_records:
|
|
684
|
+
raise SapioException(f"The data type \"{dt}\" appears more than once in the given row contents.")
|
|
685
|
+
row_records[dt] = rec
|
|
686
|
+
|
|
687
|
+
# Get the field values from the above records.
|
|
688
|
+
for field in final_fields:
|
|
689
|
+
# Find the object that corresponds to this field given its data type name.
|
|
690
|
+
record: SapioRecord | FieldMap | None = row_records.get(field.data_type_name)
|
|
691
|
+
# This could be either a record, a field map, or null. Convert any records to field maps.
|
|
692
|
+
if not isinstance(record, dict) and record is not None:
|
|
693
|
+
record: FieldMap | None = AliasUtil.to_field_map_lists([record])[0]
|
|
694
|
+
|
|
695
|
+
# Find out if this field had its data type prepended to it. If this is the case, then we need to find
|
|
696
|
+
# the true data field name before retrieving the value from the field map.
|
|
697
|
+
name: str = field.data_field_name
|
|
698
|
+
if field_modifiers.get(name, default_modifier).prepend_data_type is True:
|
|
699
|
+
name = name.split(".")[1]
|
|
700
|
+
|
|
701
|
+
# Set the value for this particular field.
|
|
702
|
+
row_values[field.data_field_name] = record.get(name) if record else None
|
|
703
|
+
values.append(row_values)
|
|
704
|
+
|
|
705
|
+
if display_name is None:
|
|
706
|
+
display_name = data_type
|
|
707
|
+
if plural_display_name is None:
|
|
708
|
+
plural_display_name = display_name + "s"
|
|
709
|
+
|
|
710
|
+
builder = FormBuilder(data_type, display_name, plural_display_name)
|
|
711
|
+
for field in final_fields:
|
|
712
|
+
builder.add_field(field)
|
|
713
|
+
|
|
714
|
+
request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
|
|
715
|
+
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
716
|
+
try:
|
|
717
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
718
|
+
response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
|
|
719
|
+
except ReadTimeout:
|
|
720
|
+
raise SapioDialogTimeoutException()
|
|
721
|
+
finally:
|
|
722
|
+
self.user.timeout_seconds = self._original_timeout
|
|
409
723
|
if response is None:
|
|
410
724
|
raise SapioUserCancelledException()
|
|
411
725
|
return response
|
|
@@ -453,8 +767,15 @@ class CallbackUtil:
|
|
|
453
767
|
raise SapioException(f"The data type \"{data_type}\" does not have a layout by the name "
|
|
454
768
|
f"\"{layout_name}\" in the system.")
|
|
455
769
|
|
|
456
|
-
request = DataRecordDialogRequest(title, record, layout, minimized, access_level, plugin_path_list
|
|
457
|
-
|
|
770
|
+
request = DataRecordDialogRequest(title, record, layout, minimized, access_level, plugin_path_list,
|
|
771
|
+
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
772
|
+
try:
|
|
773
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
774
|
+
response: bool = self.callback.data_record_form_view_dialog(request)
|
|
775
|
+
except ReadTimeout:
|
|
776
|
+
raise SapioDialogTimeoutException()
|
|
777
|
+
finally:
|
|
778
|
+
self.user.timeout_seconds = self._original_timeout
|
|
458
779
|
if not response:
|
|
459
780
|
raise SapioUserCancelledException()
|
|
460
781
|
|
|
@@ -464,7 +785,8 @@ class CallbackUtil:
|
|
|
464
785
|
values: list[FieldMap],
|
|
465
786
|
multi_select: bool = True,
|
|
466
787
|
*,
|
|
467
|
-
|
|
788
|
+
data_type: str = "Default",
|
|
789
|
+
display_name: str | None = None,
|
|
468
790
|
plural_display_name: str | None = None) -> list[FieldMap]:
|
|
469
791
|
"""
|
|
470
792
|
Create a selection dialog for a list of field maps for the user to choose from. Requires that the caller
|
|
@@ -475,24 +797,37 @@ class CallbackUtil:
|
|
|
475
797
|
they are provided in this list.
|
|
476
798
|
:param values: The values to set for each row of the table.
|
|
477
799
|
:param multi_select: Whether the user is able to select multiple rows from the list.
|
|
478
|
-
:param
|
|
800
|
+
:param data_type: The data type name for the temporary data type that will be created for this table.
|
|
801
|
+
:param display_name: The display name for the temporary data type. If not provided, defaults to the data type
|
|
802
|
+
name.
|
|
479
803
|
:param plural_display_name: The plural display name for the temporary data type. If not provided, defaults to
|
|
480
804
|
the display name + "s".
|
|
481
805
|
:return: A list of field maps corresponding to the chosen input field maps.
|
|
482
806
|
"""
|
|
807
|
+
if display_name is None:
|
|
808
|
+
display_name = data_type
|
|
483
809
|
if plural_display_name is None:
|
|
484
810
|
plural_display_name = display_name + "s"
|
|
485
811
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
812
|
+
builder = FormBuilder(data_type, display_name, plural_display_name)
|
|
813
|
+
for field in fields:
|
|
814
|
+
builder.add_field(field)
|
|
815
|
+
|
|
816
|
+
request = TempTableSelectionRequest(builder.get_temporary_data_type(), msg, values,
|
|
817
|
+
multi_select=multi_select)
|
|
818
|
+
try:
|
|
819
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
820
|
+
response: list[FieldMap] | None = self.callback.show_temp_table_selection_dialog(request)
|
|
821
|
+
except ReadTimeout:
|
|
822
|
+
raise SapioDialogTimeoutException()
|
|
823
|
+
finally:
|
|
824
|
+
self.user.timeout_seconds = self._original_timeout
|
|
490
825
|
if response is None:
|
|
491
826
|
raise SapioUserCancelledException()
|
|
492
827
|
return response
|
|
493
828
|
|
|
494
829
|
def record_selection_dialog(self, msg: str, fields: list[str], records: list[SapioRecord],
|
|
495
|
-
multi_select: bool = True) -> list[
|
|
830
|
+
multi_select: bool = True) -> list[SapioRecord]:
|
|
496
831
|
"""
|
|
497
832
|
Create a record selection dialog for a list of records for the user to choose from. Provided field names must
|
|
498
833
|
match fields on the definition of the data type of the given records.
|
|
@@ -505,8 +840,10 @@ class CallbackUtil:
|
|
|
505
840
|
they are provided in this list.
|
|
506
841
|
:param records: The records to display as rows in the table.
|
|
507
842
|
:param multi_select: Whether the user is able to select multiple records from the list.
|
|
508
|
-
:return: A list of
|
|
843
|
+
:return: A list of the selected records.
|
|
509
844
|
"""
|
|
845
|
+
if not records:
|
|
846
|
+
raise SapioException("No records provided.")
|
|
510
847
|
data_types: set[str] = {x.data_type_name for x in records}
|
|
511
848
|
if len(data_types) > 1:
|
|
512
849
|
raise SapioException("Multiple data type names encountered in records list for record table popup.")
|
|
@@ -521,21 +858,28 @@ class CallbackUtil:
|
|
|
521
858
|
type_def: DataTypeDefinition = self.dt_cache.get_data_type(data_type)
|
|
522
859
|
field_defs: dict[str, AbstractVeloxFieldDefinition] = self.dt_cache.get_fields_for_type(data_type)
|
|
523
860
|
|
|
861
|
+
# Key fields display their columns in order before all non-key fields.
|
|
862
|
+
# Unmark key fields so that the column order is respected exactly as the caller provides it.
|
|
863
|
+
# Also make everything visible, because presumably the caller give a field name because they want it to be seen.
|
|
864
|
+
modifier = FieldModifier(visible=True, key_field=False)
|
|
865
|
+
|
|
524
866
|
# Build the form using only those fields that are desired.
|
|
525
|
-
|
|
867
|
+
builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
|
|
526
868
|
for field_name in fields:
|
|
527
869
|
field_def = field_defs.get(field_name)
|
|
528
870
|
if field_def is None:
|
|
529
871
|
raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
|
|
530
|
-
field_def
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
872
|
+
builder.add_field(modifier.modify_field(field_def))
|
|
873
|
+
|
|
874
|
+
request = TempTableSelectionRequest(builder.get_temporary_data_type(), msg, field_map_list,
|
|
875
|
+
multi_select=multi_select)
|
|
876
|
+
try:
|
|
877
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
878
|
+
response: list[FieldMap] | None = self.callback.show_temp_table_selection_dialog(request)
|
|
879
|
+
except ReadTimeout:
|
|
880
|
+
raise SapioDialogTimeoutException()
|
|
881
|
+
finally:
|
|
882
|
+
self.user.timeout_seconds = self._original_timeout
|
|
539
883
|
if response is None:
|
|
540
884
|
raise SapioUserCancelledException()
|
|
541
885
|
# Map the field maps in the response back to the record they come from, returning the chosen record instead of
|
|
@@ -602,7 +946,7 @@ class CallbackUtil:
|
|
|
602
946
|
|
|
603
947
|
# If CustomReportCriteria was provided, it must be wrapped as a CustomReport.
|
|
604
948
|
if isinstance(custom_search, CustomReportCriteria):
|
|
605
|
-
custom_search: CustomReport = CustomReport(False,
|
|
949
|
+
custom_search: CustomReport = CustomReport(False, [], custom_search)
|
|
606
950
|
# If a string was provided, locate the report criteria for the predefined search in the system matching this
|
|
607
951
|
# name.
|
|
608
952
|
if isinstance(custom_search, str):
|
|
@@ -611,7 +955,13 @@ class CallbackUtil:
|
|
|
611
955
|
request = InputSelectionRequest(data_type, msg, search_types, only_key_fields, record_blacklist,
|
|
612
956
|
record_whitelist, preselected_records, custom_search, scan_criteria,
|
|
613
957
|
multi_select)
|
|
614
|
-
|
|
958
|
+
try:
|
|
959
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
960
|
+
response: list[DataRecord] | None = self.callback.show_input_selection_dialog(request)
|
|
961
|
+
except ReadTimeout:
|
|
962
|
+
raise SapioDialogTimeoutException()
|
|
963
|
+
finally:
|
|
964
|
+
self.user.timeout_seconds = self._original_timeout
|
|
615
965
|
if response is None:
|
|
616
966
|
raise SapioUserCancelledException()
|
|
617
967
|
return RecordHandler(self.user).wrap_models(response, wrapper_type)
|
|
@@ -635,14 +985,21 @@ class CallbackUtil:
|
|
|
635
985
|
for field in additional_fields:
|
|
636
986
|
builder.add_field(field)
|
|
637
987
|
temp_dt = builder.get_temporary_data_type()
|
|
638
|
-
request = ESigningRequestPojo(title, msg, show_comment, temp_dt
|
|
639
|
-
|
|
988
|
+
request = ESigningRequestPojo(title, msg, show_comment, temp_dt,
|
|
989
|
+
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
990
|
+
try:
|
|
991
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
992
|
+
response: ESigningResponsePojo | None = self.callback.show_esign_dialog(request)
|
|
993
|
+
except ReadTimeout:
|
|
994
|
+
raise SapioDialogTimeoutException()
|
|
995
|
+
finally:
|
|
996
|
+
self.user.timeout_seconds = self._original_timeout
|
|
640
997
|
if response is None:
|
|
641
998
|
raise SapioUserCancelledException()
|
|
642
999
|
return response
|
|
643
1000
|
|
|
644
1001
|
def request_file(self, title: str, exts: list[str] | None = None,
|
|
645
|
-
show_image_editor: bool = False, show_camera_button: bool = False) ->
|
|
1002
|
+
show_image_editor: bool = False, show_camera_button: bool = False) -> tuple[str, bytes]:
|
|
646
1003
|
"""
|
|
647
1004
|
Request a single file from the user.
|
|
648
1005
|
|
|
@@ -667,7 +1024,13 @@ class CallbackUtil:
|
|
|
667
1024
|
return sink.consume_data(chunk, io_obj)
|
|
668
1025
|
|
|
669
1026
|
request = FilePromptRequest(title, show_image_editor, ",".join(exts), show_camera_button)
|
|
670
|
-
|
|
1027
|
+
try:
|
|
1028
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
1029
|
+
file_path: str | None = self.callback.show_file_dialog(request, do_consume)
|
|
1030
|
+
except ReadTimeout:
|
|
1031
|
+
raise SapioDialogTimeoutException()
|
|
1032
|
+
finally:
|
|
1033
|
+
self.user.timeout_seconds = self._original_timeout
|
|
671
1034
|
if file_path is None:
|
|
672
1035
|
raise SapioUserCancelledException()
|
|
673
1036
|
|
|
@@ -675,7 +1038,7 @@ class CallbackUtil:
|
|
|
675
1038
|
return file_path, sink.data
|
|
676
1039
|
|
|
677
1040
|
def request_files(self, title: str, exts: list[str] | None = None,
|
|
678
|
-
show_image_editor: bool = False, show_camera_button: bool = False):
|
|
1041
|
+
show_image_editor: bool = False, show_camera_button: bool = False) -> dict[str, bytes]:
|
|
679
1042
|
"""
|
|
680
1043
|
Request multiple files from the user.
|
|
681
1044
|
|
|
@@ -692,7 +1055,13 @@ class CallbackUtil:
|
|
|
692
1055
|
exts: list[str] = []
|
|
693
1056
|
|
|
694
1057
|
request = MultiFilePromptRequest(title, show_image_editor, ",".join(exts), show_camera_button)
|
|
695
|
-
|
|
1058
|
+
try:
|
|
1059
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
1060
|
+
file_paths: list[str] | None = self.callback.show_multi_file_dialog(request)
|
|
1061
|
+
except ReadTimeout:
|
|
1062
|
+
raise SapioDialogTimeoutException()
|
|
1063
|
+
finally:
|
|
1064
|
+
self.user.timeout_seconds = self._original_timeout
|
|
696
1065
|
if not file_paths:
|
|
697
1066
|
raise SapioUserCancelledException()
|
|
698
1067
|
|
|
@@ -706,7 +1075,7 @@ class CallbackUtil:
|
|
|
706
1075
|
return ret_dict
|
|
707
1076
|
|
|
708
1077
|
@staticmethod
|
|
709
|
-
def __verify_file(file_path: str, file_bytes: bytes, allowed_extensions: list[str]):
|
|
1078
|
+
def __verify_file(file_path: str, file_bytes: bytes, allowed_extensions: list[str]) -> None:
|
|
710
1079
|
"""
|
|
711
1080
|
Verify that the provided file was read (i.e. the file path and file bytes aren't None or empty) and that it
|
|
712
1081
|
has the correct file extension. Raises a user error exception if something about the file is incorrect.
|
|
@@ -720,7 +1089,7 @@ class CallbackUtil:
|
|
|
720
1089
|
if len(allowed_extensions) != 0:
|
|
721
1090
|
matches: bool = False
|
|
722
1091
|
for ext in allowed_extensions:
|
|
723
|
-
if file_path.endswith("." + ext):
|
|
1092
|
+
if file_path.endswith("." + ext.lstrip(".")):
|
|
724
1093
|
matches = True
|
|
725
1094
|
break
|
|
726
1095
|
if matches is False:
|
|
@@ -734,5 +1103,85 @@ class CallbackUtil:
|
|
|
734
1103
|
:param file_name: The name of the file.
|
|
735
1104
|
:param file_data: The data of the file, provided as either a string or as a bytes array.
|
|
736
1105
|
"""
|
|
737
|
-
data = io.
|
|
1106
|
+
data = io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data)
|
|
738
1107
|
self.callback.send_file(file_name, False, data)
|
|
1108
|
+
|
|
1109
|
+
def write_zip_file(self, zip_name: str, files: dict[str, str | bytes]) -> None:
|
|
1110
|
+
"""
|
|
1111
|
+
Send a collection of files to the user in a zip file.
|
|
1112
|
+
|
|
1113
|
+
:param zip_name: The name of the zip file.
|
|
1114
|
+
:param files: A dictionary of the files to add to the zip file.
|
|
1115
|
+
"""
|
|
1116
|
+
data = io.BytesIO(FileUtil.zip_files(files))
|
|
1117
|
+
self.callback.send_file(zip_name, False, data)
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
class FieldModifier:
|
|
1121
|
+
"""
|
|
1122
|
+
A FieldModifier can be used to update the settings of a field definition from the system.
|
|
1123
|
+
"""
|
|
1124
|
+
prepend_data_type: bool
|
|
1125
|
+
display_name: str | None
|
|
1126
|
+
required: bool | None
|
|
1127
|
+
editable: bool | None
|
|
1128
|
+
visible: bool | None
|
|
1129
|
+
key_field: bool | None
|
|
1130
|
+
column_width: int | None
|
|
1131
|
+
|
|
1132
|
+
def __init__(self, *, prepend_data_type: bool = False,
|
|
1133
|
+
display_name: str | None = None, required: bool | None = None, editable: bool | None = None,
|
|
1134
|
+
visible: bool | None = None, key_field: bool | None = None, column_width: int | None = None):
|
|
1135
|
+
"""
|
|
1136
|
+
If any values are given as None then that value will not be changed on the given field.
|
|
1137
|
+
|
|
1138
|
+
:param prepend_data_type: If true, prepends the data type name of the field to the data field name. For example,
|
|
1139
|
+
if a field has a data type name X and a data field name Y, then the field name would become "X.Y". This is
|
|
1140
|
+
useful for cases where you have the same field name on two different data types and want to distinguish one
|
|
1141
|
+
or both of them.
|
|
1142
|
+
:param display_name: Change the display name.
|
|
1143
|
+
:param required: Change the required status.
|
|
1144
|
+
:param editable: Change the editable status.
|
|
1145
|
+
:param visible: Change the visible status.
|
|
1146
|
+
:param key_field: Change the key field status.
|
|
1147
|
+
:param column_width: Change the column width.
|
|
1148
|
+
"""
|
|
1149
|
+
self.prepend_data_type = prepend_data_type
|
|
1150
|
+
self.display_name = display_name
|
|
1151
|
+
self.required = required
|
|
1152
|
+
self.editable = editable
|
|
1153
|
+
self.visible = visible
|
|
1154
|
+
self.key_field = key_field
|
|
1155
|
+
self.column_width = column_width
|
|
1156
|
+
|
|
1157
|
+
def modify_field(self, field: AbstractVeloxFieldDefinition) -> AbstractVeloxFieldDefinition:
|
|
1158
|
+
"""
|
|
1159
|
+
Apply modifications to a given field.
|
|
1160
|
+
|
|
1161
|
+
:param field: The field to modify.
|
|
1162
|
+
:return: A copy of the input field with the modifications applied.
|
|
1163
|
+
"""
|
|
1164
|
+
field = copy_field(field)
|
|
1165
|
+
if self.prepend_data_type is True:
|
|
1166
|
+
field._data_field_name = field.data_field_name + "." + field.data_field_name
|
|
1167
|
+
if self.display_name is not None:
|
|
1168
|
+
field.display_name = self.display_name
|
|
1169
|
+
if self.required is not None:
|
|
1170
|
+
field.required = self.required
|
|
1171
|
+
if self.editable is not None:
|
|
1172
|
+
field.editable = self.editable
|
|
1173
|
+
if self.visible is not None:
|
|
1174
|
+
field.visible = self.visible
|
|
1175
|
+
if self.key_field is not None:
|
|
1176
|
+
field.key_field = self.key_field
|
|
1177
|
+
if self.column_width is not None:
|
|
1178
|
+
field.default_table_column_width = self.column_width
|
|
1179
|
+
return field
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
def copy_field(field: AbstractVeloxFieldDefinition) -> AbstractVeloxFieldDefinition:
|
|
1183
|
+
"""
|
|
1184
|
+
Create a copy of a given field definition. This is used to modify field definitions from the server for existing
|
|
1185
|
+
data types without also modifying the field definition in the cache.
|
|
1186
|
+
"""
|
|
1187
|
+
return FieldDefinitionParser.to_field_definition(field.to_json())
|