sapiopycommons 2025.7.9a582__py3-none-any.whl → 2025.7.10a595__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 +665 -332
- sapiopycommons/callbacks/field_builder.py +2 -0
- sapiopycommons/chem/IndigoMolecules.py +31 -1
- sapiopycommons/chem/Molecules.py +3 -3
- sapiopycommons/chem/ps_commons.py +523 -0
- sapiopycommons/customreport/auto_pagers.py +26 -1
- sapiopycommons/customreport/term_builder.py +1 -1
- sapiopycommons/datatype/pseudo_data_types.py +349 -326
- sapiopycommons/eln/experiment_cache.py +188 -0
- sapiopycommons/eln/experiment_handler.py +408 -767
- sapiopycommons/eln/experiment_report_util.py +11 -6
- sapiopycommons/eln/experiment_step_factory.py +476 -0
- sapiopycommons/eln/plate_designer.py +7 -2
- sapiopycommons/eln/step_creation.py +236 -0
- sapiopycommons/files/file_util.py +7 -5
- sapiopycommons/general/accession_service.py +2 -2
- sapiopycommons/general/aliases.py +3 -1
- sapiopycommons/general/audit_log.py +7 -0
- sapiopycommons/general/custom_report_util.py +12 -0
- sapiopycommons/general/data_structure_util.py +115 -0
- sapiopycommons/processtracking/custom_workflow_handler.py +11 -1
- sapiopycommons/processtracking/endpoints.py +27 -0
- sapiopycommons/recordmodel/record_handler.py +657 -317
- sapiopycommons/rules/eln_rule_handler.py +8 -1
- sapiopycommons/rules/on_save_rule_handler.py +8 -1
- sapiopycommons/webhook/webhook_handlers.py +3 -0
- sapiopycommons/webhook/webservice_handlers.py +2 -2
- {sapiopycommons-2025.7.9a582.dist-info → sapiopycommons-2025.7.10a595.dist-info}/METADATA +2 -2
- sapiopycommons-2025.7.10a595.dist-info/RECORD +69 -0
- sapiopycommons/ai/__init__.py +0 -0
- sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.py +0 -43
- sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.pyi +0 -31
- sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2_grpc.py +0 -24
- sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.py +0 -123
- sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.pyi +0 -598
- sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2_grpc.py +0 -24
- sapiopycommons/ai/api/plan/proto/step_output_pb2.py +0 -45
- sapiopycommons/ai/api/plan/proto/step_output_pb2.pyi +0 -42
- sapiopycommons/ai/api/plan/proto/step_output_pb2_grpc.py +0 -24
- sapiopycommons/ai/api/plan/proto/step_pb2.py +0 -43
- sapiopycommons/ai/api/plan/proto/step_pb2.pyi +0 -43
- sapiopycommons/ai/api/plan/proto/step_pb2_grpc.py +0 -24
- sapiopycommons/ai/api/plan/script/proto/script_pb2.py +0 -55
- sapiopycommons/ai/api/plan/script/proto/script_pb2.pyi +0 -115
- sapiopycommons/ai/api/plan/script/proto/script_pb2_grpc.py +0 -153
- sapiopycommons/ai/api/plan/tool/proto/entry_pb2.py +0 -57
- sapiopycommons/ai/api/plan/tool/proto/entry_pb2.pyi +0 -96
- sapiopycommons/ai/api/plan/tool/proto/entry_pb2_grpc.py +0 -24
- sapiopycommons/ai/api/plan/tool/proto/tool_pb2.py +0 -67
- sapiopycommons/ai/api/plan/tool/proto/tool_pb2.pyi +0 -220
- sapiopycommons/ai/api/plan/tool/proto/tool_pb2_grpc.py +0 -154
- sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.py +0 -39
- sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.pyi +0 -32
- sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2_grpc.py +0 -24
- sapiopycommons/ai/protobuf_utils.py +0 -508
- sapiopycommons/ai/test_client.py +0 -251
- sapiopycommons/ai/tool_service_base.py +0 -798
- sapiopycommons-2025.7.9a582.dist-info/RECORD +0 -92
- {sapiopycommons-2025.7.9a582.dist-info → sapiopycommons-2025.7.10a595.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.7.9a582.dist-info → sapiopycommons-2025.7.10a595.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,7 +4,8 @@ import io
|
|
|
4
4
|
import re
|
|
5
5
|
import warnings
|
|
6
6
|
from copy import copy
|
|
7
|
-
from
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Iterable, TypeAlias, Any, Callable, Container, Collection
|
|
8
9
|
from weakref import WeakValueDictionary
|
|
9
10
|
|
|
10
11
|
from requests import ReadTimeout
|
|
@@ -38,11 +39,35 @@ from sapiopycommons.general.aliases import FieldMap, SapioRecord, AliasUtil, Rec
|
|
|
38
39
|
from sapiopycommons.general.custom_report_util import CustomReportUtil
|
|
39
40
|
from sapiopycommons.general.exceptions import SapioUserCancelledException, SapioException, SapioUserErrorException, \
|
|
40
41
|
SapioDialogTimeoutException
|
|
42
|
+
from sapiopycommons.general.time_util import TimeUtil
|
|
41
43
|
from sapiopycommons.recordmodel.record_handler import RecordHandler
|
|
42
44
|
|
|
43
45
|
DataTypeLayoutIdentifier: TypeAlias = DataTypeLayout | str | None
|
|
44
46
|
|
|
45
47
|
|
|
48
|
+
# FR-47690: Added enum to customize blank handling result behavior, instead of using the require_selection/input
|
|
49
|
+
# boolean parameter.
|
|
50
|
+
class BlankResultHandling(Enum):
|
|
51
|
+
"""
|
|
52
|
+
An enum that controls how blank results are handled in dialogs.
|
|
53
|
+
"""
|
|
54
|
+
DEFAULT = 0
|
|
55
|
+
"""Used only by dialog functions. If a dialog function parameter is set to this value, then the blank result
|
|
56
|
+
handling of the CallbackUtil is used."""
|
|
57
|
+
REPEAT = 1
|
|
58
|
+
"""If the user provides a blank result, repeat the dialog."""
|
|
59
|
+
CANCEL = 2
|
|
60
|
+
"""If the user provides a blank result, throw a cancel exception."""
|
|
61
|
+
RETURN = 3
|
|
62
|
+
"""If the user provides a blank result, return it to the caller."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# CR-47521: Updated various parameter type hints from list or Iterable to more specific type hints.
|
|
66
|
+
# If we need to iterate over the parameter, then it is Iterable.
|
|
67
|
+
# If we need to see if the parameter contains a value, then it is Container.
|
|
68
|
+
# If the length/size of the parameter is needed, then it is Collection.
|
|
69
|
+
# If we need to access the parameter by an index, then it is Sequence. (This excludes sets and dictionaries, so it's
|
|
70
|
+
# probably better to accept a Collection then cast the parameter to a list if you need to get an element from it.)
|
|
46
71
|
class CallbackUtil:
|
|
47
72
|
user: SapioUser
|
|
48
73
|
callback: ClientCallback
|
|
@@ -53,6 +78,7 @@ class CallbackUtil:
|
|
|
53
78
|
timeout_seconds: int
|
|
54
79
|
width_pixels: int | None
|
|
55
80
|
width_percent: float | None
|
|
81
|
+
_default_blank_result_handling: BlankResultHandling
|
|
56
82
|
|
|
57
83
|
__instances: WeakValueDictionary[SapioUser, CallbackUtil] = WeakValueDictionary()
|
|
58
84
|
__initialized: bool
|
|
@@ -90,6 +116,7 @@ class CallbackUtil:
|
|
|
90
116
|
self.timeout_seconds = self.user.timeout_seconds
|
|
91
117
|
self.width_pixels = None
|
|
92
118
|
self.width_percent = None
|
|
119
|
+
self._default_blank_result_handling = BlankResultHandling.CANCEL
|
|
93
120
|
self.__layouts = {}
|
|
94
121
|
|
|
95
122
|
def set_dialog_width(self, width_pixels: int | None = None, width_percent: float | None = None):
|
|
@@ -116,6 +143,19 @@ class CallbackUtil:
|
|
|
116
143
|
"""
|
|
117
144
|
self.timeout_seconds = timeout
|
|
118
145
|
|
|
146
|
+
def set_default_blank_result_handling(self, handling: BlankResultHandling):
|
|
147
|
+
"""
|
|
148
|
+
Set the default handling of blank results provided by the user in certain dialogs. This will only be used
|
|
149
|
+
if the dialog functions have their own blank_result_handling parameter set to DEFAULT.
|
|
150
|
+
|
|
151
|
+
:param handling: The handling to use for blank results in dialogs.
|
|
152
|
+
"""
|
|
153
|
+
if not isinstance(handling, BlankResultHandling):
|
|
154
|
+
raise SapioException("Invalid blank result handling provided.")
|
|
155
|
+
if handling == BlankResultHandling.DEFAULT:
|
|
156
|
+
raise SapioException("Blank result handling cannot be set to DEFAULT.")
|
|
157
|
+
self._default_blank_result_handling = handling
|
|
158
|
+
|
|
119
159
|
def toaster_popup(self, message: str, title: str = "", popup_type: PopupType = PopupType.Info) -> None:
|
|
120
160
|
"""
|
|
121
161
|
Display a toaster popup in the bottom right corner of the user's screen.
|
|
@@ -177,7 +217,7 @@ class CallbackUtil:
|
|
|
177
217
|
# Send the request to the user.
|
|
178
218
|
request = OptionDialogRequest(title, msg, options, default_option, user_can_cancel,
|
|
179
219
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
180
|
-
response: int = self.
|
|
220
|
+
response: int = self.__send_dialog(request, self.callback.show_option_dialog)
|
|
181
221
|
return options[response]
|
|
182
222
|
|
|
183
223
|
def ok_dialog(self, title: str, msg: str) -> None:
|
|
@@ -193,7 +233,7 @@ class CallbackUtil:
|
|
|
193
233
|
def ok_cancel_dialog(self, title: str, msg: str, default_ok: bool = True) -> bool:
|
|
194
234
|
"""
|
|
195
235
|
Create an option dialog where the only options are "OK" and "Cancel". Doesn't allow the user to cancel the
|
|
196
|
-
dialog using the X
|
|
236
|
+
dialog using the X in the top right corner.
|
|
197
237
|
|
|
198
238
|
:param title: The title of the dialog.
|
|
199
239
|
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
@@ -205,7 +245,7 @@ class CallbackUtil:
|
|
|
205
245
|
def yes_no_dialog(self, title: str, msg: str, default_yes: bool = True) -> bool:
|
|
206
246
|
"""
|
|
207
247
|
Create an option dialog where the only options are "Yes" and "No". Doesn't allow the user to cancel the
|
|
208
|
-
dialog using the X
|
|
248
|
+
dialog using the X in the top right corner.
|
|
209
249
|
|
|
210
250
|
:param title: The title of the dialog.
|
|
211
251
|
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
@@ -214,16 +254,46 @@ class CallbackUtil:
|
|
|
214
254
|
"""
|
|
215
255
|
return self.option_dialog(title, msg, ["Yes", "No"], 0 if default_yes else 1, False) == "Yes"
|
|
216
256
|
|
|
257
|
+
# FR-47690: Added function.
|
|
258
|
+
def accept_decline_dialog(self, title: str, msg: str, default_accept: bool = True) -> bool:
|
|
259
|
+
"""
|
|
260
|
+
Create an option dialog where the only options are "Accept" and "Decline". Doesn't allow the user to cancel the
|
|
261
|
+
dialog using the X in the top right corner.
|
|
262
|
+
|
|
263
|
+
:param title: The title of the dialog.
|
|
264
|
+
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
265
|
+
:param default_accept: If true, "Accept" is the default choice. Otherwise, the default choice is "Decline".
|
|
266
|
+
:return: True if the user selected Accept. False if the user selected Decline.
|
|
267
|
+
"""
|
|
268
|
+
return self.option_dialog(title, msg, ["Accept", "Decline"], 0 if default_accept else 1, False) == "Accept"
|
|
269
|
+
|
|
270
|
+
# FR-47690: Added function.
|
|
271
|
+
def confirm_deny_dialog(self, title: str, msg: str, default_confirm: bool = True) -> bool:
|
|
272
|
+
"""
|
|
273
|
+
Create an option dialog where the only options are "Confirm" and "Deny". Doesn't allow the user to cancel the
|
|
274
|
+
dialog using the X in the top right corner.
|
|
275
|
+
|
|
276
|
+
:param title: The title of the dialog.
|
|
277
|
+
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
278
|
+
:param default_confirm: If true, "Confirm" is the default choice. Otherwise, the default choice is "Deny".
|
|
279
|
+
:return: True if the user selected Confirm. False if the user selected Deny.
|
|
280
|
+
"""
|
|
281
|
+
return self.option_dialog(title, msg, ["Confirm", "Deny"], 0 if default_confirm else 1, False) == "Confirm"
|
|
282
|
+
|
|
217
283
|
# CR-47310: Add a parameter to the list, input, selection, and e-sign dialog functions to control reprompting the
|
|
218
284
|
# user if no input/selection/valid credentials are provided.
|
|
285
|
+
# FR-47690: Added shortcut_single_option parameter. Updated with blank result handling behavior.
|
|
219
286
|
def list_dialog(self,
|
|
220
287
|
title: str,
|
|
221
288
|
options: Iterable[str],
|
|
222
289
|
multi_select: bool = False,
|
|
223
290
|
preselected_values: Iterable[str] | None = None,
|
|
224
291
|
*,
|
|
225
|
-
|
|
226
|
-
|
|
292
|
+
shortcut_single_option: bool = True,
|
|
293
|
+
require_selection = None,
|
|
294
|
+
blank_result_handling: BlankResultHandling = BlankResultHandling.DEFAULT,
|
|
295
|
+
repeat_message: str | None = "Please provide a selection to continue.",
|
|
296
|
+
cancel_message: str | None = "No selection was provided. Cancelling dialog.") -> list[str]:
|
|
227
297
|
"""
|
|
228
298
|
Create a list dialog with the given options for the user to choose from.
|
|
229
299
|
|
|
@@ -232,33 +302,237 @@ class CallbackUtil:
|
|
|
232
302
|
:param multi_select: Whether the user is able to select multiple options from the list.
|
|
233
303
|
:param preselected_values: A list of values that will already be selected when the list dialog is created. The
|
|
234
304
|
user can unselect these values if they want to.
|
|
235
|
-
:param
|
|
236
|
-
|
|
237
|
-
:param
|
|
238
|
-
|
|
305
|
+
:param shortcut_single_option: If true, then if the list contains only one option, the dialog will not be shown
|
|
306
|
+
and the single option will be returned immediately.
|
|
307
|
+
:param require_selection: DEPRECATED. Use blank_result_handling with a value of BlankResultHandling.REPEAT
|
|
308
|
+
instead.
|
|
309
|
+
:param blank_result_handling: Determine how to handle the result of a callback when the user provides a blank
|
|
310
|
+
result.
|
|
311
|
+
:param repeat_message: If blank_result_handling is REPEAT and a repeat_message is provided, then that message
|
|
312
|
+
appears as toaster text when the user provides a blank result.
|
|
313
|
+
:param cancel_message: If blank_result_handling is CANCEL and a cancel_message is provided, then that message
|
|
314
|
+
appears as toaster text when the user provides a blank result.
|
|
239
315
|
:return: The list of options that the user selected.
|
|
240
316
|
"""
|
|
241
317
|
if not options:
|
|
242
318
|
raise SapioException("No options provided.")
|
|
319
|
+
options = list(options)
|
|
320
|
+
if len(options) == 1 and shortcut_single_option:
|
|
321
|
+
return [options[0]]
|
|
243
322
|
|
|
244
323
|
# Send the request to the user.
|
|
245
|
-
request = ListDialogRequest(title, multi_select,
|
|
324
|
+
request = ListDialogRequest(title, multi_select, options,
|
|
246
325
|
list(preselected_values) if preselected_values else None,
|
|
247
326
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
248
327
|
|
|
249
|
-
# If require_selection is true
|
|
328
|
+
# Reverse compatibility: If require_selection is true and blank_result_handling is not set, then
|
|
329
|
+
# set blank_result_handling to REPEAT.
|
|
330
|
+
if require_selection is True and blank_result_handling == BlankResultHandling.DEFAULT:
|
|
331
|
+
blank_result_handling = BlankResultHandling.REPEAT
|
|
332
|
+
def not_blank_func(r: list[str]) -> bool:
|
|
333
|
+
return bool(r)
|
|
334
|
+
return self.__send_dialog_blank_results(request, self.callback.show_list_dialog, not_blank_func,
|
|
335
|
+
blank_result_handling, repeat_message, cancel_message)
|
|
336
|
+
|
|
337
|
+
# FR-47690: Updated with blank result handling behavior.
|
|
338
|
+
def input_dialog(self,
|
|
339
|
+
title: str,
|
|
340
|
+
msg: str,
|
|
341
|
+
field: AbstractVeloxFieldDefinition,
|
|
342
|
+
*,
|
|
343
|
+
require_input = None,
|
|
344
|
+
blank_result_handling: BlankResultHandling = BlankResultHandling.DEFAULT,
|
|
345
|
+
repeat_message: str | None = "Please provide a value to continue.",
|
|
346
|
+
cancel_message: str | None = "No input was provided. Cancelling dialog.") -> FieldValue:
|
|
347
|
+
"""
|
|
348
|
+
Create an input dialog where the user must input data for a singular field.
|
|
349
|
+
|
|
350
|
+
:param title: The title of the dialog.
|
|
351
|
+
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
352
|
+
:param field: The definition for a field that the user must provide input to.
|
|
353
|
+
:param require_input: DEPRECATED. Use blank_result_handling with a value of BlankResultHandling.REPEAT
|
|
354
|
+
instead.
|
|
355
|
+
:param blank_result_handling: Determine how to handle the result of a callback when the user provides a blank
|
|
356
|
+
result.
|
|
357
|
+
:param repeat_message: If blank_result_handling is REPEAT and a repeat_message is provided, then that message
|
|
358
|
+
appears as toaster text when the user provides a blank result.
|
|
359
|
+
:param cancel_message: If blank_result_handling is CANCEL and a cancel_message is provided, then that message
|
|
360
|
+
appears as toaster text when the user provides a blank result.
|
|
361
|
+
:return: The response value from the user for the given field.
|
|
362
|
+
"""
|
|
363
|
+
# Send the request to the user.
|
|
364
|
+
request = InputDialogCriteria(title, msg, field,
|
|
365
|
+
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
366
|
+
|
|
367
|
+
# Reverse compatibility: If require_selection is true and blank_result_handling is not set, then
|
|
368
|
+
# set blank_result_handling to REPEAT.
|
|
369
|
+
handling = blank_result_handling
|
|
370
|
+
if require_input is True and handling == BlankResultHandling.DEFAULT:
|
|
371
|
+
handling = BlankResultHandling.REPEAT
|
|
372
|
+
if handling == BlankResultHandling.DEFAULT or handling is None:
|
|
373
|
+
handling = self._default_blank_result_handling
|
|
374
|
+
|
|
250
375
|
while True:
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
376
|
+
try:
|
|
377
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
378
|
+
# It's not possible to distinguish between the user cancelling this dialog and submitting the dialog
|
|
379
|
+
# with no input if the ClientCallback show_input_dialog function is used, as both cases just return
|
|
380
|
+
# None. Therefore, in order to be able to make that distinction, we need to call the endpoint without
|
|
381
|
+
# ClientCallback and get the raw response object.
|
|
382
|
+
raw_response = self.user.post('/clientcallback/showInputDialog', payload=request.to_json())
|
|
383
|
+
# A response status code of 204 is what represents a cancelled dialog.
|
|
384
|
+
if raw_response.status_code == 204:
|
|
385
|
+
raise SapioUserCancelledException()
|
|
386
|
+
self.user.raise_for_status(raw_response)
|
|
387
|
+
json_dct: dict | None = self.user.get_json_data_or_none(raw_response)
|
|
388
|
+
response: FieldValue = json_dct['result'] if json_dct else None
|
|
389
|
+
except ReadTimeout:
|
|
390
|
+
raise SapioDialogTimeoutException()
|
|
391
|
+
finally:
|
|
392
|
+
self.user.timeout_seconds = self._original_timeout
|
|
393
|
+
|
|
394
|
+
# String fields that the user didn't provide will return as an empty string instead of a None response.
|
|
395
|
+
is_str: bool = isinstance(response, str)
|
|
396
|
+
if (is_str and response) or (not is_str and response is not None):
|
|
397
|
+
return response
|
|
398
|
+
|
|
399
|
+
match handling:
|
|
400
|
+
case BlankResultHandling.CANCEL:
|
|
401
|
+
# If the user provided no selection, throw an exception.
|
|
402
|
+
if cancel_message:
|
|
403
|
+
self.toaster_popup(cancel_message, popup_type=PopupType.Warning)
|
|
404
|
+
raise SapioUserCancelledException()
|
|
405
|
+
case BlankResultHandling.REPEAT:
|
|
406
|
+
# If the user provided no selection, repeat the dialog.
|
|
407
|
+
# If a repeatMessage is provided, display it as a toaster popup.
|
|
408
|
+
if repeat_message:
|
|
409
|
+
self.toaster_popup(repeat_message, popup_type=PopupType.Warning)
|
|
410
|
+
case BlankResultHandling.RETURN:
|
|
411
|
+
# If the user provided no selection, return the blank result.
|
|
412
|
+
return response
|
|
413
|
+
|
|
414
|
+
def string_input_dialog(self,
|
|
415
|
+
title: str,
|
|
416
|
+
msg: str,
|
|
417
|
+
field_name: str,
|
|
418
|
+
default_value: str | None = None,
|
|
419
|
+
max_length: int | None = None,
|
|
420
|
+
editable: bool = True,
|
|
421
|
+
*,
|
|
422
|
+
require_input: bool = False,
|
|
423
|
+
repeat_message: str | None = "Please provide a value to continue.",
|
|
424
|
+
**kwargs) -> str:
|
|
425
|
+
"""
|
|
426
|
+
Create an input dialog where the user must input data for a singular text field.
|
|
427
|
+
|
|
428
|
+
:param title: The title of the dialog.
|
|
429
|
+
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
430
|
+
:param field_name: The name and display name of the string field.
|
|
431
|
+
:param default_value: The default value to place into the string field, if any.
|
|
432
|
+
:param max_length: The max length of the string value. If not provided, uses the length of the default value.
|
|
433
|
+
If neither this nor a default value are provided, defaults to 100 characters.
|
|
434
|
+
:param editable: Whether the field is editable by the user.
|
|
435
|
+
:param require_input: If true, the request will be re-sent if the user submits the dialog without making
|
|
436
|
+
a selection.
|
|
437
|
+
:param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
|
|
438
|
+
as toaster text if the dialog is repeated.
|
|
439
|
+
:param kwargs: Any additional keyword arguments to pass to the field definition.
|
|
440
|
+
:return: The string that the user input into the dialog.
|
|
441
|
+
"""
|
|
442
|
+
# FR-47690: Deprecated in favor of suggesting the use of the FieldBuilder to customize an input_dialog's field.
|
|
443
|
+
warnings.warn("Deprecated. Use the base input_dialog function and the FieldBuilder class to construct the "
|
|
444
|
+
"input field.", DeprecationWarning)
|
|
445
|
+
if max_length is None:
|
|
446
|
+
max_length = len(default_value) if default_value else 100
|
|
447
|
+
field = VeloxStringFieldDefinition("Input", field_name, field_name, default_value=default_value,
|
|
448
|
+
max_length=max_length, editable=editable, **kwargs)
|
|
449
|
+
return self.input_dialog(title, msg, field,
|
|
450
|
+
require_input=require_input, repeat_message=repeat_message)
|
|
451
|
+
|
|
452
|
+
def integer_input_dialog(self,
|
|
453
|
+
title: str,
|
|
454
|
+
msg: str,
|
|
455
|
+
field_name: str,
|
|
456
|
+
default_value: int = None,
|
|
457
|
+
min_value: int = -10000,
|
|
458
|
+
max_value: int = 10000,
|
|
459
|
+
editable: bool = True,
|
|
460
|
+
*,
|
|
461
|
+
require_input: bool = False,
|
|
462
|
+
repeat_message: str | None = "Please provide a value to continue.",
|
|
463
|
+
**kwargs) -> int:
|
|
464
|
+
"""
|
|
465
|
+
Create an input dialog where the user must input data for a singular integer field.
|
|
466
|
+
|
|
467
|
+
:param title: The title of the dialog.
|
|
468
|
+
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
469
|
+
:param field_name: The name and display name of the integer field.
|
|
470
|
+
:param default_value: The default value to place into the integer field. If not provided, defaults to the 0 or
|
|
471
|
+
the minimum value, whichever is higher.
|
|
472
|
+
:param min_value: The minimum allowed value of the input.
|
|
473
|
+
:param max_value: The maximum allowed value of the input.
|
|
474
|
+
:param editable: Whether the field is editable by the user.
|
|
475
|
+
:param require_input: If true, the request will be re-sent if the user submits the dialog without making
|
|
476
|
+
a selection.
|
|
477
|
+
:param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
|
|
478
|
+
as toaster text if the dialog is repeated.
|
|
479
|
+
:param kwargs: Any additional keyword arguments to pass to the field definition.
|
|
480
|
+
:return: The integer that the user input into the dialog.
|
|
481
|
+
"""
|
|
482
|
+
# FR-47690: Deprecated in favor of suggesting the use of the FieldBuilder to customize an input_dialog's field.
|
|
483
|
+
warnings.warn("Deprecated. Use the base input_dialog function and the FieldBuilder class to construct the "
|
|
484
|
+
"input field.", DeprecationWarning)
|
|
485
|
+
if default_value is None:
|
|
486
|
+
default_value = max(0, min_value)
|
|
487
|
+
field = VeloxIntegerFieldDefinition("Input", field_name, field_name, default_value=default_value,
|
|
488
|
+
min_value=min_value, max_value=max_value, editable=editable, **kwargs)
|
|
489
|
+
return self.input_dialog(title, msg, field,
|
|
490
|
+
require_input=require_input, repeat_message=repeat_message)
|
|
491
|
+
|
|
492
|
+
def double_input_dialog(self,
|
|
493
|
+
title: str,
|
|
494
|
+
msg: str,
|
|
495
|
+
field_name: str,
|
|
496
|
+
default_value: float = None,
|
|
497
|
+
min_value: float = -10000000,
|
|
498
|
+
max_value: float = 100000000,
|
|
499
|
+
editable: bool = True,
|
|
500
|
+
*,
|
|
501
|
+
require_input: bool = False,
|
|
502
|
+
repeat_message: str | None = "Please provide a value to continue.",
|
|
503
|
+
**kwargs) -> float:
|
|
504
|
+
"""
|
|
505
|
+
Create an input dialog where the user must input data for a singular double field.
|
|
506
|
+
|
|
507
|
+
:param title: The title of the dialog.
|
|
508
|
+
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
509
|
+
:param field_name: The name and display name of the double field.
|
|
510
|
+
:param default_value: The default value to place into the double field. If not provided, defaults to the 0 or
|
|
511
|
+
the minimum value, whichever is higher.
|
|
512
|
+
:param min_value: The minimum allowed value of the input.
|
|
513
|
+
:param max_value: The maximum allowed value of the input.
|
|
514
|
+
:param editable: Whether the field is editable by the user.
|
|
515
|
+
:param require_input: If true, the request will be re-sent if the user submits the dialog without making
|
|
516
|
+
a selection.
|
|
517
|
+
:param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
|
|
518
|
+
as toaster text if the dialog is repeated.
|
|
519
|
+
:param kwargs: Any additional keyword arguments to pass to the field definition.
|
|
520
|
+
:return: The float that the user input into the dialog.
|
|
521
|
+
"""
|
|
522
|
+
# FR-47690: Deprecated in favor of suggesting the use of the FieldBuilder to customize an input_dialog's field.
|
|
523
|
+
warnings.warn("Deprecated. Use the base input_dialog function and the FieldBuilder class to construct the "
|
|
524
|
+
"input field.", DeprecationWarning)
|
|
525
|
+
if default_value is None:
|
|
526
|
+
default_value = max(0., min_value)
|
|
527
|
+
field = VeloxDoubleFieldDefinition("Input", field_name, field_name, default_value=default_value,
|
|
528
|
+
min_value=min_value, max_value=max_value, editable=editable, **kwargs)
|
|
529
|
+
return self.input_dialog(title, msg, field,
|
|
530
|
+
require_input=require_input, repeat_message=repeat_message)
|
|
257
531
|
|
|
258
532
|
def form_dialog(self,
|
|
259
533
|
title: str,
|
|
260
534
|
msg: str,
|
|
261
|
-
fields:
|
|
535
|
+
fields: Iterable[AbstractVeloxFieldDefinition],
|
|
262
536
|
values: FieldMap = None,
|
|
263
537
|
column_positions: dict[str, tuple[int, int]] = None,
|
|
264
538
|
*,
|
|
@@ -273,7 +547,8 @@ class CallbackUtil:
|
|
|
273
547
|
:param msg: The message to display at the top of the form. This can be formatted using HTML elements.
|
|
274
548
|
:param fields: The definitions of the fields to display in the form. Fields will be displayed in the order they
|
|
275
549
|
are provided in this list.
|
|
276
|
-
:param values: Sets the default values of the fields.
|
|
550
|
+
:param values: Sets the default values of the fields. If a field name from the fields parameter is not
|
|
551
|
+
provided in this dictionary, it will be initialized with its default value.
|
|
277
552
|
:param column_positions: If a tuple is provided for a field name, alters that field's column position and column
|
|
278
553
|
span. (Field order is still determined by the fields list.)
|
|
279
554
|
:param data_type: The data type name for the temporary data type that will be created for this form.
|
|
@@ -287,16 +562,23 @@ class CallbackUtil:
|
|
|
287
562
|
# Build a temporary data type for the request.
|
|
288
563
|
temp_dt = self.__temp_dt_from_field_defs(data_type, display_name, plural_display_name, fields, column_positions)
|
|
289
564
|
|
|
565
|
+
# FR-47690: Set default values for fields that aren't present.
|
|
566
|
+
if values is None:
|
|
567
|
+
values = {}
|
|
568
|
+
for field in fields:
|
|
569
|
+
if field.data_field_name not in values:
|
|
570
|
+
values[field.data_field_name] = field.default_value
|
|
571
|
+
|
|
290
572
|
# Send the request to the user.
|
|
291
573
|
request = FormEntryDialogRequest(title, msg, temp_dt, values,
|
|
292
574
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
293
|
-
response: FieldMap = self.
|
|
575
|
+
response: FieldMap = self.__send_dialog(request, self.callback.show_form_entry_dialog)
|
|
294
576
|
return response
|
|
295
577
|
|
|
296
578
|
def record_form_dialog(self,
|
|
297
579
|
title: str,
|
|
298
580
|
msg: str,
|
|
299
|
-
fields:
|
|
581
|
+
fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
|
|
300
582
|
record: SapioRecord,
|
|
301
583
|
column_positions: dict[str, tuple[int, int]] | None = None,
|
|
302
584
|
editable=None,
|
|
@@ -362,14 +644,14 @@ class CallbackUtil:
|
|
|
362
644
|
# Send the request to the user.
|
|
363
645
|
request = FormEntryDialogRequest(title, msg, temp_dt, values,
|
|
364
646
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
365
|
-
response: FieldMap = self.
|
|
647
|
+
response: FieldMap = self.__send_dialog(request, self.callback.show_form_entry_dialog)
|
|
366
648
|
return response
|
|
367
649
|
|
|
368
650
|
# FR-47314: Create record form and table dialogs for updating or creating records.
|
|
369
651
|
def set_record_form_dialog(self,
|
|
370
652
|
title: str,
|
|
371
653
|
msg: str,
|
|
372
|
-
fields:
|
|
654
|
+
fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
|
|
373
655
|
record: SapioRecord,
|
|
374
656
|
column_positions: dict[str, tuple[int, int]] | None = None,
|
|
375
657
|
*,
|
|
@@ -412,7 +694,7 @@ class CallbackUtil:
|
|
|
412
694
|
def create_record_form_dialog(self,
|
|
413
695
|
title: str,
|
|
414
696
|
msg: str,
|
|
415
|
-
fields:
|
|
697
|
+
fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
|
|
416
698
|
wrapper_type: type[WrappedType] | str,
|
|
417
699
|
column_positions: dict[str, tuple[int, int]] | None = None,
|
|
418
700
|
*,
|
|
@@ -456,176 +738,17 @@ class CallbackUtil:
|
|
|
456
738
|
default_modifier=default_modifier, field_modifiers=field_modifiers)
|
|
457
739
|
return record
|
|
458
740
|
|
|
459
|
-
def input_dialog(self,
|
|
460
|
-
title: str,
|
|
461
|
-
msg: str,
|
|
462
|
-
field: AbstractVeloxFieldDefinition,
|
|
463
|
-
*,
|
|
464
|
-
require_input: bool = False,
|
|
465
|
-
repeat_message: str | None = "Please provide a value to continue.") -> FieldValue:
|
|
466
|
-
"""
|
|
467
|
-
Create an input dialog where the user must input data for a singular field.
|
|
468
|
-
|
|
469
|
-
:param title: The title of the dialog.
|
|
470
|
-
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
471
|
-
:param field: The definition for a field that the user must provide input to.
|
|
472
|
-
:param require_input: If true, the request will be re-sent if the user submits the dialog without providing an
|
|
473
|
-
input field value.
|
|
474
|
-
:param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
|
|
475
|
-
as toaster text if the dialog is repeated.
|
|
476
|
-
:return: The response value from the user for the given field.
|
|
477
|
-
"""
|
|
478
|
-
# Send the request to the user.
|
|
479
|
-
request = InputDialogCriteria(title, msg, field,
|
|
480
|
-
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
481
|
-
|
|
482
|
-
# If require_input is true, repeat the request if the user didn't provide a field value.
|
|
483
|
-
while True:
|
|
484
|
-
try:
|
|
485
|
-
self.user.timeout_seconds = self.timeout_seconds
|
|
486
|
-
# It's not possible to distinguish between the user cancelling this dialog and submitting the dialog
|
|
487
|
-
# with no input if the ClientCallback show_input_dialog function is used, as both cases just return
|
|
488
|
-
# None. Therefore, in order to be able to make that distinction, we need to call the endpoint without
|
|
489
|
-
# ClientCallback and get the raw response object.
|
|
490
|
-
raw_response = self.user.post('/clientcallback/showInputDialog', payload=request.to_json())
|
|
491
|
-
# A response status code of 204 is what represents a cancelled dialog.
|
|
492
|
-
if raw_response.status_code == 204:
|
|
493
|
-
raise SapioUserCancelledException()
|
|
494
|
-
self.user.raise_for_status(raw_response)
|
|
495
|
-
json_dct: dict | None = self.user.get_json_data_or_none(raw_response)
|
|
496
|
-
response: FieldValue = json_dct['result'] if json_dct else None
|
|
497
|
-
except ReadTimeout:
|
|
498
|
-
raise SapioDialogTimeoutException()
|
|
499
|
-
finally:
|
|
500
|
-
self.user.timeout_seconds = self._original_timeout
|
|
501
|
-
# String fields that the user didn't provide will return as an empty string instead of a None response.
|
|
502
|
-
is_str: bool = isinstance(response, str)
|
|
503
|
-
if not require_input or (is_str and response) or (not is_str and response is not None):
|
|
504
|
-
break
|
|
505
|
-
if repeat_message:
|
|
506
|
-
self.toaster_popup(repeat_message, popup_type=PopupType.Warning)
|
|
507
|
-
return response
|
|
508
|
-
|
|
509
|
-
def string_input_dialog(self,
|
|
510
|
-
title: str,
|
|
511
|
-
msg: str,
|
|
512
|
-
field_name: str,
|
|
513
|
-
default_value: str | None = None,
|
|
514
|
-
max_length: int | None = None,
|
|
515
|
-
editable: bool = True,
|
|
516
|
-
*,
|
|
517
|
-
require_input: bool = False,
|
|
518
|
-
repeat_message: str | None = "Please provide a value to continue.",
|
|
519
|
-
**kwargs) -> str:
|
|
520
|
-
"""
|
|
521
|
-
Create an input dialog where the user must input data for a singular text field.
|
|
522
|
-
|
|
523
|
-
:param title: The title of the dialog.
|
|
524
|
-
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
525
|
-
:param field_name: The name and display name of the string field.
|
|
526
|
-
:param default_value: The default value to place into the string field, if any.
|
|
527
|
-
:param max_length: The max length of the string value. If not provided, uses the length of the default value.
|
|
528
|
-
If neither this nor a default value are provided, defaults to 100 characters.
|
|
529
|
-
:param editable: Whether the field is editable by the user.
|
|
530
|
-
:param require_input: If true, the request will be re-sent if the user submits the dialog without making
|
|
531
|
-
a selection.
|
|
532
|
-
:param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
|
|
533
|
-
as toaster text if the dialog is repeated.
|
|
534
|
-
:param kwargs: Any additional keyword arguments to pass to the field definition.
|
|
535
|
-
:return: The string that the user input into the dialog.
|
|
536
|
-
"""
|
|
537
|
-
if max_length is None:
|
|
538
|
-
max_length = len(default_value) if default_value else 100
|
|
539
|
-
field = VeloxStringFieldDefinition("Input", field_name, field_name, default_value=default_value,
|
|
540
|
-
max_length=max_length, editable=editable, **kwargs)
|
|
541
|
-
return self.input_dialog(title, msg, field,
|
|
542
|
-
require_input=require_input, repeat_message=repeat_message)
|
|
543
|
-
|
|
544
|
-
def integer_input_dialog(self,
|
|
545
|
-
title: str,
|
|
546
|
-
msg: str,
|
|
547
|
-
field_name: str,
|
|
548
|
-
default_value: int = None,
|
|
549
|
-
min_value: int = -10000,
|
|
550
|
-
max_value: int = 10000,
|
|
551
|
-
editable: bool = True,
|
|
552
|
-
*,
|
|
553
|
-
require_input: bool = False,
|
|
554
|
-
repeat_message: str | None = "Please provide a value to continue.",
|
|
555
|
-
**kwargs) -> int:
|
|
556
|
-
"""
|
|
557
|
-
Create an input dialog where the user must input data for a singular integer field.
|
|
558
|
-
|
|
559
|
-
:param title: The title of the dialog.
|
|
560
|
-
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
561
|
-
:param field_name: The name and display name of the integer field.
|
|
562
|
-
:param default_value: The default value to place into the integer field. If not provided, defaults to the 0 or
|
|
563
|
-
the minimum value, whichever is higher.
|
|
564
|
-
:param min_value: The minimum allowed value of the input.
|
|
565
|
-
:param max_value: The maximum allowed value of the input.
|
|
566
|
-
:param editable: Whether the field is editable by the user.
|
|
567
|
-
:param require_input: If true, the request will be re-sent if the user submits the dialog without making
|
|
568
|
-
a selection.
|
|
569
|
-
:param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
|
|
570
|
-
as toaster text if the dialog is repeated.
|
|
571
|
-
:param kwargs: Any additional keyword arguments to pass to the field definition.
|
|
572
|
-
:return: The integer that the user input into the dialog.
|
|
573
|
-
"""
|
|
574
|
-
if default_value is None:
|
|
575
|
-
default_value = max(0, min_value)
|
|
576
|
-
field = VeloxIntegerFieldDefinition("Input", field_name, field_name, default_value=default_value,
|
|
577
|
-
min_value=min_value, max_value=max_value, editable=editable, **kwargs)
|
|
578
|
-
return self.input_dialog(title, msg, field,
|
|
579
|
-
require_input=require_input, repeat_message=repeat_message)
|
|
580
|
-
|
|
581
|
-
def double_input_dialog(self,
|
|
582
|
-
title: str,
|
|
583
|
-
msg: str,
|
|
584
|
-
field_name: str,
|
|
585
|
-
default_value: float = None,
|
|
586
|
-
min_value: float = -10000000,
|
|
587
|
-
max_value: float = 100000000,
|
|
588
|
-
editable: bool = True,
|
|
589
|
-
*,
|
|
590
|
-
require_input: bool = False,
|
|
591
|
-
repeat_message: str | None = "Please provide a value to continue.",
|
|
592
|
-
**kwargs) -> float:
|
|
593
|
-
"""
|
|
594
|
-
Create an input dialog where the user must input data for a singular double field.
|
|
595
|
-
|
|
596
|
-
:param title: The title of the dialog.
|
|
597
|
-
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
598
|
-
:param field_name: The name and display name of the double field.
|
|
599
|
-
:param default_value: The default value to place into the double field. If not provided, defaults to the 0 or
|
|
600
|
-
the minimum value, whichever is higher.
|
|
601
|
-
:param min_value: The minimum allowed value of the input.
|
|
602
|
-
:param max_value: The maximum allowed value of the input.
|
|
603
|
-
:param editable: Whether the field is editable by the user.
|
|
604
|
-
:param require_input: If true, the request will be re-sent if the user submits the dialog without making
|
|
605
|
-
a selection.
|
|
606
|
-
:param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
|
|
607
|
-
as toaster text if the dialog is repeated.
|
|
608
|
-
:param kwargs: Any additional keyword arguments to pass to the field definition.
|
|
609
|
-
:return: The float that the user input into the dialog.
|
|
610
|
-
"""
|
|
611
|
-
if default_value is None:
|
|
612
|
-
default_value = max(0., min_value)
|
|
613
|
-
field = VeloxDoubleFieldDefinition("Input", field_name, field_name, default_value=default_value,
|
|
614
|
-
min_value=min_value, max_value=max_value, editable=editable, **kwargs)
|
|
615
|
-
return self.input_dialog(title, msg, field,
|
|
616
|
-
require_input=require_input, repeat_message=repeat_message)
|
|
617
|
-
|
|
618
741
|
def table_dialog(self,
|
|
619
742
|
title: str,
|
|
620
743
|
msg: str,
|
|
621
|
-
fields:
|
|
622
|
-
values:
|
|
744
|
+
fields: Iterable[AbstractVeloxFieldDefinition],
|
|
745
|
+
values: Iterable[FieldMap] | int,
|
|
623
746
|
*,
|
|
624
747
|
data_type: DataTypeIdentifier = "Default",
|
|
625
748
|
display_name: str | None = None,
|
|
626
749
|
plural_display_name: str | None = None,
|
|
627
750
|
group_by: FieldIdentifier | None = None,
|
|
628
|
-
image_data:
|
|
751
|
+
image_data: Iterable[bytes] | None = None) -> list[FieldMap]:
|
|
629
752
|
"""
|
|
630
753
|
Create a table dialog where the user may input data into the fields of the table. Requires that the caller
|
|
631
754
|
provide the definitions of every field in the table.
|
|
@@ -634,7 +757,8 @@ class CallbackUtil:
|
|
|
634
757
|
:param msg: The message to display at the top of the form. This can be formatted using HTML elements.
|
|
635
758
|
:param fields: The definitions of the fields to display as table columns. Fields will be displayed in the order
|
|
636
759
|
they are provided in this list.
|
|
637
|
-
:param values: The values to set for each row of the table.
|
|
760
|
+
:param values: The values to set for each row of the table. If an integer is provided, it is treated as the
|
|
761
|
+
number of rows to create in the table, with each row using the default values of the field definitions.
|
|
638
762
|
:param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
|
|
639
763
|
The user may remove this grouping if they want to.
|
|
640
764
|
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
@@ -647,9 +771,18 @@ class CallbackUtil:
|
|
|
647
771
|
:return: A list of dictionaries mapping the data field names of the given field definitions to the response
|
|
648
772
|
value from the user for that field for each row.
|
|
649
773
|
"""
|
|
774
|
+
# FR-47690: Accept an integer as the values parameter to create a table with that many rows.
|
|
775
|
+
if isinstance(values, int):
|
|
776
|
+
values: list[dict[str, Any]] = [{} for _ in range(values)]
|
|
650
777
|
if not values:
|
|
651
778
|
raise SapioException("No values provided.")
|
|
652
779
|
|
|
780
|
+
# FR-47690: Set default values for fields that aren't present.
|
|
781
|
+
for row in values:
|
|
782
|
+
for field in fields:
|
|
783
|
+
if field.data_field_name not in values:
|
|
784
|
+
row[field.data_field_name] = field.default_value
|
|
785
|
+
|
|
653
786
|
# Convert the group_by parameter to a field name.
|
|
654
787
|
if group_by is not None:
|
|
655
788
|
group_by: str = AliasUtil.to_data_field_name(group_by)
|
|
@@ -660,23 +793,24 @@ class CallbackUtil:
|
|
|
660
793
|
temp_dt.record_image_assignable = bool(image_data)
|
|
661
794
|
|
|
662
795
|
# Send the request to the user.
|
|
663
|
-
request = TableEntryDialogRequest(title, msg, temp_dt, values,
|
|
796
|
+
request = TableEntryDialogRequest(title, msg, temp_dt, list(values),
|
|
664
797
|
record_image_data_list=image_data, group_by_field=group_by,
|
|
665
798
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
666
|
-
response: list[FieldMap] = self.
|
|
799
|
+
response: list[FieldMap] = self.__send_dialog(request, self.callback.show_table_entry_dialog)
|
|
667
800
|
return response
|
|
668
801
|
|
|
669
802
|
def record_table_dialog(self,
|
|
670
803
|
title: str,
|
|
671
804
|
msg: str,
|
|
672
|
-
fields:
|
|
673
|
-
records:
|
|
805
|
+
fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
|
|
806
|
+
records: Iterable[SapioRecord],
|
|
674
807
|
editable=None,
|
|
675
808
|
*,
|
|
676
809
|
default_modifier: FieldModifier | None = None,
|
|
677
810
|
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
|
|
678
811
|
group_by: FieldIdentifier | None = None,
|
|
679
|
-
image_data:
|
|
812
|
+
image_data: Iterable[bytes] | None = None,
|
|
813
|
+
index_field: str | None = None) -> list[FieldMap]:
|
|
680
814
|
"""
|
|
681
815
|
Create a table dialog where the user may input data into the fields of the table. The table is constructed from
|
|
682
816
|
a given list of records of a singular type.
|
|
@@ -706,6 +840,12 @@ class CallbackUtil:
|
|
|
706
840
|
The user may remove this grouping if they want to.
|
|
707
841
|
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
708
842
|
the image data list corresponds to the element at the same index in the records list.
|
|
843
|
+
:param index_field: If provided, the returned field maps will contain a field with this name that is equal to
|
|
844
|
+
the record ID of the record at the same index in the records list. This can be used to map the results
|
|
845
|
+
back to the original records. This is used instead of using a RecordId field, as the RecordId field has
|
|
846
|
+
special behavior in the system that can cause issues if the given records are uncommitted record models
|
|
847
|
+
with negative record IDs, meaning we don't want to have a RecordId field in the field maps provided to the
|
|
848
|
+
system.
|
|
709
849
|
:return: A list of dictionaries mapping the data field names of the given field definitions to the response
|
|
710
850
|
value from the user for that field for each row.
|
|
711
851
|
"""
|
|
@@ -717,7 +857,10 @@ class CallbackUtil:
|
|
|
717
857
|
if not records:
|
|
718
858
|
raise SapioException("No records provided.")
|
|
719
859
|
data_type: str = AliasUtil.to_singular_data_type_name(records)
|
|
720
|
-
|
|
860
|
+
if index_field is not None:
|
|
861
|
+
field_map_list: list[FieldMap] = self.__get_indexed_field_maps(records, index_field)
|
|
862
|
+
else:
|
|
863
|
+
field_map_list: list[FieldMap] = AliasUtil.to_field_map_list(records)
|
|
721
864
|
|
|
722
865
|
# Convert the group_by parameter to a field name.
|
|
723
866
|
if group_by is not None:
|
|
@@ -743,20 +886,20 @@ class CallbackUtil:
|
|
|
743
886
|
request = TableEntryDialogRequest(title, msg, temp_dt, field_map_list,
|
|
744
887
|
record_image_data_list=image_data, group_by_field=group_by,
|
|
745
888
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
746
|
-
response: list[FieldMap] = self.
|
|
889
|
+
response: list[FieldMap] = self.__send_dialog(request, self.callback.show_table_entry_dialog)
|
|
747
890
|
return response
|
|
748
891
|
|
|
749
892
|
# FR-47314: Create record form and table dialogs for updating or creating records.
|
|
750
893
|
def set_record_table_dialog(self,
|
|
751
894
|
title: str,
|
|
752
895
|
msg: str,
|
|
753
|
-
fields:
|
|
754
|
-
records:
|
|
896
|
+
fields: Iterable[FieldValue] | DataTypeLayoutIdentifier,
|
|
897
|
+
records: Iterable[SapioRecord],
|
|
755
898
|
*,
|
|
756
899
|
default_modifier: FieldModifier | None = None,
|
|
757
900
|
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
|
|
758
901
|
group_by: FieldIdentifier | None = None,
|
|
759
|
-
image_data:
|
|
902
|
+
image_data: Iterable[bytes] | None = None):
|
|
760
903
|
"""
|
|
761
904
|
Create a table dialog where the user may input data into the fields of the table. The table is constructed from
|
|
762
905
|
a given list of records of a singular type. After the user submits this dialog, the values that the user
|
|
@@ -787,27 +930,35 @@ class CallbackUtil:
|
|
|
787
930
|
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
788
931
|
the image data list corresponds to the element at the same index in the records list.
|
|
789
932
|
"""
|
|
933
|
+
# Index the records with a field name that is the current time in milliseconds. This is done to avoid
|
|
934
|
+
# collisions with any existing field names.
|
|
935
|
+
index_field: str = f"_{TimeUtil.now_in_millis()}"
|
|
790
936
|
results: list[FieldMap] = self.record_table_dialog(title, msg, fields, records,
|
|
791
937
|
default_modifier=default_modifier,
|
|
792
938
|
field_modifiers=field_modifiers,
|
|
793
|
-
group_by=group_by, image_data=image_data
|
|
939
|
+
group_by=group_by, image_data=image_data,
|
|
940
|
+
index_field=index_field)
|
|
794
941
|
records_by_id: dict[int, SapioRecord] = self.rec_handler.map_by_id(records)
|
|
795
942
|
for result in results:
|
|
796
|
-
|
|
943
|
+
index: int = result.pop(index_field)
|
|
944
|
+
records_by_id[index].set_field_values(result)
|
|
797
945
|
|
|
946
|
+
# FR-47690: Updated with blank result handling behavior.
|
|
798
947
|
def create_record_table_dialog(self,
|
|
799
948
|
title: str,
|
|
800
949
|
msg: str,
|
|
801
|
-
fields:
|
|
950
|
+
fields: Iterable[FieldValue] | DataTypeLayoutIdentifier,
|
|
802
951
|
wrapper_type: type[WrappedType] | str,
|
|
803
952
|
count: int | tuple[int, int],
|
|
804
953
|
*,
|
|
805
954
|
default_modifier: FieldModifier | None = None,
|
|
806
955
|
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
|
|
807
956
|
group_by: FieldIdentifier | None = None,
|
|
808
|
-
image_data:
|
|
809
|
-
require_input
|
|
810
|
-
|
|
957
|
+
image_data: Iterable[bytes] | None = None,
|
|
958
|
+
require_input = None,
|
|
959
|
+
blank_result_handling: BlankResultHandling = BlankResultHandling.DEFAULT,
|
|
960
|
+
repeat_message: str | None = "Please provide a value to continue.",
|
|
961
|
+
cancel_message: str | None = "No value was provided. Cancelling dialog.") \
|
|
811
962
|
-> list[WrappedType] | list[PyRecordModel]:
|
|
812
963
|
"""
|
|
813
964
|
Create a table dialog where the user may input data into the fields of the table. The table is constructed from
|
|
@@ -841,13 +992,18 @@ class CallbackUtil:
|
|
|
841
992
|
The user may remove this grouping if they want to.
|
|
842
993
|
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
843
994
|
the image data list corresponds to the element at the same index in the records list.
|
|
844
|
-
:param require_input:
|
|
845
|
-
|
|
846
|
-
:param
|
|
847
|
-
|
|
995
|
+
:param require_input: DEPRECATED. Use blank_result_handling with a value of BlankResultHandling.REPEAT
|
|
996
|
+
instead.
|
|
997
|
+
:param blank_result_handling: Determine how to handle the result of a callback when the user provides a blank
|
|
998
|
+
result.
|
|
999
|
+
:param repeat_message: If blank_result_handling is REPEAT and a repeat_message is provided, then that message
|
|
1000
|
+
appears as toaster text when the user provides a blank result.
|
|
1001
|
+
:param cancel_message: If blank_result_handling is CANCEL and a cancel_message is provided, then that message
|
|
1002
|
+
appears as toaster text when the user provides a blank result.
|
|
848
1003
|
:return: A list of the newly created records.
|
|
849
1004
|
"""
|
|
850
|
-
count: int = self.__prompt_for_count(count, wrapper_type, require_input, repeat_message
|
|
1005
|
+
count: int = self.__prompt_for_count(count, wrapper_type, require_input, blank_result_handling, repeat_message,
|
|
1006
|
+
cancel_message)
|
|
851
1007
|
if count <= 0:
|
|
852
1008
|
return []
|
|
853
1009
|
records: list[WrappedType] | list[PyRecordModel] = self.rec_handler.add_models(wrapper_type, count)
|
|
@@ -860,14 +1016,15 @@ class CallbackUtil:
|
|
|
860
1016
|
def record_adaptive_dialog(self,
|
|
861
1017
|
title: str,
|
|
862
1018
|
msg: str,
|
|
863
|
-
fields:
|
|
864
|
-
records:
|
|
1019
|
+
fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
|
|
1020
|
+
records: Collection[SapioRecord],
|
|
865
1021
|
*,
|
|
866
1022
|
default_modifier: FieldModifier | None = None,
|
|
867
1023
|
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
|
|
868
1024
|
column_positions: dict[str, tuple[int, int]] | None = None,
|
|
869
1025
|
group_by: FieldIdentifier | None = None,
|
|
870
|
-
image_data:
|
|
1026
|
+
image_data: Iterable[bytes] | None = None,
|
|
1027
|
+
index_field: str | None = None) -> list[FieldMap]:
|
|
871
1028
|
"""
|
|
872
1029
|
Create a dialog where the user may input data into the specified fields. The dialog is constructed from
|
|
873
1030
|
a given list of records of a singular type.
|
|
@@ -903,6 +1060,12 @@ class CallbackUtil:
|
|
|
903
1060
|
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
904
1061
|
the image data list corresponds to the element at the same index in the records list. Only used if the
|
|
905
1062
|
adaptive dialog becomes a table.
|
|
1063
|
+
:param index_field: If provided, the returned field maps will contain a field with this name that is equal to
|
|
1064
|
+
the record ID of the record at the same index in the records list. This can be used to map the results
|
|
1065
|
+
back to the original records. This is used instead of using a RecordId field, as the RecordId field has
|
|
1066
|
+
special behavior in the system that can cause issues if the given records are uncommitted record models
|
|
1067
|
+
with negative record IDs, meaning we don't want to have a RecordId field in the field maps provided to the
|
|
1068
|
+
system. Only used if the adaptive dialog becomes a table.
|
|
906
1069
|
:return: A list of dictionaries mapping the data field names of the given field definitions to the response
|
|
907
1070
|
value from the user for that field for each row. Even if a form was displayed, the field values will still
|
|
908
1071
|
be returned in a list.
|
|
@@ -911,23 +1074,23 @@ class CallbackUtil:
|
|
|
911
1074
|
if not count:
|
|
912
1075
|
raise SapioException("No records provided.")
|
|
913
1076
|
if count == 1:
|
|
914
|
-
return [self.record_form_dialog(title, msg, fields, records[0], column_positions,
|
|
1077
|
+
return [self.record_form_dialog(title, msg, fields, list(records)[0], column_positions,
|
|
915
1078
|
default_modifier=default_modifier, field_modifiers=field_modifiers)]
|
|
916
1079
|
return self.record_table_dialog(title, msg, fields, records,
|
|
917
1080
|
default_modifier=default_modifier, field_modifiers=field_modifiers,
|
|
918
|
-
group_by=group_by, image_data=image_data)
|
|
1081
|
+
group_by=group_by, image_data=image_data, index_field=index_field)
|
|
919
1082
|
|
|
920
1083
|
def set_record_adaptive_dialog(self,
|
|
921
1084
|
title: str,
|
|
922
1085
|
msg: str,
|
|
923
|
-
fields:
|
|
924
|
-
records:
|
|
1086
|
+
fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
|
|
1087
|
+
records: Collection[SapioRecord],
|
|
925
1088
|
*,
|
|
926
1089
|
default_modifier: FieldModifier | None = None,
|
|
927
1090
|
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
|
|
928
1091
|
column_positions: dict[str, tuple[int, int]] | None = None,
|
|
929
1092
|
group_by: FieldIdentifier | None = None,
|
|
930
|
-
image_data:
|
|
1093
|
+
image_data: Iterable[bytes] | None = None) -> None:
|
|
931
1094
|
"""
|
|
932
1095
|
Create a dialog where the user may input data into the fields of the dialog. The dialog is constructed from
|
|
933
1096
|
a given list of records of a singular type. After the user submits this dialog, the values that the user
|
|
@@ -969,17 +1132,18 @@ class CallbackUtil:
|
|
|
969
1132
|
if not count:
|
|
970
1133
|
raise SapioException("No records provided.")
|
|
971
1134
|
if count == 1:
|
|
972
|
-
self.set_record_form_dialog(title, msg, fields, records[0], column_positions,
|
|
1135
|
+
self.set_record_form_dialog(title, msg, fields, list(records)[0], column_positions,
|
|
973
1136
|
default_modifier=default_modifier, field_modifiers=field_modifiers)
|
|
974
1137
|
else:
|
|
975
1138
|
self.set_record_table_dialog(title, msg, fields, records,
|
|
976
1139
|
default_modifier=default_modifier, field_modifiers=field_modifiers,
|
|
977
1140
|
group_by=group_by, image_data=image_data)
|
|
978
1141
|
|
|
1142
|
+
# FR-47690: Updated with blank result handling behavior.
|
|
979
1143
|
def create_record_adaptive_dialog(self,
|
|
980
1144
|
title: str,
|
|
981
1145
|
msg: str,
|
|
982
|
-
fields:
|
|
1146
|
+
fields: Iterable[FieldValue] | DataTypeLayoutIdentifier,
|
|
983
1147
|
wrapper_type: type[WrappedType] | str,
|
|
984
1148
|
count: int | tuple[int, int],
|
|
985
1149
|
*,
|
|
@@ -987,9 +1151,11 @@ class CallbackUtil:
|
|
|
987
1151
|
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
|
|
988
1152
|
column_positions: dict[str, tuple[int, int]] | None = None,
|
|
989
1153
|
group_by: FieldIdentifier | None = None,
|
|
990
|
-
image_data:
|
|
991
|
-
require_input
|
|
992
|
-
|
|
1154
|
+
image_data: Iterable[bytes] | None = None,
|
|
1155
|
+
require_input = None,
|
|
1156
|
+
blank_result_handling: BlankResultHandling = BlankResultHandling.DEFAULT,
|
|
1157
|
+
repeat_message: str | None = "Please provide a value to continue.",
|
|
1158
|
+
cancel_message: str | None = "No value was provided. Cancelling dialog.") \
|
|
993
1159
|
-> list[WrappedType]:
|
|
994
1160
|
"""
|
|
995
1161
|
Create a dialog where the user may input data into the specified fields. The dialog is constructed from
|
|
@@ -1030,14 +1196,19 @@ class CallbackUtil:
|
|
|
1030
1196
|
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
1031
1197
|
the image data list corresponds to the element at the same index in the records list. Only used if the
|
|
1032
1198
|
adaptive dialog becomes a table.
|
|
1033
|
-
:param require_input:
|
|
1034
|
-
|
|
1035
|
-
:param
|
|
1036
|
-
|
|
1199
|
+
:param require_input: DEPRECATED. Use blank_result_handling with a value of BlankResultHandling.REPEAT
|
|
1200
|
+
instead.
|
|
1201
|
+
:param blank_result_handling: Determine how to handle the result of a callback when the user provides a blank
|
|
1202
|
+
result.
|
|
1203
|
+
:param repeat_message: If blank_result_handling is REPEAT and a repeat_message is provided, then that message
|
|
1204
|
+
appears as toaster text when the user provides a blank result.
|
|
1205
|
+
:param cancel_message: If blank_result_handling is CANCEL and a cancel_message is provided, then that message
|
|
1206
|
+
appears as toaster text when the user provides a blank result.
|
|
1037
1207
|
:return: A list of the newly created records. Even if a form was displayed, the created record will still be
|
|
1038
1208
|
returned in a list.
|
|
1039
1209
|
"""
|
|
1040
|
-
count: int = self.__prompt_for_count(count, wrapper_type, require_input, repeat_message
|
|
1210
|
+
count: int = self.__prompt_for_count(count, wrapper_type, require_input, blank_result_handling, repeat_message,
|
|
1211
|
+
cancel_message)
|
|
1041
1212
|
if count <= 0:
|
|
1042
1213
|
return []
|
|
1043
1214
|
if count == 1:
|
|
@@ -1047,17 +1218,20 @@ class CallbackUtil:
|
|
|
1047
1218
|
default_modifier=default_modifier, field_modifiers=field_modifiers,
|
|
1048
1219
|
group_by=group_by, image_data=image_data)
|
|
1049
1220
|
|
|
1221
|
+
# FR-47690: Add group_by and image_data parameters.
|
|
1050
1222
|
def multi_type_table_dialog(self,
|
|
1051
1223
|
title: str,
|
|
1052
1224
|
msg: str,
|
|
1053
|
-
fields:
|
|
1054
|
-
row_contents:
|
|
1225
|
+
fields: Iterable[tuple[DataTypeIdentifier, FieldIdentifier] | AbstractVeloxFieldDefinition],
|
|
1226
|
+
row_contents: Iterable[Iterable[SapioRecord | FieldMap]],
|
|
1055
1227
|
*,
|
|
1056
1228
|
default_modifier: FieldModifier | None = None,
|
|
1057
1229
|
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
|
|
1058
1230
|
data_type: DataTypeIdentifier = "Default",
|
|
1059
1231
|
display_name: str | None = None,
|
|
1060
|
-
plural_display_name: str | None = None
|
|
1232
|
+
plural_display_name: str | None = None,
|
|
1233
|
+
group_by: FieldIdentifier | None = None,
|
|
1234
|
+
image_data: list[bytes] | None = None) -> list[FieldMap]:
|
|
1061
1235
|
"""
|
|
1062
1236
|
Create a table dialog where the user may input data into the fields of the table. The table is constructed from
|
|
1063
1237
|
a given list of records of multiple data types or field maps. Provided field names must match with field names
|
|
@@ -1111,6 +1285,10 @@ class CallbackUtil:
|
|
|
1111
1285
|
name.
|
|
1112
1286
|
:param plural_display_name: The plural display name for the temporary data type. If not provided, defaults to
|
|
1113
1287
|
the display name + "s".
|
|
1288
|
+
:param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
|
|
1289
|
+
The user may remove this grouping if they want to.
|
|
1290
|
+
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
1291
|
+
the image data list corresponds to the element at the same index in the values list.
|
|
1114
1292
|
:return: A list of dictionaries mapping the data field names of the given field definitions to the response
|
|
1115
1293
|
value from the user for that field for each row.
|
|
1116
1294
|
"""
|
|
@@ -1175,31 +1353,49 @@ class CallbackUtil:
|
|
|
1175
1353
|
field_names.add(name)
|
|
1176
1354
|
|
|
1177
1355
|
# Get the values for each row.
|
|
1356
|
+
# FR-47690: Updated this for loop to better match the Java implementation.
|
|
1178
1357
|
values: list[dict[str, FieldValue]] = []
|
|
1179
1358
|
for row in row_contents:
|
|
1180
1359
|
# The final values for this row:
|
|
1181
1360
|
row_values: dict[str, FieldValue] = {}
|
|
1182
1361
|
|
|
1183
|
-
# Map the records for this row by their data type. If a field map is provided,
|
|
1184
|
-
|
|
1362
|
+
# Map the records for this row by their data type. If a field map is provided, save it separately to
|
|
1363
|
+
# the temp_values dict.
|
|
1364
|
+
row_records: dict[str, SapioRecord] = {}
|
|
1365
|
+
temp_values: FieldMap = {}
|
|
1185
1366
|
for rec in row:
|
|
1186
1367
|
# Toss out null elements.
|
|
1187
1368
|
if rec is None:
|
|
1188
1369
|
continue
|
|
1189
1370
|
# Map records to their data type name. Map field maps to Default.
|
|
1190
|
-
dt: str = "Default" if isinstance(rec, dict) else AliasUtil.
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1371
|
+
dt: str = "Default" if isinstance(rec, dict) else AliasUtil.to_data_type_name(rec)
|
|
1372
|
+
if dt == "Default":
|
|
1373
|
+
temp_values.update(rec)
|
|
1374
|
+
else:
|
|
1375
|
+
# Warn if the same data type name appears more than once.
|
|
1376
|
+
if dt in row_records:
|
|
1377
|
+
raise SapioException(f"The data type \"{dt}\" appears more than once in the given row contents.")
|
|
1378
|
+
row_records[dt] = rec
|
|
1195
1379
|
|
|
1196
1380
|
# Get the field values from the above records.
|
|
1197
1381
|
for field in final_fields:
|
|
1382
|
+
value: Any | None = None
|
|
1383
|
+
|
|
1198
1384
|
# Find the object that corresponds to this field given its data type name.
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
if
|
|
1202
|
-
|
|
1385
|
+
dt: str = field.data_type_name
|
|
1386
|
+
fd: str = field.data_field_name
|
|
1387
|
+
if dt == "Default":
|
|
1388
|
+
# If the field map is provided, get the value from it.
|
|
1389
|
+
# FR-47690: If a value is not provided, then use the default value of the field definition.
|
|
1390
|
+
if fd in temp_values:
|
|
1391
|
+
value = temp_values.get(fd)
|
|
1392
|
+
else:
|
|
1393
|
+
value = field.default_value
|
|
1394
|
+
elif dt in row_records:
|
|
1395
|
+
record: SapioRecord = row_records[dt]
|
|
1396
|
+
# If the record is not null, get the value from the record.
|
|
1397
|
+
if record is not None:
|
|
1398
|
+
value = record.get_field_value(fd)
|
|
1203
1399
|
|
|
1204
1400
|
# Find out if this field had its data type prepended to it. If this is the case, then we need to find
|
|
1205
1401
|
# the true data field name before retrieving the value from the field map.
|
|
@@ -1208,16 +1404,22 @@ class CallbackUtil:
|
|
|
1208
1404
|
name = name.split(".")[1]
|
|
1209
1405
|
|
|
1210
1406
|
# Set the value for this particular field.
|
|
1211
|
-
row_values[
|
|
1407
|
+
row_values[name] = value
|
|
1212
1408
|
values.append(row_values)
|
|
1213
1409
|
|
|
1214
1410
|
# Build a temporary data type for the request.
|
|
1215
1411
|
temp_dt = self.__temp_dt_from_field_defs(data_type, display_name, plural_display_name, final_fields, None)
|
|
1412
|
+
temp_dt.record_image_assignable = bool(image_data)
|
|
1413
|
+
|
|
1414
|
+
# Convert the group_by parameter to a field name.
|
|
1415
|
+
if group_by is not None:
|
|
1416
|
+
group_by: str = AliasUtil.to_data_field_name(group_by)
|
|
1216
1417
|
|
|
1217
1418
|
# Send the request to the user.
|
|
1218
1419
|
request = TableEntryDialogRequest(title, msg, temp_dt, values,
|
|
1420
|
+
record_image_data_list=image_data, group_by_field=group_by,
|
|
1219
1421
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
1220
|
-
response: list[FieldMap] = self.
|
|
1422
|
+
response: list[FieldMap] = self.__send_dialog(request, self.callback.show_table_entry_dialog)
|
|
1221
1423
|
return response
|
|
1222
1424
|
|
|
1223
1425
|
def record_view_dialog(self,
|
|
@@ -1226,7 +1428,7 @@ class CallbackUtil:
|
|
|
1226
1428
|
layout: DataTypeLayoutIdentifier = None,
|
|
1227
1429
|
minimized: bool = False,
|
|
1228
1430
|
access_level: FormAccessLevel | None = None,
|
|
1229
|
-
plugin_path_list:
|
|
1431
|
+
plugin_path_list: Iterable[str] | None = None) -> None:
|
|
1230
1432
|
"""
|
|
1231
1433
|
Create an IDV dialog for the given record. This IDV may use an existing layout already defined in the system,
|
|
1232
1434
|
and can be created to allow the user to edit the field in the IDV, or to be read-only for the user to review.
|
|
@@ -1254,26 +1456,30 @@ class CallbackUtil:
|
|
|
1254
1456
|
# Send the request to the user.
|
|
1255
1457
|
request = DataRecordDialogRequest(title, record, layout, minimized, access_level, plugin_path_list,
|
|
1256
1458
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
1257
|
-
response: bool = self.
|
|
1459
|
+
response: bool = self.__send_dialog(request, self.callback.data_record_form_view_dialog)
|
|
1258
1460
|
# The __handle_dialog_request function only throws a cancelled exception if the response is None, but in
|
|
1259
1461
|
# this case we also want to throw if the response is False.
|
|
1260
1462
|
if not response:
|
|
1261
1463
|
raise SapioUserCancelledException()
|
|
1262
1464
|
|
|
1263
1465
|
# CR-47326: Allow the selection dialog functions to preselect rows/records in the table.
|
|
1466
|
+
# FR-47690: Added shortcut_single_option parameter. Updated with blank result handling behavior.
|
|
1264
1467
|
def selection_dialog(self,
|
|
1265
1468
|
msg: str,
|
|
1266
|
-
fields:
|
|
1267
|
-
values:
|
|
1469
|
+
fields: Iterable[AbstractVeloxFieldDefinition],
|
|
1470
|
+
values: Iterable[FieldMap],
|
|
1268
1471
|
multi_select: bool = True,
|
|
1269
|
-
preselected_rows:
|
|
1472
|
+
preselected_rows: Iterable[FieldMap | RecordIdentifier] | None = None,
|
|
1270
1473
|
*,
|
|
1271
1474
|
data_type: DataTypeIdentifier = "Default",
|
|
1272
1475
|
display_name: str | None = None,
|
|
1273
1476
|
plural_display_name: str | None = None,
|
|
1274
|
-
image_data:
|
|
1275
|
-
|
|
1276
|
-
|
|
1477
|
+
image_data: Iterable[bytes] | None = None,
|
|
1478
|
+
shortcut_single_option: bool = True,
|
|
1479
|
+
require_selection = None,
|
|
1480
|
+
blank_result_handling: BlankResultHandling = BlankResultHandling.DEFAULT,
|
|
1481
|
+
repeat_message: str | None = "Please provide a selection to continue.",
|
|
1482
|
+
cancel_message: str | None = "No selection was made. Cancelling dialog.") -> list[FieldMap]:
|
|
1277
1483
|
"""
|
|
1278
1484
|
Create a selection dialog for a list of field maps for the user to choose from. Requires that the caller
|
|
1279
1485
|
provide the definitions of every field in the table.
|
|
@@ -1296,14 +1502,23 @@ class CallbackUtil:
|
|
|
1296
1502
|
the display name + "s".
|
|
1297
1503
|
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
1298
1504
|
the image data list corresponds to the element at the same index in the values list.
|
|
1299
|
-
:param
|
|
1300
|
-
|
|
1301
|
-
:param
|
|
1302
|
-
|
|
1505
|
+
:param shortcut_single_option: If true, then if the list contains only one option, the dialog will not be shown
|
|
1506
|
+
and the single option will be returned immediately.
|
|
1507
|
+
:param require_selection: DEPRECATED. Use blank_result_handling with a value of BlankResultHandling.REPEAT
|
|
1508
|
+
instead.
|
|
1509
|
+
:param blank_result_handling: Determine how to handle the result of a callback when the user provides a blank
|
|
1510
|
+
result.
|
|
1511
|
+
:param repeat_message: If blank_result_handling is REPEAT and a repeat_message is provided, then that message
|
|
1512
|
+
appears as toaster text when the user provides a blank result.
|
|
1513
|
+
:param cancel_message: If blank_result_handling is CANCEL and a cancel_message is provided, then that message
|
|
1514
|
+
appears as toaster text when the user provides a blank result.
|
|
1303
1515
|
:return: A list of field maps corresponding to the chosen input field maps.
|
|
1304
1516
|
"""
|
|
1305
1517
|
if not values:
|
|
1306
1518
|
raise SapioException("No values provided.")
|
|
1519
|
+
values = list(values)
|
|
1520
|
+
if len(values) == 1 and shortcut_single_option:
|
|
1521
|
+
return [values[0]]
|
|
1307
1522
|
|
|
1308
1523
|
if preselected_rows:
|
|
1309
1524
|
# Confirm that the provided field maps are validly configured to allow the use of preselected rows.
|
|
@@ -1329,6 +1544,7 @@ class CallbackUtil:
|
|
|
1329
1544
|
|
|
1330
1545
|
# Add a RecordId definition to the fields if one is not already present. This is necessary for the
|
|
1331
1546
|
# pre-selected records parameter to function.
|
|
1547
|
+
fields = list(fields)
|
|
1332
1548
|
if "RecordId" not in [x.data_field_name for x in fields]:
|
|
1333
1549
|
builder = FieldBuilder(data_type)
|
|
1334
1550
|
fields.append(builder.long_field("RecordId", abstract_info=AnyFieldInfo(visible=False)))
|
|
@@ -1338,27 +1554,31 @@ class CallbackUtil:
|
|
|
1338
1554
|
temp_dt.record_image_assignable = bool(image_data)
|
|
1339
1555
|
|
|
1340
1556
|
# Send the request to the user.
|
|
1341
|
-
request = TempTableSelectionRequest(temp_dt, msg, values, image_data, preselected_rows, multi_select)
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1557
|
+
request = TempTableSelectionRequest(temp_dt, msg, list(values), image_data, preselected_rows, multi_select)
|
|
1558
|
+
|
|
1559
|
+
# Reverse compatibility: If require_selection is true and blank_result_handling is not set, then
|
|
1560
|
+
# set blank_result_handling to REPEAT.
|
|
1561
|
+
if require_selection is True and blank_result_handling == BlankResultHandling.DEFAULT:
|
|
1562
|
+
blank_result_handling = BlankResultHandling.REPEAT
|
|
1563
|
+
def not_blank_func(r: list[FieldMap]) -> bool:
|
|
1564
|
+
return bool(r)
|
|
1565
|
+
return self.__send_dialog_blank_results(request, self.callback.show_temp_table_selection_dialog, not_blank_func,
|
|
1566
|
+
blank_result_handling, repeat_message, cancel_message)
|
|
1567
|
+
|
|
1568
|
+
# FR-47690: Added shortcut_single_option parameter. Updated with blank result handling behavior.
|
|
1352
1569
|
def record_selection_dialog(self,
|
|
1353
1570
|
msg: str,
|
|
1354
|
-
fields:
|
|
1355
|
-
records:
|
|
1571
|
+
fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
|
|
1572
|
+
records: Iterable[SapioRecord],
|
|
1356
1573
|
multi_select: bool = True,
|
|
1357
|
-
preselected_records:
|
|
1574
|
+
preselected_records: Iterable[RecordIdentifier] | None = None,
|
|
1358
1575
|
*,
|
|
1359
|
-
image_data:
|
|
1360
|
-
|
|
1361
|
-
|
|
1576
|
+
image_data: Iterable[bytes] | None = None,
|
|
1577
|
+
shortcut_single_option: bool = True,
|
|
1578
|
+
require_selection = None,
|
|
1579
|
+
blank_result_handling: BlankResultHandling = BlankResultHandling.DEFAULT,
|
|
1580
|
+
repeat_message: str | None = "Please provide a selection to continue.",
|
|
1581
|
+
cancel_message: str | None = "No selection was made. Cancelling dialog.") \
|
|
1362
1582
|
-> list[SapioRecord]:
|
|
1363
1583
|
"""
|
|
1364
1584
|
Create a record selection dialog for a list of records for the user to choose from. Provided field names must
|
|
@@ -1380,15 +1600,24 @@ class CallbackUtil:
|
|
|
1380
1600
|
record IDs are provided, the dialog will automatically allow multi-selection of records.
|
|
1381
1601
|
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
1382
1602
|
the image data list corresponds to the element at the same index in the records list.
|
|
1383
|
-
:param
|
|
1384
|
-
|
|
1385
|
-
:param
|
|
1386
|
-
|
|
1603
|
+
:param shortcut_single_option: If true, then if the list contains only one option, the dialog will not be shown
|
|
1604
|
+
and the single option will be returned immediately.
|
|
1605
|
+
:param require_selection: DEPRECATED. Use blank_result_handling with a value of BlankResultHandling.REPEAT
|
|
1606
|
+
instead.
|
|
1607
|
+
:param blank_result_handling: Determine how to handle the result of a callback when the user provides a blank
|
|
1608
|
+
result.
|
|
1609
|
+
:param repeat_message: If blank_result_handling is REPEAT and a repeat_message is provided, then that message
|
|
1610
|
+
appears as toaster text when the user provides a blank result.
|
|
1611
|
+
:param cancel_message: If blank_result_handling is CANCEL and a cancel_message is provided, then that message
|
|
1612
|
+
appears as toaster text when the user provides a blank result.
|
|
1387
1613
|
:return: A list of the selected records.
|
|
1388
1614
|
"""
|
|
1389
1615
|
# Get the data type name and field values from the provided records.
|
|
1390
1616
|
if not records:
|
|
1391
1617
|
raise SapioException("No records provided.")
|
|
1618
|
+
records = list(records)
|
|
1619
|
+
if len(records) == 1 and shortcut_single_option:
|
|
1620
|
+
return [records[0]]
|
|
1392
1621
|
data_type: str = AliasUtil.to_singular_data_type_name(records)
|
|
1393
1622
|
field_map_list: list[FieldMap] = AliasUtil.to_field_map_list(records, include_record_id=True)
|
|
1394
1623
|
|
|
@@ -1415,14 +1644,17 @@ class CallbackUtil:
|
|
|
1415
1644
|
|
|
1416
1645
|
# Send the request to the user.
|
|
1417
1646
|
request = TempTableSelectionRequest(temp_dt, msg, field_map_list, image_data, preselected_records, multi_select)
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1647
|
+
|
|
1648
|
+
# Reverse compatibility: If require_selection is true and blank_result_handling is not set, then
|
|
1649
|
+
# set blank_result_handling to REPEAT.
|
|
1650
|
+
if require_selection is True and blank_result_handling == BlankResultHandling.DEFAULT:
|
|
1651
|
+
blank_result_handling = BlankResultHandling.REPEAT
|
|
1652
|
+
def not_blank_func(r: list[FieldMap]) -> bool:
|
|
1653
|
+
return bool(r)
|
|
1654
|
+
response: list[FieldMap] = self.__send_dialog_blank_results(request,
|
|
1655
|
+
self.callback.show_temp_table_selection_dialog,
|
|
1656
|
+
not_blank_func, blank_result_handling,
|
|
1657
|
+
repeat_message, cancel_message)
|
|
1426
1658
|
|
|
1427
1659
|
# Map the field maps in the response back to the record they come from, returning the chosen record instead of
|
|
1428
1660
|
# the chosen field map.
|
|
@@ -1433,22 +1665,25 @@ class CallbackUtil:
|
|
|
1433
1665
|
return ret_list
|
|
1434
1666
|
|
|
1435
1667
|
# CR-47377: Add allow_creation and default_creation_number to cover new parameters of this request type from 24.12.
|
|
1668
|
+
# FR-47690: Updated with blank result handling behavior.
|
|
1436
1669
|
def input_selection_dialog(self,
|
|
1437
1670
|
wrapper_type: type[WrappedType] | str,
|
|
1438
1671
|
msg: str,
|
|
1439
1672
|
multi_select: bool = True,
|
|
1440
1673
|
only_key_fields: bool = False,
|
|
1441
|
-
search_types:
|
|
1674
|
+
search_types: Iterable[SearchType] | None = None,
|
|
1442
1675
|
scan_criteria: ScanToSelectCriteria | None = None,
|
|
1443
1676
|
custom_search: CustomReport | CustomReportCriteria | str | None = None,
|
|
1444
|
-
preselected_records:
|
|
1445
|
-
record_blacklist:
|
|
1446
|
-
record_whitelist:
|
|
1677
|
+
preselected_records: Iterable[RecordIdentifier] | None = None,
|
|
1678
|
+
record_blacklist: Iterable[RecordIdentifier] | None = None,
|
|
1679
|
+
record_whitelist: Iterable[RecordIdentifier] | None = None,
|
|
1447
1680
|
allow_creation: bool = False,
|
|
1448
1681
|
default_creation_number: int = 1,
|
|
1449
1682
|
*,
|
|
1450
|
-
require_selection
|
|
1451
|
-
|
|
1683
|
+
require_selection = None,
|
|
1684
|
+
blank_result_handling: BlankResultHandling = BlankResultHandling.DEFAULT,
|
|
1685
|
+
repeat_message: str | None = "Please provide a selection to continue.",
|
|
1686
|
+
cancel_message: str | None = "No selection was made. Cancelling dialog.") \
|
|
1452
1687
|
-> list[WrappedType] | list[PyRecordModel]:
|
|
1453
1688
|
"""
|
|
1454
1689
|
Display a table of records that exist in the system matching the given data type and filter criteria and have
|
|
@@ -1490,10 +1725,14 @@ class CallbackUtil:
|
|
|
1490
1725
|
than 1, then multi-selection must be true. The data type definition of the records being created must have
|
|
1491
1726
|
"Prompt for Number to Add" set to true in order to allow the user to select how many records to create, as
|
|
1492
1727
|
otherwise user will only ever be able to create one record at a time.
|
|
1493
|
-
:param require_selection:
|
|
1494
|
-
|
|
1495
|
-
:param
|
|
1496
|
-
|
|
1728
|
+
:param require_selection: DEPRECATED. Use blank_result_handling with a value of BlankResultHandling.REPEAT
|
|
1729
|
+
instead.
|
|
1730
|
+
:param blank_result_handling: Determine how to handle the result of a callback when the user provides a blank
|
|
1731
|
+
result.
|
|
1732
|
+
:param repeat_message: If blank_result_handling is REPEAT and a repeat_message is provided, then that message
|
|
1733
|
+
appears as toaster text when the user provides a blank result.
|
|
1734
|
+
:param cancel_message: If blank_result_handling is CANCEL and a cancel_message is provided, then that message
|
|
1735
|
+
appears as toaster text when the user provides a blank result.
|
|
1497
1736
|
:return: A list of the records selected by the user in the dialog, wrapped as record models using the provided
|
|
1498
1737
|
wrapper.
|
|
1499
1738
|
"""
|
|
@@ -1519,23 +1758,25 @@ class CallbackUtil:
|
|
|
1519
1758
|
request = InputSelectionRequest(data_type, msg, search_types, only_key_fields, record_blacklist,
|
|
1520
1759
|
record_whitelist, preselected_records, custom_search, scan_criteria,
|
|
1521
1760
|
multi_select, allow_creation, default_creation_number)
|
|
1522
|
-
# If require_selection is true, repeat the request if the user didn't make a selection.
|
|
1523
|
-
while True:
|
|
1524
|
-
response: list[DataRecord] = self.__handle_dialog_request(request,
|
|
1525
|
-
self.callback.show_input_selection_dialog)
|
|
1526
|
-
if not require_selection or response:
|
|
1527
|
-
break
|
|
1528
|
-
if repeat_message:
|
|
1529
|
-
self.toaster_popup(repeat_message, popup_type=PopupType.Warning)
|
|
1530
|
-
return self.rec_handler.wrap_models(response, wrapper_type)
|
|
1531
1761
|
|
|
1762
|
+
# Reverse compatibility: If require_selection is true and blank_result_handling is not set, then
|
|
1763
|
+
# set blank_result_handling to REPEAT.
|
|
1764
|
+
if require_selection is True and blank_result_handling == BlankResultHandling.DEFAULT:
|
|
1765
|
+
blank_result_handling = BlankResultHandling.REPEAT
|
|
1766
|
+
def not_blank_func(r: list[DataRecord]) -> bool:
|
|
1767
|
+
return bool(r)
|
|
1768
|
+
return self.__send_dialog_blank_results(request, self.callback.show_input_selection_dialog, not_blank_func,
|
|
1769
|
+
blank_result_handling, repeat_message, cancel_message)
|
|
1770
|
+
|
|
1771
|
+
# FR-47690: Deprecated the require_authentication parameter.
|
|
1772
|
+
# noinspection PyUnusedLocal
|
|
1532
1773
|
def esign_dialog(self,
|
|
1533
1774
|
title: str,
|
|
1534
1775
|
msg: str,
|
|
1535
1776
|
show_comment: bool = True,
|
|
1536
|
-
additional_fields:
|
|
1777
|
+
additional_fields: Iterable[AbstractVeloxFieldDefinition] | None = None,
|
|
1537
1778
|
*,
|
|
1538
|
-
require_authentication
|
|
1779
|
+
require_authentication = None) -> ESigningResponsePojo:
|
|
1539
1780
|
"""
|
|
1540
1781
|
Create an e-sign dialog for the user to interact with.
|
|
1541
1782
|
|
|
@@ -1545,8 +1786,7 @@ class CallbackUtil:
|
|
|
1545
1786
|
user is required to provide an action.
|
|
1546
1787
|
:param additional_fields: Field definitions for additional fields to display in the dialog, for if there is
|
|
1547
1788
|
other information you wish to gather from the user alongside the e-sign.
|
|
1548
|
-
:param require_authentication:
|
|
1549
|
-
credentials.
|
|
1789
|
+
:param require_authentication: DEPRECATED. Authentication is always required when using this function.
|
|
1550
1790
|
:return: An e-sign response object containing information about the e-sign attempt.
|
|
1551
1791
|
"""
|
|
1552
1792
|
# Construct a temporary data type if any additional fields are provided.
|
|
@@ -1560,10 +1800,9 @@ class CallbackUtil:
|
|
|
1560
1800
|
# Send the request to the user.
|
|
1561
1801
|
request = ESigningRequestPojo(title, msg, show_comment, temp_dt,
|
|
1562
1802
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
1563
|
-
# If require_authentication is true, repeat the request if the user didn't provide valid credentials.
|
|
1564
1803
|
while True:
|
|
1565
|
-
response: ESigningResponsePojo = self.
|
|
1566
|
-
if
|
|
1804
|
+
response: ESigningResponsePojo = self.__send_dialog(request, self.callback.show_esign_dialog)
|
|
1805
|
+
if response.authenticated:
|
|
1567
1806
|
break
|
|
1568
1807
|
# This matches the OOB behavior.
|
|
1569
1808
|
self.toaster_popup("Incorrect username/password", popup_type=PopupType.Error)
|
|
@@ -1572,7 +1811,7 @@ class CallbackUtil:
|
|
|
1572
1811
|
popup_type=PopupType.Error)
|
|
1573
1812
|
return response
|
|
1574
1813
|
|
|
1575
|
-
def request_file(self, title: str, exts:
|
|
1814
|
+
def request_file(self, title: str, exts: Iterable[str] | None = None,
|
|
1576
1815
|
show_image_editor: bool = False, show_camera_button: bool = False) -> tuple[str, bytes]:
|
|
1577
1816
|
"""
|
|
1578
1817
|
Request a single file from the user.
|
|
@@ -1599,13 +1838,13 @@ class CallbackUtil:
|
|
|
1599
1838
|
|
|
1600
1839
|
# Send the request to the user.
|
|
1601
1840
|
request = FilePromptRequest(title, show_image_editor, ",".join(exts), show_camera_button)
|
|
1602
|
-
file_path: str = self.
|
|
1841
|
+
file_path: str = self.__send_dialog(request, self.callback.show_file_dialog, data_sink=do_consume)
|
|
1603
1842
|
|
|
1604
1843
|
# Verify that each of the file given matches the expected extension(s).
|
|
1605
1844
|
self.__verify_file(file_path, sink.data, exts)
|
|
1606
1845
|
return file_path, sink.data
|
|
1607
1846
|
|
|
1608
|
-
def request_files(self, title: str, exts:
|
|
1847
|
+
def request_files(self, title: str, exts: Iterable[str] | None = None,
|
|
1609
1848
|
show_image_editor: bool = False, show_camera_button: bool = False) -> dict[str, bytes]:
|
|
1610
1849
|
"""
|
|
1611
1850
|
Request multiple files from the user.
|
|
@@ -1624,7 +1863,7 @@ class CallbackUtil:
|
|
|
1624
1863
|
|
|
1625
1864
|
# Send the request to the user.
|
|
1626
1865
|
request = MultiFilePromptRequest(title, show_image_editor, ",".join(exts), show_camera_button)
|
|
1627
|
-
file_paths: list[str] = self.
|
|
1866
|
+
file_paths: list[str] = self.__send_dialog(request, self.callback.show_multi_file_dialog)
|
|
1628
1867
|
|
|
1629
1868
|
# Verify that each of the files given match the expected extension(s).
|
|
1630
1869
|
ret_dict: dict[str, bytes] = {}
|
|
@@ -1637,7 +1876,7 @@ class CallbackUtil:
|
|
|
1637
1876
|
return ret_dict
|
|
1638
1877
|
|
|
1639
1878
|
@staticmethod
|
|
1640
|
-
def __verify_file(file_path: str, file_bytes: bytes, allowed_extensions:
|
|
1879
|
+
def __verify_file(file_path: str, file_bytes: bytes, allowed_extensions: Iterable[str]) -> None:
|
|
1641
1880
|
"""
|
|
1642
1881
|
Verify that the provided file was read (i.e. the file path and file bytes aren't None or empty) and that it
|
|
1643
1882
|
has the correct file extension. Raises a user error exception if something about the file is incorrect.
|
|
@@ -1648,10 +1887,11 @@ class CallbackUtil:
|
|
|
1648
1887
|
"""
|
|
1649
1888
|
if file_path is None or len(file_path) == 0 or file_bytes is None or len(file_bytes) == 0:
|
|
1650
1889
|
raise SapioUserErrorException("Empty file provided or file unable to be read.")
|
|
1651
|
-
if
|
|
1890
|
+
if allowed_extensions:
|
|
1652
1891
|
matches: bool = False
|
|
1653
1892
|
for ext in allowed_extensions:
|
|
1654
|
-
|
|
1893
|
+
# FR-47690: Changed to a case-insensitive match.
|
|
1894
|
+
if file_path.casefold().endswith("." + ext.lstrip(".").casefold()):
|
|
1655
1895
|
matches = True
|
|
1656
1896
|
break
|
|
1657
1897
|
if matches is False:
|
|
@@ -1665,8 +1905,8 @@ class CallbackUtil:
|
|
|
1665
1905
|
:param file_name: The name of the file.
|
|
1666
1906
|
:param file_data: The data of the file, provided as either a string or as a bytes array.
|
|
1667
1907
|
"""
|
|
1668
|
-
|
|
1669
|
-
|
|
1908
|
+
with io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data) as data:
|
|
1909
|
+
self.callback.send_file(file_name, False, data)
|
|
1670
1910
|
|
|
1671
1911
|
def write_zip_file(self, zip_name: str, files: dict[str, str | bytes]) -> None:
|
|
1672
1912
|
"""
|
|
@@ -1675,16 +1915,38 @@ class CallbackUtil:
|
|
|
1675
1915
|
:param zip_name: The name of the zip file.
|
|
1676
1916
|
:param files: A dictionary of the files to add to the zip file.
|
|
1677
1917
|
"""
|
|
1678
|
-
|
|
1679
|
-
|
|
1918
|
+
self.write_file(zip_name, FileUtil.zip_files(files))
|
|
1919
|
+
|
|
1920
|
+
@staticmethod
|
|
1921
|
+
def __get_indexed_field_maps(records: Iterable[SapioRecord], index_field: str) -> list[FieldMap]:
|
|
1922
|
+
"""
|
|
1923
|
+
For dialogs that accept multiple records, we may want to be able to match the returned results back to the
|
|
1924
|
+
records that they're for. In this case, we need to add an index to each record so that we can match them back
|
|
1925
|
+
to the original records. We can't use the RecordId field, as new record models have negative record IDs that
|
|
1926
|
+
cause the callback dialogs to bug out if the RecordId field is present and negative.
|
|
1927
|
+
|
|
1928
|
+
:param records: The records to return indexed field maps of.
|
|
1929
|
+
:param index_field: The name of the field to use as the index. Make sure that this field doesn't exist on the
|
|
1930
|
+
records, as then it will overwrite the existing value.
|
|
1931
|
+
:return: A list of field maps for the records, with an index field added to each. The value of the index on
|
|
1932
|
+
each field map is the record's record ID (even if it's a record model with a negative ID).
|
|
1933
|
+
"""
|
|
1934
|
+
ret_val: list[FieldMap] = []
|
|
1935
|
+
for record in records:
|
|
1936
|
+
field_map: FieldMap = AliasUtil.to_field_map(record)
|
|
1937
|
+
field_map[index_field] = AliasUtil.to_record_id(record)
|
|
1938
|
+
ret_val.append(field_map)
|
|
1939
|
+
return ret_val
|
|
1680
1940
|
|
|
1681
1941
|
@staticmethod
|
|
1682
1942
|
def __temp_dt_from_field_defs(data_type: DataTypeIdentifier, display_name: str | None,
|
|
1683
|
-
plural_display_name: str | None, fields:
|
|
1943
|
+
plural_display_name: str | None, fields: Iterable[AbstractVeloxFieldDefinition],
|
|
1684
1944
|
column_positions: dict[str, tuple[int, int]] | None) -> TemporaryDataType:
|
|
1685
1945
|
"""
|
|
1686
1946
|
Construct a Temporary Data Type definition from a provided list of field definitions for use in a callback.
|
|
1687
1947
|
"""
|
|
1948
|
+
if not fields:
|
|
1949
|
+
raise SapioException("No fields provided to create a temporary data type.")
|
|
1688
1950
|
# Get the data type name as a string from the parameters, and set the display name and plural display name if
|
|
1689
1951
|
# they haven't been set.
|
|
1690
1952
|
data_type: str = AliasUtil.to_data_type_name(data_type)
|
|
@@ -1714,7 +1976,7 @@ class CallbackUtil:
|
|
|
1714
1976
|
builder.add_field(field_def, column, span)
|
|
1715
1977
|
return builder.get_temporary_data_type()
|
|
1716
1978
|
|
|
1717
|
-
def __temp_dt_from_field_names(self, data_type: str, fields:
|
|
1979
|
+
def __temp_dt_from_field_names(self, data_type: str, fields: Iterable[FieldIdentifier | FieldFilterCriteria],
|
|
1718
1980
|
column_positions: dict[str, tuple[int, int]] | None,
|
|
1719
1981
|
default_modifier: FieldModifier, field_modifiers: dict[str, FieldModifier]) \
|
|
1720
1982
|
-> TemporaryDataType:
|
|
@@ -1731,6 +1993,7 @@ class CallbackUtil:
|
|
|
1731
1993
|
|
|
1732
1994
|
# Determine if any FieldFilterCriteria were provided. If so, remove them from the fields list so that it
|
|
1733
1995
|
# contains only field identifiers.
|
|
1996
|
+
fields = list(fields)
|
|
1734
1997
|
filter_criteria: list[FieldFilterCriteria] = [x for x in fields if isinstance(x, FieldFilterCriteria)]
|
|
1735
1998
|
for criteria in filter_criteria:
|
|
1736
1999
|
fields.remove(criteria)
|
|
@@ -1803,8 +2066,10 @@ class CallbackUtil:
|
|
|
1803
2066
|
temp_dt.set_field_definition(modifier.modify_field(field_def))
|
|
1804
2067
|
return temp_dt
|
|
1805
2068
|
|
|
2069
|
+
# FR-47690: Updated with blank result handling behavior.
|
|
1806
2070
|
def __prompt_for_count(self, count: tuple[int, int] | int, wrapper_type: type[WrappedType] | str,
|
|
1807
|
-
require_input: bool, repeat_message: str
|
|
2071
|
+
require_input: bool, blank_result_handling: BlankResultHandling, repeat_message: str,
|
|
2072
|
+
cancel_message: str) -> int:
|
|
1808
2073
|
"""
|
|
1809
2074
|
Given a count value, if it is a tuple representing an allowable range of values for a number of records to
|
|
1810
2075
|
create, prompt the user to input the exact count to use. If the count is already a single integer, simply
|
|
@@ -1817,8 +2082,14 @@ class CallbackUtil:
|
|
|
1817
2082
|
plural: str = self.dt_cache.get_plural_display_name(AliasUtil.to_data_type_name(wrapper_type))
|
|
1818
2083
|
min_val, max_val = count
|
|
1819
2084
|
msg: str = f"How many {plural} should be created? ({min_val} to {max_val})"
|
|
1820
|
-
|
|
1821
|
-
|
|
2085
|
+
count_field: VeloxIntegerFieldDefinition = FieldBuilder().int_field("Count", min_value=min_val,
|
|
2086
|
+
max_value=max_val,
|
|
2087
|
+
default_value=min_val)
|
|
2088
|
+
count: int = self.input_dialog(f"Create {plural}", msg, count_field,
|
|
2089
|
+
require_input=require_input, blank_result_handling=blank_result_handling,
|
|
2090
|
+
repeat_message=repeat_message, cancel_message=cancel_message)
|
|
2091
|
+
if count is None:
|
|
2092
|
+
count = 0
|
|
1822
2093
|
return count
|
|
1823
2094
|
|
|
1824
2095
|
def __to_layout(self, data_type: str, layout: DataTypeLayoutIdentifier) -> DataTypeLayout | None:
|
|
@@ -1883,19 +2154,18 @@ class CallbackUtil:
|
|
|
1883
2154
|
raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
|
|
1884
2155
|
return field_def
|
|
1885
2156
|
|
|
1886
|
-
def
|
|
2157
|
+
def __handle_timeout(self, func: Callable, request: Any, **kwargs) -> Any:
|
|
1887
2158
|
"""
|
|
1888
2159
|
Send a client callback request to the user that creates a dialog.
|
|
1889
2160
|
|
|
1890
2161
|
This function handles updating the user object's request timeout to match the request timeout of this
|
|
1891
2162
|
CallbackUtil for the duration of the dialog.
|
|
1892
2163
|
If the dialog times out then a SapioDialogTimeoutException is thrown.
|
|
1893
|
-
If the user cancels the dialog then a SapioUserCancelledException is thrown.
|
|
1894
2164
|
|
|
1895
2165
|
:param request: The client callback request to send to the user.
|
|
1896
2166
|
:param func: The ClientCallback function to call with the given request as input.
|
|
1897
2167
|
:param kwargs: Additional keywords for the provided function call.
|
|
1898
|
-
:return: The response from the client callback
|
|
2168
|
+
:return: The response from the client callback.
|
|
1899
2169
|
"""
|
|
1900
2170
|
try:
|
|
1901
2171
|
self.user.timeout_seconds = self.timeout_seconds
|
|
@@ -1904,10 +2174,73 @@ class CallbackUtil:
|
|
|
1904
2174
|
raise SapioDialogTimeoutException()
|
|
1905
2175
|
finally:
|
|
1906
2176
|
self.user.timeout_seconds = self._original_timeout
|
|
2177
|
+
return response
|
|
2178
|
+
|
|
2179
|
+
def __send_dialog(self, request: Any, func: Callable, **kwargs) -> Any:
|
|
2180
|
+
"""
|
|
2181
|
+
Send a client callback request to the user that creates a dialog.
|
|
2182
|
+
|
|
2183
|
+
This function handles updating the user object's request timeout to match the request timeout of this
|
|
2184
|
+
CallbackUtil for the duration of the dialog.
|
|
2185
|
+
If the dialog times out then a SapioDialogTimeoutException is thrown.
|
|
2186
|
+
If the user cancels the dialog then a SapioUserCancelledException is thrown.
|
|
2187
|
+
|
|
2188
|
+
:param request: The client callback request to send to the user.
|
|
2189
|
+
:param func: The ClientCallback function to call with the given request as input.
|
|
2190
|
+
:param kwargs: Additional keywords for the provided function call.
|
|
2191
|
+
:return: The response from the client callback, if one was received.
|
|
2192
|
+
"""
|
|
2193
|
+
response: Any | None = self.__handle_timeout(func, request, **kwargs)
|
|
1907
2194
|
if response is None:
|
|
1908
2195
|
raise SapioUserCancelledException()
|
|
1909
2196
|
return response
|
|
1910
2197
|
|
|
2198
|
+
def __send_dialog_blank_results(self, request: Any, func: Callable, not_blank_func: Callable,
|
|
2199
|
+
handling: BlankResultHandling,
|
|
2200
|
+
repeat_message: str | None, cancel_message: str | None, **kwargs):
|
|
2201
|
+
"""
|
|
2202
|
+
Send a client callback request to the user that creates a dialog.
|
|
2203
|
+
|
|
2204
|
+
This function handles updating the user object's request timeout to match the request timeout of this
|
|
2205
|
+
CallbackUtil for the duration of the dialog.
|
|
2206
|
+
If the dialog times out then a SapioDialogTimeoutException is thrown.
|
|
2207
|
+
If the user cancels the dialog then a SapioUserCancelledException is thrown.
|
|
2208
|
+
If the user provides a blank result, then the handling is used to determine what to do with that result.
|
|
2209
|
+
|
|
2210
|
+
:param request: The client callback request to send to the user.
|
|
2211
|
+
:param func: The ClientCallback function to call with the given request as input.
|
|
2212
|
+
:param not_blank_func: The function to determine whether the provided result is blank or not.
|
|
2213
|
+
:param handling: The handling to use for blank results.
|
|
2214
|
+
:param repeat_message: If handling is REPEAT and a repeat_message is provided, then that message appears as
|
|
2215
|
+
toaster text when the user provides a blank result.
|
|
2216
|
+
:param cancel_message: If handling is CANCEL and a cancel_message is provided, then that message appears as
|
|
2217
|
+
toaster text when the user provides a blank result.
|
|
2218
|
+
:param kwargs: Additional keywords for the provided function call.
|
|
2219
|
+
:return: The response from the client callback, if one was received.
|
|
2220
|
+
"""
|
|
2221
|
+
if handling == BlankResultHandling.DEFAULT or handling is None:
|
|
2222
|
+
handling = self._default_blank_result_handling
|
|
2223
|
+
while True:
|
|
2224
|
+
response: Any | None = self.__handle_timeout(func, request, **kwargs)
|
|
2225
|
+
if response is None:
|
|
2226
|
+
raise SapioUserCancelledException()
|
|
2227
|
+
if not_blank_func(response):
|
|
2228
|
+
return response
|
|
2229
|
+
match handling:
|
|
2230
|
+
case BlankResultHandling.CANCEL:
|
|
2231
|
+
# If the user provided no selection, throw an exception.
|
|
2232
|
+
if cancel_message:
|
|
2233
|
+
self.toaster_popup(cancel_message, popup_type=PopupType.Warning)
|
|
2234
|
+
raise SapioUserCancelledException()
|
|
2235
|
+
case BlankResultHandling.REPEAT:
|
|
2236
|
+
# If the user provided no selection, repeat the dialog.
|
|
2237
|
+
# If a repeatMessage is provided, display it as a toaster popup.
|
|
2238
|
+
if repeat_message:
|
|
2239
|
+
self.toaster_popup(repeat_message, popup_type=PopupType.Warning)
|
|
2240
|
+
case BlankResultHandling.RETURN:
|
|
2241
|
+
# If the user provided no selection, return the blank result.
|
|
2242
|
+
return response
|
|
2243
|
+
|
|
1911
2244
|
|
|
1912
2245
|
class FieldModifier:
|
|
1913
2246
|
"""
|
|
@@ -1983,15 +2316,15 @@ class FieldFilterCriteria:
|
|
|
1983
2316
|
key_field: bool | None
|
|
1984
2317
|
identifier: bool | None
|
|
1985
2318
|
system_field: bool | None
|
|
1986
|
-
field_types:
|
|
1987
|
-
not_field_types:
|
|
2319
|
+
field_types: Container[FieldType] | None
|
|
2320
|
+
not_field_types: Container[FieldType] | None
|
|
1988
2321
|
matches_tag: str | None
|
|
1989
2322
|
contains_tag: str | None
|
|
1990
2323
|
regex_tag: str | re.Pattern[str] | None
|
|
1991
2324
|
|
|
1992
2325
|
def __init__(self, *, required: bool | None = None, editable: bool | None = None, key_field: bool | None = None,
|
|
1993
2326
|
identifier: bool | None = None, system_field: bool | None = None,
|
|
1994
|
-
field_types:
|
|
2327
|
+
field_types: Container[FieldType] | None = None, not_field_types: Container[FieldType] | None = None,
|
|
1995
2328
|
matches_tag: str | None = None, contains_tag: str | None = None,
|
|
1996
2329
|
regex_tag: str | re.Pattern[str] | None = None):
|
|
1997
2330
|
"""
|