sapiopycommons 2025.4.8a473__py3-none-any.whl → 2025.4.9a150__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 +392 -1262
- sapiopycommons/callbacks/field_builder.py +0 -2
- sapiopycommons/chem/Molecules.py +2 -0
- sapiopycommons/customreport/term_builder.py +1 -1
- sapiopycommons/datatype/attachment_util.py +2 -4
- sapiopycommons/datatype/data_fields.py +1 -23
- sapiopycommons/eln/experiment_handler.py +279 -933
- sapiopycommons/eln/experiment_report_util.py +10 -15
- sapiopycommons/eln/plate_designer.py +59 -159
- sapiopycommons/files/file_bridge.py +0 -76
- sapiopycommons/files/file_bridge_handler.py +110 -325
- sapiopycommons/files/file_data_handler.py +2 -2
- sapiopycommons/files/file_util.py +15 -40
- sapiopycommons/files/file_validator.py +5 -6
- sapiopycommons/files/file_writer.py +1 -1
- sapiopycommons/flowcyto/flow_cyto.py +1 -1
- sapiopycommons/general/accession_service.py +3 -3
- sapiopycommons/general/aliases.py +28 -51
- sapiopycommons/general/audit_log.py +2 -2
- sapiopycommons/general/custom_report_util.py +1 -24
- sapiopycommons/general/exceptions.py +2 -41
- sapiopycommons/general/popup_util.py +2 -2
- sapiopycommons/multimodal/multimodal.py +0 -1
- sapiopycommons/processtracking/custom_workflow_handler.py +30 -46
- sapiopycommons/recordmodel/record_handler.py +159 -547
- sapiopycommons/rules/eln_rule_handler.py +30 -41
- sapiopycommons/rules/on_save_rule_handler.py +30 -41
- sapiopycommons/webhook/webhook_handlers.py +55 -448
- sapiopycommons/webhook/webservice_handlers.py +2 -2
- {sapiopycommons-2025.4.8a473.dist-info → sapiopycommons-2025.4.9a150.dist-info}/METADATA +1 -1
- sapiopycommons-2025.4.9a150.dist-info/RECORD +59 -0
- sapiopycommons/customreport/auto_pagers.py +0 -281
- sapiopycommons/eln/experiment_cache.py +0 -173
- sapiopycommons/eln/experiment_step_factory.py +0 -474
- sapiopycommons/eln/experiment_tags.py +0 -7
- sapiopycommons/eln/step_creation.py +0 -235
- sapiopycommons/general/data_structure_util.py +0 -115
- sapiopycommons/general/directive_util.py +0 -86
- sapiopycommons/samples/aliquot.py +0 -48
- sapiopycommons-2025.4.8a473.dist-info/RECORD +0 -67
- {sapiopycommons-2025.4.8a473.dist-info → sapiopycommons-2025.4.9a150.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.4.8a473.dist-info → sapiopycommons-2025.4.9a150.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import io
|
|
4
|
-
import re
|
|
5
|
-
import warnings
|
|
6
|
-
from copy import copy
|
|
7
|
-
from typing import Iterable, TypeAlias, Any, Callable, Container, Collection
|
|
8
4
|
from weakref import WeakValueDictionary
|
|
9
5
|
|
|
10
6
|
from requests import ReadTimeout
|
|
11
7
|
from sapiopylib.rest.ClientCallbackService import ClientCallback
|
|
12
8
|
from sapiopylib.rest.DataMgmtService import DataMgmtServer
|
|
13
|
-
from sapiopylib.rest.DataTypeService import DataTypeManager
|
|
14
9
|
from sapiopylib.rest.User import SapioUser
|
|
15
10
|
from sapiopylib.rest.pojo.CustomReport import CustomReport, CustomReportCriteria
|
|
16
11
|
from sapiopylib.rest.pojo.DataRecord import DataRecord
|
|
17
12
|
from sapiopylib.rest.pojo.datatype.DataType import DataTypeDefinition
|
|
18
13
|
from sapiopylib.rest.pojo.datatype.DataTypeLayout import DataTypeLayout
|
|
19
14
|
from sapiopylib.rest.pojo.datatype.FieldDefinition import AbstractVeloxFieldDefinition, VeloxStringFieldDefinition, \
|
|
20
|
-
VeloxIntegerFieldDefinition, VeloxDoubleFieldDefinition,
|
|
21
|
-
from sapiopylib.rest.pojo.datatype.TemporaryDataType import TemporaryDataType
|
|
15
|
+
VeloxIntegerFieldDefinition, VeloxDoubleFieldDefinition, FieldDefinitionParser
|
|
22
16
|
from sapiopylib.rest.pojo.webhook.ClientCallbackRequest import OptionDialogRequest, ListDialogRequest, \
|
|
23
17
|
FormEntryDialogRequest, InputDialogCriteria, TableEntryDialogRequest, ESigningRequestPojo, \
|
|
24
18
|
DataRecordDialogRequest, InputSelectionRequest, FilePromptRequest, MultiFilePromptRequest, \
|
|
@@ -28,32 +22,20 @@ from sapiopylib.rest.pojo.webhook.WebhookEnums import FormAccessLevel, ScanToSel
|
|
|
28
22
|
from sapiopylib.rest.utils.DataTypeCacheManager import DataTypeCacheManager
|
|
29
23
|
from sapiopylib.rest.utils.FormBuilder import FormBuilder
|
|
30
24
|
from sapiopylib.rest.utils.recorddatasinks import InMemoryRecordDataSink
|
|
31
|
-
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
|
|
32
25
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
|
|
33
26
|
|
|
34
|
-
from sapiopycommons.callbacks.field_builder import FieldBuilder, AnyFieldInfo
|
|
35
27
|
from sapiopycommons.files.file_util import FileUtil
|
|
36
28
|
from sapiopycommons.general.aliases import FieldMap, SapioRecord, AliasUtil, RecordIdentifier, FieldValue, \
|
|
37
|
-
UserIdentifier
|
|
29
|
+
UserIdentifier
|
|
38
30
|
from sapiopycommons.general.custom_report_util import CustomReportUtil
|
|
39
31
|
from sapiopycommons.general.exceptions import SapioUserCancelledException, SapioException, SapioUserErrorException, \
|
|
40
32
|
SapioDialogTimeoutException
|
|
41
33
|
from sapiopycommons.recordmodel.record_handler import RecordHandler
|
|
42
34
|
|
|
43
|
-
DataTypeLayoutIdentifier: TypeAlias = DataTypeLayout | str | None
|
|
44
35
|
|
|
45
|
-
|
|
46
|
-
# CR-47521: Updated various parameter type hints from list or Iterable to more specific type hints.
|
|
47
|
-
# If we need to iterate over the parameter, then it is Iterable.
|
|
48
|
-
# If we need to see if the parameter contains a value, then it is Container.
|
|
49
|
-
# If the length/size of the parameter is needed, then it is Collection.
|
|
50
|
-
# If we need to access the parameter by an index, then it is Sequence. (This excludes sets and dictionaries, so it's
|
|
51
|
-
# probably better to accept a Collection then cast the parameter to a list if you need to get an element from it.)
|
|
52
36
|
class CallbackUtil:
|
|
53
37
|
user: SapioUser
|
|
54
38
|
callback: ClientCallback
|
|
55
|
-
rec_handler: RecordHandler
|
|
56
|
-
dt_man: DataTypeManager
|
|
57
39
|
dt_cache: DataTypeCacheManager
|
|
58
40
|
_original_timeout: int
|
|
59
41
|
timeout_seconds: int
|
|
@@ -63,10 +45,6 @@ class CallbackUtil:
|
|
|
63
45
|
__instances: WeakValueDictionary[SapioUser, CallbackUtil] = WeakValueDictionary()
|
|
64
46
|
__initialized: bool
|
|
65
47
|
|
|
66
|
-
# TODO: Remove this if ever the DataTypeCacheManager starts handling it.
|
|
67
|
-
__layouts: dict[str, dict[str, DataTypeLayout]]
|
|
68
|
-
"""A cache for data type layouts that have been requested by this CallbackUtil."""
|
|
69
|
-
|
|
70
48
|
def __new__(cls, context: UserIdentifier):
|
|
71
49
|
"""
|
|
72
50
|
:param context: The current webhook context or a user object to send requests from.
|
|
@@ -89,14 +67,11 @@ class CallbackUtil:
|
|
|
89
67
|
|
|
90
68
|
self.user = AliasUtil.to_sapio_user(context)
|
|
91
69
|
self.callback = DataMgmtServer.get_client_callback(self.user)
|
|
92
|
-
self.rec_handler = RecordHandler(self.user)
|
|
93
|
-
self.dt_man = DataMgmtServer.get_data_type_manager(self.user)
|
|
94
70
|
self.dt_cache = DataTypeCacheManager(self.user)
|
|
95
71
|
self._original_timeout = self.user.timeout_seconds
|
|
96
72
|
self.timeout_seconds = self.user.timeout_seconds
|
|
97
73
|
self.width_pixels = None
|
|
98
74
|
self.width_percent = None
|
|
99
|
-
self.__layouts = {}
|
|
100
75
|
|
|
101
76
|
def set_dialog_width(self, width_pixels: int | None = None, width_percent: float | None = None):
|
|
102
77
|
"""
|
|
@@ -126,7 +101,7 @@ class CallbackUtil:
|
|
|
126
101
|
"""
|
|
127
102
|
Display a toaster popup in the bottom right corner of the user's screen.
|
|
128
103
|
|
|
129
|
-
:param message: The message to display in the toaster.
|
|
104
|
+
:param message: The message to display in the toaster.
|
|
130
105
|
:param title: The title to display at the top of the toaster.
|
|
131
106
|
:param popup_type: The popup type to use for the toaster. This controls the color that the toaster appears with.
|
|
132
107
|
Info is blue, Success is green, Warning is yellow, and Error is red
|
|
@@ -138,7 +113,7 @@ class CallbackUtil:
|
|
|
138
113
|
Display an info message to the user in a dialog. Repeated calls to this function will append the new messages
|
|
139
114
|
to the same dialog if it is still opened by the user.
|
|
140
115
|
|
|
141
|
-
:param message: The message to display to the user.
|
|
116
|
+
:param message: The message to display to the user.
|
|
142
117
|
"""
|
|
143
118
|
self.callback.display_info(message)
|
|
144
119
|
|
|
@@ -147,7 +122,7 @@ class CallbackUtil:
|
|
|
147
122
|
Display a warning message to the user in a dialog. Repeated calls to this function will append the new messages
|
|
148
123
|
to the same dialog if it is still opened by the user.
|
|
149
124
|
|
|
150
|
-
:param message: The message to display to the user.
|
|
125
|
+
:param message: The message to display to the user.
|
|
151
126
|
"""
|
|
152
127
|
self.callback.display_warning(message)
|
|
153
128
|
|
|
@@ -156,17 +131,17 @@ class CallbackUtil:
|
|
|
156
131
|
Display an error message to the user in a dialog. Repeated calls to this function will append the new messages
|
|
157
132
|
to the same dialog if it is still opened by the user.
|
|
158
133
|
|
|
159
|
-
:param message: The message to display to the user.
|
|
134
|
+
:param message: The message to display to the user.
|
|
160
135
|
"""
|
|
161
136
|
self.callback.display_error(message)
|
|
162
137
|
|
|
163
|
-
def option_dialog(self, title: str, msg: str, options:
|
|
138
|
+
def option_dialog(self, title: str, msg: str, options: list[str], default_option: int = 0,
|
|
164
139
|
user_can_cancel: bool = False) -> str:
|
|
165
140
|
"""
|
|
166
141
|
Create an option dialog with the given options for the user to choose from.
|
|
167
142
|
|
|
168
143
|
:param title: The title of the dialog.
|
|
169
|
-
:param msg: The message to display in the dialog.
|
|
144
|
+
:param msg: The message to display in the dialog.
|
|
170
145
|
:param options: The button options that the user has to choose from.
|
|
171
146
|
:param default_option: The index of the option in the options list that defaults as the first choice.
|
|
172
147
|
:param user_can_cancel: True if the user is able to click the X to close the dialog. False if the user cannot
|
|
@@ -174,35 +149,36 @@ class CallbackUtil:
|
|
|
174
149
|
SapioUserCancelledException is thrown.
|
|
175
150
|
:return: The name of the button that the user selected.
|
|
176
151
|
"""
|
|
177
|
-
if not options:
|
|
178
|
-
raise SapioException("No options provided.")
|
|
179
|
-
|
|
180
|
-
# Convert the iterable of options to a list of options.
|
|
181
|
-
options: list[str] = list(options)
|
|
182
|
-
|
|
183
|
-
# Send the request to the user.
|
|
184
152
|
request = OptionDialogRequest(title, msg, options, default_option, user_can_cancel,
|
|
185
153
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
186
|
-
|
|
154
|
+
try:
|
|
155
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
156
|
+
response: int | None = self.callback.show_option_dialog(request)
|
|
157
|
+
except ReadTimeout:
|
|
158
|
+
raise SapioDialogTimeoutException()
|
|
159
|
+
finally:
|
|
160
|
+
self.user.timeout_seconds = self._original_timeout
|
|
161
|
+
if response is None:
|
|
162
|
+
raise SapioUserCancelledException()
|
|
187
163
|
return options[response]
|
|
188
164
|
|
|
189
165
|
def ok_dialog(self, title: str, msg: str) -> None:
|
|
190
166
|
"""
|
|
191
167
|
Create an option dialog where the only option is "OK". Doesn't allow the user to cancel the
|
|
192
|
-
dialog using the X
|
|
168
|
+
dialog using the X at the top right corner. Returns nothing.
|
|
193
169
|
|
|
194
170
|
:param title: The title of the dialog.
|
|
195
|
-
:param msg: The message to display in the dialog.
|
|
171
|
+
:param msg: The message to display in the dialog.
|
|
196
172
|
"""
|
|
197
173
|
self.option_dialog(title, msg, ["OK"], 0, False)
|
|
198
174
|
|
|
199
175
|
def ok_cancel_dialog(self, title: str, msg: str, default_ok: bool = True) -> bool:
|
|
200
176
|
"""
|
|
201
177
|
Create an option dialog where the only options are "OK" and "Cancel". Doesn't allow the user to cancel the
|
|
202
|
-
dialog using the X
|
|
178
|
+
dialog using the X at the top right corner.
|
|
203
179
|
|
|
204
180
|
:param title: The title of the dialog.
|
|
205
|
-
:param msg: The message to display in the dialog.
|
|
181
|
+
:param msg: The message to display in the dialog.
|
|
206
182
|
:param default_ok: If true, "OK" is the default choice. Otherwise, the default choice is "Cancel".
|
|
207
183
|
:return: True if the user selected OK. False if the user selected Cancel.
|
|
208
184
|
"""
|
|
@@ -211,25 +187,17 @@ class CallbackUtil:
|
|
|
211
187
|
def yes_no_dialog(self, title: str, msg: str, default_yes: bool = True) -> bool:
|
|
212
188
|
"""
|
|
213
189
|
Create an option dialog where the only options are "Yes" and "No". Doesn't allow the user to cancel the
|
|
214
|
-
dialog using the X
|
|
190
|
+
dialog using the X at the top right corner.
|
|
215
191
|
|
|
216
192
|
:param title: The title of the dialog.
|
|
217
|
-
:param msg: The message to display in the dialog.
|
|
193
|
+
:param msg: The message to display in the dialog.
|
|
218
194
|
:param default_yes: If true, "Yes" is the default choice. Otherwise, the default choice is "No".
|
|
219
195
|
:return: True if the user selected Yes. False if the user selected No.
|
|
220
196
|
"""
|
|
221
197
|
return self.option_dialog(title, msg, ["Yes", "No"], 0 if default_yes else 1, False) == "Yes"
|
|
222
198
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def list_dialog(self,
|
|
226
|
-
title: str,
|
|
227
|
-
options: Iterable[str],
|
|
228
|
-
multi_select: bool = False,
|
|
229
|
-
preselected_values: Iterable[str] | None = None,
|
|
230
|
-
*,
|
|
231
|
-
require_selection: bool = False,
|
|
232
|
-
repeat_message: str | None = "Please provide a selection to continue.") -> list[str]:
|
|
199
|
+
def list_dialog(self, title: str, options: list[str], multi_select: bool = False,
|
|
200
|
+
preselected_values: list[str] | None = None) -> list[str]:
|
|
233
201
|
"""
|
|
234
202
|
Create a list dialog with the given options for the user to choose from.
|
|
235
203
|
|
|
@@ -238,37 +206,29 @@ class CallbackUtil:
|
|
|
238
206
|
:param multi_select: Whether the user is able to select multiple options from the list.
|
|
239
207
|
:param preselected_values: A list of values that will already be selected when the list dialog is created. The
|
|
240
208
|
user can unselect these values if they want to.
|
|
241
|
-
:param require_selection: If true, the request will be re-sent if the user submits the dialog without making
|
|
242
|
-
a selection.
|
|
243
|
-
:param repeat_message: If require_selection is true and a repeat_message is provided, then that message appears
|
|
244
|
-
as toaster text if the dialog is repeated.
|
|
245
209
|
:return: The list of options that the user selected.
|
|
246
210
|
"""
|
|
247
|
-
|
|
248
|
-
raise SapioException("No options provided.")
|
|
249
|
-
|
|
250
|
-
# Send the request to the user.
|
|
251
|
-
request = ListDialogRequest(title, multi_select, list(options),
|
|
252
|
-
list(preselected_values) if preselected_values else None,
|
|
211
|
+
request = ListDialogRequest(title, multi_select, options, preselected_values,
|
|
253
212
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
213
|
+
try:
|
|
214
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
215
|
+
response: list[str] | None = self.callback.show_list_dialog(request)
|
|
216
|
+
except ReadTimeout:
|
|
217
|
+
raise SapioDialogTimeoutException()
|
|
218
|
+
finally:
|
|
219
|
+
self.user.timeout_seconds = self._original_timeout
|
|
220
|
+
if response is None:
|
|
221
|
+
raise SapioUserCancelledException()
|
|
262
222
|
return response
|
|
263
223
|
|
|
264
224
|
def form_dialog(self,
|
|
265
225
|
title: str,
|
|
266
226
|
msg: str,
|
|
267
|
-
fields:
|
|
227
|
+
fields: list[AbstractVeloxFieldDefinition],
|
|
268
228
|
values: FieldMap = None,
|
|
269
229
|
column_positions: dict[str, tuple[int, int]] = None,
|
|
270
230
|
*,
|
|
271
|
-
data_type:
|
|
231
|
+
data_type: str = "Default",
|
|
272
232
|
display_name: str | None = None,
|
|
273
233
|
plural_display_name: str | None = None) -> FieldMap:
|
|
274
234
|
"""
|
|
@@ -276,7 +236,7 @@ class CallbackUtil:
|
|
|
276
236
|
provide the definitions of every field in the form.
|
|
277
237
|
|
|
278
238
|
:param title: The title of the dialog.
|
|
279
|
-
:param msg: The message to display at the top of the form.
|
|
239
|
+
:param msg: The message to display at the top of the form.
|
|
280
240
|
:param fields: The definitions of the fields to display in the form. Fields will be displayed in the order they
|
|
281
241
|
are provided in this list.
|
|
282
242
|
:param values: Sets the default values of the fields.
|
|
@@ -290,253 +250,133 @@ class CallbackUtil:
|
|
|
290
250
|
:return: A dictionary mapping the data field names of the given field definitions to the response value from
|
|
291
251
|
the user for that field.
|
|
292
252
|
"""
|
|
293
|
-
|
|
294
|
-
|
|
253
|
+
if display_name is None:
|
|
254
|
+
display_name = data_type
|
|
255
|
+
if plural_display_name is None:
|
|
256
|
+
plural_display_name = display_name + "s"
|
|
295
257
|
|
|
296
|
-
|
|
297
|
-
|
|
258
|
+
builder = FormBuilder(data_type, display_name, plural_display_name)
|
|
259
|
+
for field_def in fields:
|
|
260
|
+
field_name = field_def.data_field_name
|
|
261
|
+
column: int = 0
|
|
262
|
+
span: int = 4
|
|
263
|
+
if column_positions and field_name in column_positions:
|
|
264
|
+
position = column_positions.get(field_name)
|
|
265
|
+
column = position[0]
|
|
266
|
+
span = position[1]
|
|
267
|
+
builder.add_field(field_def, column, span)
|
|
268
|
+
|
|
269
|
+
request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
|
|
298
270
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
299
|
-
|
|
271
|
+
try:
|
|
272
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
273
|
+
response: FieldMap | None = self.callback.show_form_entry_dialog(request)
|
|
274
|
+
except ReadTimeout:
|
|
275
|
+
raise SapioDialogTimeoutException()
|
|
276
|
+
finally:
|
|
277
|
+
self.user.timeout_seconds = self._original_timeout
|
|
278
|
+
if response is None:
|
|
279
|
+
raise SapioUserCancelledException()
|
|
300
280
|
return response
|
|
301
281
|
|
|
302
282
|
def record_form_dialog(self,
|
|
303
283
|
title: str,
|
|
304
284
|
msg: str,
|
|
305
|
-
fields:
|
|
285
|
+
fields: list[str],
|
|
306
286
|
record: SapioRecord,
|
|
307
|
-
column_positions: dict[str, tuple[int, int]]
|
|
308
|
-
editable=
|
|
309
|
-
*,
|
|
310
|
-
default_modifier: FieldModifier | None = None,
|
|
311
|
-
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None) -> FieldMap:
|
|
287
|
+
column_positions: dict[str, tuple[int, int]] = None,
|
|
288
|
+
editable: bool | None = True) -> FieldMap:
|
|
312
289
|
"""
|
|
313
290
|
Create a form dialog where the user may input data into the fields of the form. The form is constructed from
|
|
314
|
-
a given record.
|
|
291
|
+
a given record. Provided field names must match fields on the definition of the data type of the given record.
|
|
292
|
+
The fields that are displayed will have their default value be that of the fields on the given record.
|
|
315
293
|
|
|
316
294
|
Makes webservice calls to get the data type definition and fields of the given record if they weren't
|
|
317
295
|
previously cached.
|
|
318
296
|
|
|
319
297
|
:param title: The title of the dialog.
|
|
320
|
-
:param msg: The message to display in the dialog.
|
|
321
|
-
:param fields: The names of the fields to display
|
|
322
|
-
|
|
323
|
-
[Extension Data Type Name].[Data Field Name]. This parameter may also be an identifier for a data type
|
|
324
|
-
layout from the data type of the provided records. If None, then the layout assigned to the current user's
|
|
325
|
-
group for this data type will be used.
|
|
298
|
+
:param msg: The message to display in the dialog.
|
|
299
|
+
:param fields: The data field names of the fields from the record to display in the form. Fields will be
|
|
300
|
+
displayed in the order they are provided in this list.
|
|
326
301
|
:param record: The record to display the values of.
|
|
327
302
|
:param column_positions: If a tuple is provided for a field name, alters that field's column position and column
|
|
328
|
-
span. (Field order is still determined by the fields list.)
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
:param default_modifier: A default field modifier that will be applied to the given fields. This can be used to
|
|
332
|
-
make field definitions from the system behave differently than their system values. If this value is None,
|
|
333
|
-
then a default field modifier is created that causes all specified fields to be both visible and not key
|
|
334
|
-
fields. (Key fields get displayed first before any non-key fields in tables, so the key field setting is
|
|
335
|
-
disabled by default in order to have the columns in the table respect the order of the fields as they are
|
|
336
|
-
provided to this function.)
|
|
337
|
-
:param field_modifiers: A mapping of data field name to field modifier for changes that should be applied to
|
|
338
|
-
the matching field. If a data field name is not present in the provided dict, or the provided dictionary is
|
|
339
|
-
None, then the default modifier will be used.
|
|
303
|
+
span. (Field order is still determined by the fields list.)
|
|
304
|
+
:param editable: If true, all fields are displayed as editable. If false, all fields are displayed as
|
|
305
|
+
uneditable. If none, only those fields that are defined as editable by the data designer will be editable.
|
|
340
306
|
:return: A dictionary mapping the data field names of the given field definitions to the response value from
|
|
341
307
|
the user for that field.
|
|
342
308
|
"""
|
|
343
|
-
#
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
309
|
+
# Get the field definitions of the data type.
|
|
310
|
+
data_type: str = record.data_type_name
|
|
311
|
+
type_def: DataTypeDefinition = self.dt_cache.get_data_type(data_type)
|
|
312
|
+
field_defs: dict[str, AbstractVeloxFieldDefinition] = self.dt_cache.get_fields_for_type(data_type)
|
|
347
313
|
|
|
348
|
-
#
|
|
349
|
-
|
|
350
|
-
values: dict[str, FieldValue] = AliasUtil.to_field_map(record)
|
|
314
|
+
# Make everything visible, because presumably the caller gave a field name because they want it to be seen.
|
|
315
|
+
modifier = FieldModifier(visible=True, editable=editable)
|
|
351
316
|
|
|
352
|
-
#
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
request = FormEntryDialogRequest(title, msg, temp_dt, values,
|
|
317
|
+
# Build the form using only those fields that are desired.
|
|
318
|
+
values: dict[str, FieldValue] = {}
|
|
319
|
+
builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
|
|
320
|
+
for field_name in fields:
|
|
321
|
+
field_def = field_defs.get(field_name)
|
|
322
|
+
if field_def is None:
|
|
323
|
+
raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
|
|
324
|
+
values[field_name] = record.get_field_value(field_name)
|
|
325
|
+
column: int = 0
|
|
326
|
+
span: int = 4
|
|
327
|
+
if column_positions and field_name in column_positions:
|
|
328
|
+
position = column_positions.get(field_name)
|
|
329
|
+
column = position[0]
|
|
330
|
+
span = position[1]
|
|
331
|
+
builder.add_field(modifier.modify_field(field_def), column, span)
|
|
332
|
+
|
|
333
|
+
request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
|
|
370
334
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
371
|
-
|
|
335
|
+
try:
|
|
336
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
337
|
+
response: FieldMap | None = self.callback.show_form_entry_dialog(request)
|
|
338
|
+
except ReadTimeout:
|
|
339
|
+
raise SapioDialogTimeoutException()
|
|
340
|
+
finally:
|
|
341
|
+
self.user.timeout_seconds = self._original_timeout
|
|
342
|
+
if response is None:
|
|
343
|
+
raise SapioUserCancelledException()
|
|
372
344
|
return response
|
|
373
345
|
|
|
374
|
-
|
|
375
|
-
def set_record_form_dialog(self,
|
|
376
|
-
title: str,
|
|
377
|
-
msg: str,
|
|
378
|
-
fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
|
|
379
|
-
record: SapioRecord,
|
|
380
|
-
column_positions: dict[str, tuple[int, int]] | None = None,
|
|
381
|
-
*,
|
|
382
|
-
default_modifier: FieldModifier | None = None,
|
|
383
|
-
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None) -> None:
|
|
384
|
-
"""
|
|
385
|
-
Create a form dialog where the user may input data into the fields of the form. The form is constructed from
|
|
386
|
-
a given record. After the user submits this dialog, the values that the user provided are used to update the
|
|
387
|
-
provided record.
|
|
388
|
-
|
|
389
|
-
Makes webservice calls to get the data type definition and fields of the given record if they weren't
|
|
390
|
-
previously cached.
|
|
391
|
-
|
|
392
|
-
:param title: The title of the dialog.
|
|
393
|
-
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
394
|
-
:param fields: The names of the fields to display as columns in the table. These names must match field names on
|
|
395
|
-
the data type of the provided record. Provided field names may also be extension fields of the form
|
|
396
|
-
[Extension Data Type Name].[Data Field Name]. This parameter may also be an identifier for a data type
|
|
397
|
-
layout from the data type of the provided records. If None, then the layout assigned to the current user's
|
|
398
|
-
group for this data type will be used.
|
|
399
|
-
:param record: The record to display and update the values of.
|
|
400
|
-
:param column_positions: If a tuple is provided for a field name, alters that field's column position and column
|
|
401
|
-
span. (Field order is still determined by the fields list.) Has no effect if the fields parameter provides
|
|
402
|
-
a data type layout.
|
|
403
|
-
:param default_modifier: A default field modifier that will be applied to the given fields. This can be used to
|
|
404
|
-
make field definitions from the system behave differently than their system values. If this value is None,
|
|
405
|
-
then a default field modifier is created that causes all specified fields to be both visible and not key
|
|
406
|
-
fields. (Key fields get displayed first before any non-key fields in tables, so the key field setting is
|
|
407
|
-
disabled by default in order to have the columns in the table respect the order of the fields as they are
|
|
408
|
-
provided to this function.)
|
|
409
|
-
:param field_modifiers: A mapping of data field name to field modifier for changes that should be applied to
|
|
410
|
-
the matching field. If a data field name is not present in the provided dict, or the provided dictionary is
|
|
411
|
-
None, then the default modifier will be used.
|
|
412
|
-
"""
|
|
413
|
-
results: FieldMap = self.record_form_dialog(title, msg, fields, record, column_positions,
|
|
414
|
-
default_modifier=default_modifier, field_modifiers=field_modifiers)
|
|
415
|
-
record.set_field_values(results)
|
|
416
|
-
|
|
417
|
-
# CR-47491: Support providing a data type name string to receive PyRecordModels instead of requiring a WrapperType.
|
|
418
|
-
def create_record_form_dialog(self,
|
|
419
|
-
title: str,
|
|
420
|
-
msg: str,
|
|
421
|
-
fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
|
|
422
|
-
wrapper_type: type[WrappedType] | str,
|
|
423
|
-
column_positions: dict[str, tuple[int, int]] | None = None,
|
|
424
|
-
*,
|
|
425
|
-
default_modifier: FieldModifier | None = None,
|
|
426
|
-
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None) \
|
|
427
|
-
-> WrappedType | PyRecordModel:
|
|
428
|
-
"""
|
|
429
|
-
Create a form dialog where the user may input data into the fields of the form. The form is constructed from
|
|
430
|
-
a record that is created using the given record model wrapper. After the user submits this dialog, the values
|
|
431
|
-
that the user provided are used to update the created record.
|
|
432
|
-
|
|
433
|
-
Makes webservice calls to get the data type definition and fields of the given record if they weren't
|
|
434
|
-
previously cached.
|
|
435
|
-
|
|
436
|
-
:param title: The title of the dialog.
|
|
437
|
-
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
438
|
-
:param fields: The names of the fields to display as columns in the table. These names must match field names on
|
|
439
|
-
the data type of the provided wrapper. Provided field names may also be extension fields of the form
|
|
440
|
-
[Extension Data Type Name].[Data Field Name]. This parameter may also be an identifier for a data type
|
|
441
|
-
layout from the data type of the provided records. If None, then the layout assigned to the current user's
|
|
442
|
-
group for this data type will be used. FieldFilterCriteria may also be provided in lieu of field names.
|
|
443
|
-
:param wrapper_type: The record model wrapper or data type name of the record to be created and updated.
|
|
444
|
-
If a data type name is provided, the returned record will be a PyRecordModel instead of a
|
|
445
|
-
WrappedRecordModel.
|
|
446
|
-
:param column_positions: If a tuple is provided for a field name, alters that field's column position and column
|
|
447
|
-
span. (Field order is still determined by the fields list.) Has no effect if the fields parameter provides
|
|
448
|
-
a data type layout.
|
|
449
|
-
:param default_modifier: A default field modifier that will be applied to the given fields. This can be used to
|
|
450
|
-
make field definitions from the system behave differently than their system values. If this value is None,
|
|
451
|
-
then a default field modifier is created that causes all specified fields to be both visible and not key
|
|
452
|
-
fields. (Key fields get displayed first before any non-key fields in tables, so the key field setting is
|
|
453
|
-
disabled by default in order to have the columns in the table respect the order of the fields as they are
|
|
454
|
-
provided to this function.)
|
|
455
|
-
:param field_modifiers: A mapping of data field name to field modifier for changes that should be applied to
|
|
456
|
-
the matching field. If a data field name is not present in the provided dict, or the provided dictionary is
|
|
457
|
-
None, then the default modifier will be used.
|
|
458
|
-
:return: The record model that was created and updated by the user.
|
|
459
|
-
"""
|
|
460
|
-
record: WrappedType | PyRecordModel = self.rec_handler.add_model(wrapper_type)
|
|
461
|
-
self.set_record_form_dialog(title, msg, fields, record, column_positions,
|
|
462
|
-
default_modifier=default_modifier, field_modifiers=field_modifiers)
|
|
463
|
-
return record
|
|
464
|
-
|
|
465
|
-
def input_dialog(self,
|
|
466
|
-
title: str,
|
|
467
|
-
msg: str,
|
|
468
|
-
field: AbstractVeloxFieldDefinition,
|
|
469
|
-
*,
|
|
470
|
-
require_input: bool = False,
|
|
471
|
-
repeat_message: str | None = "Please provide a value to continue.") -> FieldValue:
|
|
346
|
+
def input_dialog(self, title: str, msg: str, field: AbstractVeloxFieldDefinition) -> FieldValue:
|
|
472
347
|
"""
|
|
473
348
|
Create an input dialog where the user must input data for a singular field.
|
|
474
349
|
|
|
475
350
|
:param title: The title of the dialog.
|
|
476
|
-
:param msg: The message to display in the dialog.
|
|
351
|
+
:param msg: The message to display in the dialog.
|
|
477
352
|
:param field: The definition for a field that the user must provide input to.
|
|
478
|
-
:param require_input: If true, the request will be re-sent if the user submits the dialog without providing an
|
|
479
|
-
input field value.
|
|
480
|
-
:param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
|
|
481
|
-
as toaster text if the dialog is repeated.
|
|
482
353
|
:return: The response value from the user for the given field.
|
|
483
354
|
"""
|
|
484
|
-
# Send the request to the user.
|
|
485
355
|
request = InputDialogCriteria(title, msg, field,
|
|
486
356
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
raw_response = self.user.post('/clientcallback/showInputDialog', payload=request.to_json())
|
|
497
|
-
# A response status code of 204 is what represents a cancelled dialog.
|
|
498
|
-
if raw_response.status_code == 204:
|
|
499
|
-
raise SapioUserCancelledException()
|
|
500
|
-
self.user.raise_for_status(raw_response)
|
|
501
|
-
json_dct: dict | None = self.user.get_json_data_or_none(raw_response)
|
|
502
|
-
response: FieldValue = json_dct['result'] if json_dct else None
|
|
503
|
-
except ReadTimeout:
|
|
504
|
-
raise SapioDialogTimeoutException()
|
|
505
|
-
finally:
|
|
506
|
-
self.user.timeout_seconds = self._original_timeout
|
|
507
|
-
# String fields that the user didn't provide will return as an empty string instead of a None response.
|
|
508
|
-
is_str: bool = isinstance(response, str)
|
|
509
|
-
if not require_input or (is_str and response) or (not is_str and response is not None):
|
|
510
|
-
break
|
|
511
|
-
if repeat_message:
|
|
512
|
-
self.toaster_popup(repeat_message, popup_type=PopupType.Warning)
|
|
357
|
+
try:
|
|
358
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
359
|
+
response: FieldValue | None = self.callback.show_input_dialog(request)
|
|
360
|
+
except ReadTimeout:
|
|
361
|
+
raise SapioDialogTimeoutException()
|
|
362
|
+
finally:
|
|
363
|
+
self.user.timeout_seconds = self._original_timeout
|
|
364
|
+
if response is None:
|
|
365
|
+
raise SapioUserCancelledException()
|
|
513
366
|
return response
|
|
514
367
|
|
|
515
|
-
def string_input_dialog(self,
|
|
516
|
-
|
|
517
|
-
msg: str,
|
|
518
|
-
field_name: str,
|
|
519
|
-
default_value: str | None = None,
|
|
520
|
-
max_length: int | None = None,
|
|
521
|
-
editable: bool = True,
|
|
522
|
-
*,
|
|
523
|
-
require_input: bool = False,
|
|
524
|
-
repeat_message: str | None = "Please provide a value to continue.",
|
|
525
|
-
**kwargs) -> str:
|
|
368
|
+
def string_input_dialog(self, title: str, msg: str, field_name: str, default_value: str | None = None,
|
|
369
|
+
max_length: int | None = None, editable: bool = True, **kwargs) -> str:
|
|
526
370
|
"""
|
|
527
371
|
Create an input dialog where the user must input data for a singular text field.
|
|
528
372
|
|
|
529
373
|
:param title: The title of the dialog.
|
|
530
|
-
:param msg: The message to display in the dialog.
|
|
374
|
+
:param msg: The message to display in the dialog.
|
|
531
375
|
:param field_name: The name and display name of the string field.
|
|
532
376
|
:param default_value: The default value to place into the string field, if any.
|
|
533
377
|
:param max_length: The max length of the string value. If not provided, uses the length of the default value.
|
|
534
378
|
If neither this nor a default value are provided, defaults to 100 characters.
|
|
535
379
|
:param editable: Whether the field is editable by the user.
|
|
536
|
-
:param require_input: If true, the request will be re-sent if the user submits the dialog without making
|
|
537
|
-
a selection.
|
|
538
|
-
:param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
|
|
539
|
-
as toaster text if the dialog is repeated.
|
|
540
380
|
:param kwargs: Any additional keyword arguments to pass to the field definition.
|
|
541
381
|
:return: The string that the user input into the dialog.
|
|
542
382
|
"""
|
|
@@ -544,36 +384,21 @@ class CallbackUtil:
|
|
|
544
384
|
max_length = len(default_value) if default_value else 100
|
|
545
385
|
field = VeloxStringFieldDefinition("Input", field_name, field_name, default_value=default_value,
|
|
546
386
|
max_length=max_length, editable=editable, **kwargs)
|
|
547
|
-
return self.input_dialog(title, msg, field
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
title: str,
|
|
552
|
-
msg: str,
|
|
553
|
-
field_name: str,
|
|
554
|
-
default_value: int = None,
|
|
555
|
-
min_value: int = -10000,
|
|
556
|
-
max_value: int = 10000,
|
|
557
|
-
editable: bool = True,
|
|
558
|
-
*,
|
|
559
|
-
require_input: bool = False,
|
|
560
|
-
repeat_message: str | None = "Please provide a value to continue.",
|
|
561
|
-
**kwargs) -> int:
|
|
387
|
+
return self.input_dialog(title, msg, field)
|
|
388
|
+
|
|
389
|
+
def integer_input_dialog(self, title: str, msg: str, field_name: str, default_value: int = None,
|
|
390
|
+
min_value: int = -10000, max_value: int = 10000, editable: bool = True, **kwargs) -> int:
|
|
562
391
|
"""
|
|
563
392
|
Create an input dialog where the user must input data for a singular integer field.
|
|
564
393
|
|
|
565
394
|
:param title: The title of the dialog.
|
|
566
|
-
:param msg: The message to display in the dialog.
|
|
395
|
+
:param msg: The message to display in the dialog.
|
|
567
396
|
:param field_name: The name and display name of the integer field.
|
|
568
397
|
:param default_value: The default value to place into the integer field. If not provided, defaults to the 0 or
|
|
569
398
|
the minimum value, whichever is higher.
|
|
570
399
|
:param min_value: The minimum allowed value of the input.
|
|
571
400
|
:param max_value: The maximum allowed value of the input.
|
|
572
401
|
:param editable: Whether the field is editable by the user.
|
|
573
|
-
:param require_input: If true, the request will be re-sent if the user submits the dialog without making
|
|
574
|
-
a selection.
|
|
575
|
-
:param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
|
|
576
|
-
as toaster text if the dialog is repeated.
|
|
577
402
|
:param kwargs: Any additional keyword arguments to pass to the field definition.
|
|
578
403
|
:return: The integer that the user input into the dialog.
|
|
579
404
|
"""
|
|
@@ -581,36 +406,22 @@ class CallbackUtil:
|
|
|
581
406
|
default_value = max(0, min_value)
|
|
582
407
|
field = VeloxIntegerFieldDefinition("Input", field_name, field_name, default_value=default_value,
|
|
583
408
|
min_value=min_value, max_value=max_value, editable=editable, **kwargs)
|
|
584
|
-
return self.input_dialog(title, msg, field
|
|
585
|
-
require_input=require_input, repeat_message=repeat_message)
|
|
409
|
+
return self.input_dialog(title, msg, field)
|
|
586
410
|
|
|
587
|
-
def double_input_dialog(self,
|
|
588
|
-
|
|
589
|
-
msg: str,
|
|
590
|
-
field_name: str,
|
|
591
|
-
default_value: float = None,
|
|
592
|
-
min_value: float = -10000000,
|
|
593
|
-
max_value: float = 100000000,
|
|
594
|
-
editable: bool = True,
|
|
595
|
-
*,
|
|
596
|
-
require_input: bool = False,
|
|
597
|
-
repeat_message: str | None = "Please provide a value to continue.",
|
|
411
|
+
def double_input_dialog(self, title: str, msg: str, field_name: str, default_value: float = None,
|
|
412
|
+
min_value: float = -10000000, max_value: float = 100000000, editable: bool = True,
|
|
598
413
|
**kwargs) -> float:
|
|
599
414
|
"""
|
|
600
415
|
Create an input dialog where the user must input data for a singular double field.
|
|
601
416
|
|
|
602
417
|
:param title: The title of the dialog.
|
|
603
|
-
:param msg: The message to display in the dialog.
|
|
418
|
+
:param msg: The message to display in the dialog.
|
|
604
419
|
:param field_name: The name and display name of the double field.
|
|
605
420
|
:param default_value: The default value to place into the double field. If not provided, defaults to the 0 or
|
|
606
421
|
the minimum value, whichever is higher.
|
|
607
422
|
:param min_value: The minimum allowed value of the input.
|
|
608
423
|
:param max_value: The maximum allowed value of the input.
|
|
609
424
|
:param editable: Whether the field is editable by the user.
|
|
610
|
-
:param require_input: If true, the request will be re-sent if the user submits the dialog without making
|
|
611
|
-
a selection.
|
|
612
|
-
:param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
|
|
613
|
-
as toaster text if the dialog is repeated.
|
|
614
425
|
:param kwargs: Any additional keyword arguments to pass to the field definition.
|
|
615
426
|
:return: The float that the user input into the dialog.
|
|
616
427
|
"""
|
|
@@ -618,26 +429,25 @@ class CallbackUtil:
|
|
|
618
429
|
default_value = max(0., min_value)
|
|
619
430
|
field = VeloxDoubleFieldDefinition("Input", field_name, field_name, default_value=default_value,
|
|
620
431
|
min_value=min_value, max_value=max_value, editable=editable, **kwargs)
|
|
621
|
-
return self.input_dialog(title, msg, field
|
|
622
|
-
require_input=require_input, repeat_message=repeat_message)
|
|
432
|
+
return self.input_dialog(title, msg, field)
|
|
623
433
|
|
|
624
434
|
def table_dialog(self,
|
|
625
435
|
title: str,
|
|
626
436
|
msg: str,
|
|
627
|
-
fields:
|
|
628
|
-
values:
|
|
437
|
+
fields: list[AbstractVeloxFieldDefinition],
|
|
438
|
+
values: list[FieldMap],
|
|
439
|
+
group_by: str | None = None,
|
|
440
|
+
image_data: list[bytes] | None = None,
|
|
629
441
|
*,
|
|
630
|
-
data_type:
|
|
442
|
+
data_type: str = "Default",
|
|
631
443
|
display_name: str | None = None,
|
|
632
|
-
plural_display_name: str | None = None
|
|
633
|
-
group_by: FieldIdentifier | None = None,
|
|
634
|
-
image_data: Iterable[bytes] | None = None) -> list[FieldMap]:
|
|
444
|
+
plural_display_name: str | None = None) -> list[FieldMap]:
|
|
635
445
|
"""
|
|
636
446
|
Create a table dialog where the user may input data into the fields of the table. Requires that the caller
|
|
637
447
|
provide the definitions of every field in the table.
|
|
638
448
|
|
|
639
449
|
:param title: The title of the dialog.
|
|
640
|
-
:param msg: The message to display at the top of the form.
|
|
450
|
+
:param msg: The message to display at the top of the form.
|
|
641
451
|
:param fields: The definitions of the fields to display as table columns. Fields will be displayed in the order
|
|
642
452
|
they are provided in this list.
|
|
643
453
|
:param values: The values to set for each row of the table.
|
|
@@ -653,61 +463,57 @@ class CallbackUtil:
|
|
|
653
463
|
:return: A list of dictionaries mapping the data field names of the given field definitions to the response
|
|
654
464
|
value from the user for that field for each row.
|
|
655
465
|
"""
|
|
656
|
-
if
|
|
657
|
-
|
|
466
|
+
if display_name is None:
|
|
467
|
+
display_name = data_type
|
|
468
|
+
if plural_display_name is None:
|
|
469
|
+
plural_display_name = display_name + "s"
|
|
658
470
|
|
|
659
|
-
#
|
|
660
|
-
|
|
661
|
-
|
|
471
|
+
# Key fields display their columns in order before all non-key fields.
|
|
472
|
+
# Unmark key fields so that the column order is respected exactly as the caller provides it.
|
|
473
|
+
modifier = FieldModifier(key_field=False)
|
|
662
474
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
temp_dt.record_image_assignable = bool(image_data)
|
|
475
|
+
builder = FormBuilder(data_type, display_name, plural_display_name)
|
|
476
|
+
for field in fields:
|
|
477
|
+
builder.add_field(modifier.modify_field(field))
|
|
667
478
|
|
|
668
|
-
|
|
669
|
-
request = TableEntryDialogRequest(title, msg, temp_dt, list(values),
|
|
479
|
+
request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
|
|
670
480
|
record_image_data_list=image_data, group_by_field=group_by,
|
|
671
481
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
672
|
-
|
|
482
|
+
try:
|
|
483
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
484
|
+
response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
|
|
485
|
+
except ReadTimeout:
|
|
486
|
+
raise SapioDialogTimeoutException()
|
|
487
|
+
finally:
|
|
488
|
+
self.user.timeout_seconds = self._original_timeout
|
|
489
|
+
if response is None:
|
|
490
|
+
raise SapioUserCancelledException()
|
|
673
491
|
return response
|
|
674
492
|
|
|
675
493
|
def record_table_dialog(self,
|
|
676
494
|
title: str,
|
|
677
495
|
msg: str,
|
|
678
|
-
fields:
|
|
679
|
-
records:
|
|
680
|
-
editable=
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
|
|
684
|
-
group_by: FieldIdentifier | None = None,
|
|
685
|
-
image_data: Iterable[bytes] | None = None) -> list[FieldMap]:
|
|
496
|
+
fields: list[str],
|
|
497
|
+
records: list[SapioRecord],
|
|
498
|
+
editable: bool | None = True,
|
|
499
|
+
group_by: str | None = None,
|
|
500
|
+
image_data: list[bytes] | None = None) -> list[FieldMap]:
|
|
686
501
|
"""
|
|
687
502
|
Create a table dialog where the user may input data into the fields of the table. The table is constructed from
|
|
688
|
-
a given list of records of a singular type.
|
|
503
|
+
a given list of records of a singular type. Provided field names must match fields on the definition of the data
|
|
504
|
+
type of the given records. The fields that are displayed will have their default value be that of the fields on
|
|
505
|
+
the given records.
|
|
689
506
|
|
|
690
507
|
Makes webservice calls to get the data type definition and fields of the given records if they weren't
|
|
691
508
|
previously cached.
|
|
692
509
|
|
|
693
510
|
:param title: The title of the dialog.
|
|
694
|
-
:param msg: The message to display in the dialog.
|
|
695
|
-
:param fields: The names of the fields to display as columns in the table. These names must match field names on
|
|
696
|
-
the data type of the provided record. Provided field names may also be extension fields of the form
|
|
697
|
-
[Extension Data Type Name].[Data Field Name]. This parameter may also be an identifier for a data type
|
|
698
|
-
layout from the data type of the provided records. If None, then the layout assigned to the current user's
|
|
699
|
-
group for this data type will be used.
|
|
511
|
+
:param msg: The message to display in the dialog.
|
|
700
512
|
:param records: The records to display as rows in the table.
|
|
701
|
-
:param
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
fields. (Key fields get displayed first before any non-key fields in tables, so the key field setting is
|
|
706
|
-
disabled by default in order to have the columns in the table respect the order of the fields as they are
|
|
707
|
-
provided to this function.)
|
|
708
|
-
:param field_modifiers: A mapping of data field name to field modifier for changes that should be applied to
|
|
709
|
-
the matching field. If a data field name is not present in the provided dict, or the provided dictionary is
|
|
710
|
-
None, then the default modifier will be used.
|
|
513
|
+
:param fields: The names of the fields to display as columns in the table. Fields will be displayed in the order
|
|
514
|
+
they are provided in this list.
|
|
515
|
+
:param editable: If true, all fields are displayed as editable. If false, all fields are displayed as
|
|
516
|
+
uneditable. If none, only those fields that are defined as editable by the data designer will be editable.
|
|
711
517
|
:param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
|
|
712
518
|
The user may remove this grouping if they want to.
|
|
713
519
|
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
@@ -715,353 +521,54 @@ class CallbackUtil:
|
|
|
715
521
|
:return: A list of dictionaries mapping the data field names of the given field definitions to the response
|
|
716
522
|
value from the user for that field for each row.
|
|
717
523
|
"""
|
|
718
|
-
# CR-47313: Replace the editable boolean with the default_modifier and field_modifiers parameters.
|
|
719
|
-
if editable is not None:
|
|
720
|
-
warnings.warn("The editable parameter is deprecated. Use the default_modifier and field_modifiers "
|
|
721
|
-
"parameters instead.", DeprecationWarning)
|
|
722
|
-
# Get the data type name and field values from the provided records.
|
|
723
524
|
if not records:
|
|
724
525
|
raise SapioException("No records provided.")
|
|
725
|
-
|
|
726
|
-
|
|
526
|
+
data_types: set[str] = {x.data_type_name for x in records}
|
|
527
|
+
if len(data_types) > 1:
|
|
528
|
+
raise SapioException("Multiple data type names encountered in records list for record table popup.")
|
|
529
|
+
data_type: str = data_types.pop()
|
|
530
|
+
# Get the field maps from the records.
|
|
531
|
+
field_map_list: list[FieldMap] = AliasUtil.to_field_map_lists(records)
|
|
532
|
+
# Get the field definitions of the data type.
|
|
533
|
+
type_def: DataTypeDefinition = self.dt_cache.get_data_type(data_type)
|
|
534
|
+
field_defs: dict[str, AbstractVeloxFieldDefinition] = self.dt_cache.get_fields_for_type(data_type)
|
|
727
535
|
|
|
728
|
-
#
|
|
729
|
-
|
|
730
|
-
|
|
536
|
+
# Key fields display their columns in order before all non-key fields.
|
|
537
|
+
# Unmark key fields so that the column order is respected exactly as the caller provides it.
|
|
538
|
+
# Also make everything visible, because presumably the caller gave a field name because they want it to be seen.
|
|
539
|
+
modifier = FieldModifier(visible=True, key_field=False, editable=editable)
|
|
731
540
|
|
|
732
|
-
#
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
# Build a temporary data type for the request.
|
|
742
|
-
if isinstance(fields, DataTypeLayoutIdentifier):
|
|
743
|
-
temp_dt = self.__temp_dt_from_layout(data_type, fields, default_modifier, field_modifiers)
|
|
744
|
-
else:
|
|
745
|
-
temp_dt = self.__temp_dt_from_field_names(data_type, fields, None, default_modifier, field_modifiers)
|
|
746
|
-
temp_dt.record_image_assignable = bool(image_data)
|
|
747
|
-
|
|
748
|
-
# Send the request to the user.
|
|
749
|
-
request = TableEntryDialogRequest(title, msg, temp_dt, field_map_list,
|
|
541
|
+
# Build the form using only those fields that are desired.
|
|
542
|
+
builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
|
|
543
|
+
for field_name in fields:
|
|
544
|
+
field_def = field_defs.get(field_name)
|
|
545
|
+
if field_def is None:
|
|
546
|
+
raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
|
|
547
|
+
builder.add_field(modifier.modify_field(field_def))
|
|
548
|
+
|
|
549
|
+
request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), field_map_list,
|
|
750
550
|
record_image_data_list=image_data, group_by_field=group_by,
|
|
751
551
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
752
|
-
|
|
552
|
+
try:
|
|
553
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
554
|
+
response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
|
|
555
|
+
except ReadTimeout:
|
|
556
|
+
raise SapioDialogTimeoutException()
|
|
557
|
+
finally:
|
|
558
|
+
self.user.timeout_seconds = self._original_timeout
|
|
559
|
+
if response is None:
|
|
560
|
+
raise SapioUserCancelledException()
|
|
753
561
|
return response
|
|
754
562
|
|
|
755
|
-
# FR-47314: Create record form and table dialogs for updating or creating records.
|
|
756
|
-
def set_record_table_dialog(self,
|
|
757
|
-
title: str,
|
|
758
|
-
msg: str,
|
|
759
|
-
fields: Iterable[FieldValue] | DataTypeLayoutIdentifier,
|
|
760
|
-
records: Iterable[SapioRecord],
|
|
761
|
-
*,
|
|
762
|
-
default_modifier: FieldModifier | None = None,
|
|
763
|
-
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
|
|
764
|
-
group_by: FieldIdentifier | None = None,
|
|
765
|
-
image_data: Iterable[bytes] | None = None):
|
|
766
|
-
"""
|
|
767
|
-
Create a table dialog where the user may input data into the fields of the table. The table is constructed from
|
|
768
|
-
a given list of records of a singular type. After the user submits this dialog, the values that the user
|
|
769
|
-
provided are used to update the provided records.
|
|
770
|
-
|
|
771
|
-
Makes webservice calls to get the data type definition and fields of the given records if they weren't
|
|
772
|
-
previously cached.
|
|
773
|
-
|
|
774
|
-
:param title: The title of the dialog.
|
|
775
|
-
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
776
|
-
:param fields: The names of the fields to display as columns in the table. These names must match field names on
|
|
777
|
-
the data type of the provided record. Provided field names may also be extension fields of the form
|
|
778
|
-
[Extension Data Type Name].[Data Field Name]. This parameter may also be an identifier for a data type
|
|
779
|
-
layout from the data type of the provided records. If None, then the layout assigned to the current user's
|
|
780
|
-
group for this data type will be used.
|
|
781
|
-
:param records: The records to display as rows in the table and update the values of.
|
|
782
|
-
:param default_modifier: A default field modifier that will be applied to the given fields. This can be used to
|
|
783
|
-
make field definitions from the system behave differently than their system values. If this value is None,
|
|
784
|
-
then a default field modifier is created that causes all specified fields to be both visible and not key
|
|
785
|
-
fields. (Key fields get displayed first before any non-key fields in tables, so the key field setting is
|
|
786
|
-
disabled by default in order to have the columns in the table respect the order of the fields as they are
|
|
787
|
-
provided to this function.)
|
|
788
|
-
:param field_modifiers: A mapping of data field name to field modifier for changes that should be applied to
|
|
789
|
-
the matching field. If a data field name is not present in the provided dict, or the provided dictionary is
|
|
790
|
-
None, then the default modifier will be used.
|
|
791
|
-
:param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
|
|
792
|
-
The user may remove this grouping if they want to.
|
|
793
|
-
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
794
|
-
the image data list corresponds to the element at the same index in the records list.
|
|
795
|
-
"""
|
|
796
|
-
results: list[FieldMap] = self.record_table_dialog(title, msg, fields, records,
|
|
797
|
-
default_modifier=default_modifier,
|
|
798
|
-
field_modifiers=field_modifiers,
|
|
799
|
-
group_by=group_by, image_data=image_data)
|
|
800
|
-
records_by_id: dict[int, SapioRecord] = self.rec_handler.map_by_id(records)
|
|
801
|
-
for result in results:
|
|
802
|
-
records_by_id[result["RecordId"]].set_field_values(result)
|
|
803
|
-
|
|
804
|
-
def create_record_table_dialog(self,
|
|
805
|
-
title: str,
|
|
806
|
-
msg: str,
|
|
807
|
-
fields: Iterable[FieldValue] | DataTypeLayoutIdentifier,
|
|
808
|
-
wrapper_type: type[WrappedType] | str,
|
|
809
|
-
count: int | tuple[int, int],
|
|
810
|
-
*,
|
|
811
|
-
default_modifier: FieldModifier | None = None,
|
|
812
|
-
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
|
|
813
|
-
group_by: FieldIdentifier | None = None,
|
|
814
|
-
image_data: Iterable[bytes] | None = None,
|
|
815
|
-
require_input: bool = False,
|
|
816
|
-
repeat_message: str | None = "Please provide a value to continue.") \
|
|
817
|
-
-> list[WrappedType] | list[PyRecordModel]:
|
|
818
|
-
"""
|
|
819
|
-
Create a table dialog where the user may input data into the fields of the table. The table is constructed from
|
|
820
|
-
a list of records that are created using the given record model wrapper. After the user submits this dialog,
|
|
821
|
-
the values that the user provided are used to update the created records.
|
|
822
|
-
|
|
823
|
-
Makes webservice calls to get the data type definition and fields of the given records if they weren't
|
|
824
|
-
previously cached.
|
|
825
|
-
|
|
826
|
-
:param title: The title of the dialog.
|
|
827
|
-
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
828
|
-
:param fields: The names of the fields to display as columns in the table. These names must match field names on
|
|
829
|
-
the data type of the provided wrapper. Provided field names may also be extension fields of the form
|
|
830
|
-
[Extension Data Type Name].[Data Field Name]. This parameter may also be an identifier for a data type
|
|
831
|
-
layout from the data type of the provided records. If None, then the layout assigned to the current user's
|
|
832
|
-
group for this data type will be used.
|
|
833
|
-
:param wrapper_type: The record model wrapper or data type name of the records to be created and updated. If
|
|
834
|
-
a data type name is provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
835
|
-
:param count: The number of records to create. If provided as a tuple of two integers, the user will first be
|
|
836
|
-
prompted to select an integer between the two values in the tuple.
|
|
837
|
-
:param default_modifier: A default field modifier that will be applied to the given fields. This can be used to
|
|
838
|
-
make field definitions from the system behave differently than their system values. If this value is None,
|
|
839
|
-
then a default field modifier is created that causes all specified fields to be both visible and not key
|
|
840
|
-
fields. (Key fields get displayed first before any non-key fields in tables, so the key field setting is
|
|
841
|
-
disabled by default in order to have the columns in the table respect the order of the fields as they are
|
|
842
|
-
provided to this function.)
|
|
843
|
-
:param field_modifiers: A mapping of data field name to field modifier for changes that should be applied to
|
|
844
|
-
the matching field. If a data field name is not present in the provided dict, or the provided dictionary is
|
|
845
|
-
None, then the default modifier will be used.
|
|
846
|
-
:param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
|
|
847
|
-
The user may remove this grouping if they want to.
|
|
848
|
-
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
849
|
-
the image data list corresponds to the element at the same index in the records list.
|
|
850
|
-
:param require_input: If true and the user is prompted to input the number of records to create, the request
|
|
851
|
-
will be re-sent if the user submits the dialog without making a selection.
|
|
852
|
-
:param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
|
|
853
|
-
as toaster text if the record count dialog is repeated.
|
|
854
|
-
:return: A list of the newly created records.
|
|
855
|
-
"""
|
|
856
|
-
count: int = self.__prompt_for_count(count, wrapper_type, require_input, repeat_message)
|
|
857
|
-
if count <= 0:
|
|
858
|
-
return []
|
|
859
|
-
records: list[WrappedType] | list[PyRecordModel] = self.rec_handler.add_models(wrapper_type, count)
|
|
860
|
-
self.set_record_table_dialog(title, msg, fields, records,
|
|
861
|
-
default_modifier=default_modifier, field_modifiers=field_modifiers,
|
|
862
|
-
group_by=group_by, image_data=image_data)
|
|
863
|
-
return records
|
|
864
|
-
|
|
865
|
-
# FR-47314: Create record dialogs that adapt to become a form or table based on the size of the input.
|
|
866
|
-
def record_adaptive_dialog(self,
|
|
867
|
-
title: str,
|
|
868
|
-
msg: str,
|
|
869
|
-
fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
|
|
870
|
-
records: Collection[SapioRecord],
|
|
871
|
-
*,
|
|
872
|
-
default_modifier: FieldModifier | None = None,
|
|
873
|
-
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
|
|
874
|
-
column_positions: dict[str, tuple[int, int]] | None = None,
|
|
875
|
-
group_by: FieldIdentifier | None = None,
|
|
876
|
-
image_data: Iterable[bytes] | None = None) -> list[FieldMap]:
|
|
877
|
-
"""
|
|
878
|
-
Create a dialog where the user may input data into the specified fields. The dialog is constructed from
|
|
879
|
-
a given list of records of a singular type.
|
|
880
|
-
|
|
881
|
-
The dialog created will adapt to the number of records. If there is only one record then a form dialog will be
|
|
882
|
-
created. Otherwise, a table dialog is created.
|
|
883
|
-
|
|
884
|
-
Makes webservice calls to get the data type definition and fields of the given records if they weren't
|
|
885
|
-
previously cached.
|
|
886
|
-
|
|
887
|
-
:param title: The title of the dialog.
|
|
888
|
-
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
889
|
-
:param fields: The names of the fields to display in the dialog. These names must match field names on
|
|
890
|
-
the data type of the provided record. Provided field names may also be extension fields of the form
|
|
891
|
-
[Extension Data Type Name].[Data Field Name]. This parameter may also be an identifier for a data type
|
|
892
|
-
layout from the data type of the provided records. If None, then the layout assigned to the current user's
|
|
893
|
-
group for this data type will be used.
|
|
894
|
-
:param records: The records to display in the dialog.
|
|
895
|
-
:param default_modifier: A default field modifier that will be applied to the given fields. This can be used to
|
|
896
|
-
make field definitions from the system behave differently than their system values. If this value is None,
|
|
897
|
-
then a default field modifier is created that causes all specified fields to be both visible and not key
|
|
898
|
-
fields. (Key fields get displayed first before any non-key fields in tables, so the key field setting is
|
|
899
|
-
disabled by default in order to have the columns in the table respect the order of the fields as they are
|
|
900
|
-
provided to this function.)
|
|
901
|
-
:param field_modifiers: A mapping of data field name to field modifier for changes that should be applied to
|
|
902
|
-
the matching field. If a data field name is not present in the provided dict, or the provided dictionary is
|
|
903
|
-
None, then the default modifier will be used.
|
|
904
|
-
:param column_positions: If a tuple is provided for a field name, alters that field's column position and column
|
|
905
|
-
span. (Field order is still determined by the fields list.) Has no effect if the fields parameter provides
|
|
906
|
-
a data type layout. Only used if the adaptive dialog becomes a form.
|
|
907
|
-
:param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
|
|
908
|
-
The user may remove this grouping if they want to. Only used if the adaptive dialog becomes a table.
|
|
909
|
-
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
910
|
-
the image data list corresponds to the element at the same index in the records list. Only used if the
|
|
911
|
-
adaptive dialog becomes a table.
|
|
912
|
-
:return: A list of dictionaries mapping the data field names of the given field definitions to the response
|
|
913
|
-
value from the user for that field for each row. Even if a form was displayed, the field values will still
|
|
914
|
-
be returned in a list.
|
|
915
|
-
"""
|
|
916
|
-
count: int = len(records)
|
|
917
|
-
if not count:
|
|
918
|
-
raise SapioException("No records provided.")
|
|
919
|
-
if count == 1:
|
|
920
|
-
return [self.record_form_dialog(title, msg, fields, list(records)[0], column_positions,
|
|
921
|
-
default_modifier=default_modifier, field_modifiers=field_modifiers)]
|
|
922
|
-
return self.record_table_dialog(title, msg, fields, records,
|
|
923
|
-
default_modifier=default_modifier, field_modifiers=field_modifiers,
|
|
924
|
-
group_by=group_by, image_data=image_data)
|
|
925
|
-
|
|
926
|
-
def set_record_adaptive_dialog(self,
|
|
927
|
-
title: str,
|
|
928
|
-
msg: str,
|
|
929
|
-
fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
|
|
930
|
-
records: Collection[SapioRecord],
|
|
931
|
-
*,
|
|
932
|
-
default_modifier: FieldModifier | None = None,
|
|
933
|
-
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
|
|
934
|
-
column_positions: dict[str, tuple[int, int]] | None = None,
|
|
935
|
-
group_by: FieldIdentifier | None = None,
|
|
936
|
-
image_data: Iterable[bytes] | None = None) -> None:
|
|
937
|
-
"""
|
|
938
|
-
Create a dialog where the user may input data into the fields of the dialog. The dialog is constructed from
|
|
939
|
-
a given list of records of a singular type. After the user submits this dialog, the values that the user
|
|
940
|
-
provided are used to update the provided records.
|
|
941
|
-
|
|
942
|
-
The dialog created will adapt to the number of records. If there is only one record then a form dialog will be
|
|
943
|
-
created. Otherwise, a table dialog is created.
|
|
944
|
-
|
|
945
|
-
Makes webservice calls to get the data type definition and fields of the given records if they weren't
|
|
946
|
-
previously cached.
|
|
947
|
-
|
|
948
|
-
:param title: The title of the dialog.
|
|
949
|
-
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
950
|
-
:param fields: The names of the fields to display in the dialog. These names must match field names on
|
|
951
|
-
the data type of the provided record. Provided field names may also be extension fields of the form
|
|
952
|
-
[Extension Data Type Name].[Data Field Name]. This parameter may also be an identifier for a data type
|
|
953
|
-
layout from the data type of the provided records. If None, then the layout assigned to the current user's
|
|
954
|
-
group for this data type will be used.
|
|
955
|
-
:param records: The records to display in the dialog and update the values of.
|
|
956
|
-
:param default_modifier: A default field modifier that will be applied to the given fields. This can be used to
|
|
957
|
-
make field definitions from the system behave differently than their system values. If this value is None,
|
|
958
|
-
then a default field modifier is created that causes all specified fields to be both visible and not key
|
|
959
|
-
fields. (Key fields get displayed first before any non-key fields in tables, so the key field setting is
|
|
960
|
-
disabled by default in order to have the columns in the table respect the order of the fields as they are
|
|
961
|
-
provided to this function.)
|
|
962
|
-
:param field_modifiers: A mapping of data field name to field modifier for changes that should be applied to
|
|
963
|
-
the matching field. If a data field name is not present in the provided dict, or the provided dictionary is
|
|
964
|
-
None, then the default modifier will be used.
|
|
965
|
-
:param column_positions: If a tuple is provided for a field name, alters that field's column position and column
|
|
966
|
-
span. (Field order is still determined by the fields list.) Has no effect if the fields parameter provides
|
|
967
|
-
a data type layout. Only used if the adaptive dialog becomes a form.
|
|
968
|
-
:param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
|
|
969
|
-
The user may remove this grouping if they want to. Only used if the adaptive dialog becomes a table.
|
|
970
|
-
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
971
|
-
the image data list corresponds to the element at the same index in the records list. Only used if the
|
|
972
|
-
adaptive dialog becomes a table.
|
|
973
|
-
"""
|
|
974
|
-
count: int = len(records)
|
|
975
|
-
if not count:
|
|
976
|
-
raise SapioException("No records provided.")
|
|
977
|
-
if count == 1:
|
|
978
|
-
self.set_record_form_dialog(title, msg, fields, list(records)[0], column_positions,
|
|
979
|
-
default_modifier=default_modifier, field_modifiers=field_modifiers)
|
|
980
|
-
else:
|
|
981
|
-
self.set_record_table_dialog(title, msg, fields, records,
|
|
982
|
-
default_modifier=default_modifier, field_modifiers=field_modifiers,
|
|
983
|
-
group_by=group_by, image_data=image_data)
|
|
984
|
-
|
|
985
|
-
def create_record_adaptive_dialog(self,
|
|
986
|
-
title: str,
|
|
987
|
-
msg: str,
|
|
988
|
-
fields: Iterable[FieldValue] | DataTypeLayoutIdentifier,
|
|
989
|
-
wrapper_type: type[WrappedType] | str,
|
|
990
|
-
count: int | tuple[int, int],
|
|
991
|
-
*,
|
|
992
|
-
default_modifier: FieldModifier | None = None,
|
|
993
|
-
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
|
|
994
|
-
column_positions: dict[str, tuple[int, int]] | None = None,
|
|
995
|
-
group_by: FieldIdentifier | None = None,
|
|
996
|
-
image_data: Iterable[bytes] | None = None,
|
|
997
|
-
require_input: bool = False,
|
|
998
|
-
repeat_message: str | None = "Please provide a value to continue.") \
|
|
999
|
-
-> list[WrappedType]:
|
|
1000
|
-
"""
|
|
1001
|
-
Create a dialog where the user may input data into the specified fields. The dialog is constructed from
|
|
1002
|
-
a list of records that are created using the given record model wrapper. After the user submits this dialog,
|
|
1003
|
-
the values that the user provided are used to update the created records.
|
|
1004
|
-
|
|
1005
|
-
The dialog created will adapt to the number of records. If there is only one record then a form dialog will be
|
|
1006
|
-
created. Otherwise, a table dialog is created.
|
|
1007
|
-
|
|
1008
|
-
Makes webservice calls to get the data type definition and fields of the given records if they weren't
|
|
1009
|
-
previously cached.
|
|
1010
|
-
|
|
1011
|
-
:param title: The title of the dialog.
|
|
1012
|
-
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
1013
|
-
:param fields: The names of the fields to display in the dialog. These names must match field names on
|
|
1014
|
-
the data type of the provided wrapper. Provided field names may also be extension fields of the form
|
|
1015
|
-
[Extension Data Type Name].[Data Field Name]. This parameter may also be an identifier for a data type
|
|
1016
|
-
layout from the data type of the provided records. If None, then the layout assigned to the current user's
|
|
1017
|
-
group for this data type will be used.
|
|
1018
|
-
:param wrapper_type: The record model wrapper or data type name of the records to be created and updated. If
|
|
1019
|
-
a data type name is provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
1020
|
-
:param count: The number of records to create. If provided as a tuple of two integers, the user will first be
|
|
1021
|
-
prompted to select an integer between the two values in the tuple.
|
|
1022
|
-
:param default_modifier: A default field modifier that will be applied to the given fields. This can be used to
|
|
1023
|
-
make field definitions from the system behave differently than their system values. If this value is None,
|
|
1024
|
-
then a default field modifier is created that causes all specified fields to be both visible and not key
|
|
1025
|
-
fields. (Key fields get displayed first before any non-key fields in tables, so the key field setting is
|
|
1026
|
-
disabled by default in order to have the columns in the table respect the order of the fields as they are
|
|
1027
|
-
provided to this function.)
|
|
1028
|
-
:param field_modifiers: A mapping of data field name to field modifier for changes that should be applied to
|
|
1029
|
-
the matching field. If a data field name is not present in the provided dict, or the provided dictionary is
|
|
1030
|
-
None, then the default modifier will be used.
|
|
1031
|
-
:param column_positions: If a tuple is provided for a field name, alters that field's column position and column
|
|
1032
|
-
span. (Field order is still determined by the fields list.) Has no effect if the fields parameter provides
|
|
1033
|
-
a data type layout. Only used if the adaptive dialog becomes a form.
|
|
1034
|
-
:param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
|
|
1035
|
-
The user may remove this grouping if they want to. Only used if the adaptive dialog becomes a table.
|
|
1036
|
-
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
1037
|
-
the image data list corresponds to the element at the same index in the records list. Only used if the
|
|
1038
|
-
adaptive dialog becomes a table.
|
|
1039
|
-
:param require_input: If true and the user is prompted to input the number of records to create, the request
|
|
1040
|
-
will be re-sent if the user submits the dialog without making a selection.
|
|
1041
|
-
:param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
|
|
1042
|
-
as toaster text if the record count dialog is repeated.
|
|
1043
|
-
:return: A list of the newly created records. Even if a form was displayed, the created record will still be
|
|
1044
|
-
returned in a list.
|
|
1045
|
-
"""
|
|
1046
|
-
count: int = self.__prompt_for_count(count, wrapper_type, require_input, repeat_message)
|
|
1047
|
-
if count <= 0:
|
|
1048
|
-
return []
|
|
1049
|
-
if count == 1:
|
|
1050
|
-
return [self.create_record_form_dialog(title, msg, fields, wrapper_type, column_positions,
|
|
1051
|
-
default_modifier=default_modifier, field_modifiers=field_modifiers)]
|
|
1052
|
-
return self.create_record_table_dialog(title, msg, fields, wrapper_type, count,
|
|
1053
|
-
default_modifier=default_modifier, field_modifiers=field_modifiers,
|
|
1054
|
-
group_by=group_by, image_data=image_data)
|
|
1055
|
-
|
|
1056
563
|
def multi_type_table_dialog(self,
|
|
1057
564
|
title: str,
|
|
1058
565
|
msg: str,
|
|
1059
|
-
fields:
|
|
1060
|
-
row_contents:
|
|
566
|
+
fields: list[(str, str) | AbstractVeloxFieldDefinition],
|
|
567
|
+
row_contents: list[list[SapioRecord | FieldMap]],
|
|
1061
568
|
*,
|
|
1062
569
|
default_modifier: FieldModifier | None = None,
|
|
1063
|
-
field_modifiers: dict[
|
|
1064
|
-
data_type:
|
|
570
|
+
field_modifiers: dict[str, FieldModifier] | None = None,
|
|
571
|
+
data_type: str = "Default",
|
|
1065
572
|
display_name: str | None = None,
|
|
1066
573
|
plural_display_name: str | None = None) -> list[FieldMap]:
|
|
1067
574
|
"""
|
|
@@ -1074,7 +581,7 @@ class CallbackUtil:
|
|
|
1074
581
|
previously cached.
|
|
1075
582
|
|
|
1076
583
|
:param title: The title of the dialog.
|
|
1077
|
-
:param msg: The message to display in the dialog.
|
|
584
|
+
:param msg: The message to display in the dialog.
|
|
1078
585
|
:param fields: A list of objects representing the fields in the table. This could either be a two-element tuple
|
|
1079
586
|
where the first element is a data type name and the second is a field name, or it could be a field
|
|
1080
587
|
definition. If it is the former, a query will be made to find the field definition matching tht data type.
|
|
@@ -1120,31 +627,23 @@ class CallbackUtil:
|
|
|
1120
627
|
:return: A list of dictionaries mapping the data field names of the given field definitions to the response
|
|
1121
628
|
value from the user for that field for each row.
|
|
1122
629
|
"""
|
|
1123
|
-
if not row_contents:
|
|
1124
|
-
raise SapioException("No values provided.")
|
|
1125
|
-
|
|
1126
630
|
# Set the default modifier to make all fields visible and not key if no default was provided.
|
|
1127
631
|
if default_modifier is None:
|
|
1128
632
|
default_modifier = FieldModifier(visible=True, key_field=False)
|
|
1129
633
|
# To make things simpler, treat null field modifiers as an empty dict.
|
|
1130
634
|
if field_modifiers is None:
|
|
1131
635
|
field_modifiers = {}
|
|
1132
|
-
else:
|
|
1133
|
-
field_modifiers: dict[str, FieldModifier] = AliasUtil.to_data_field_names_dict(field_modifiers)
|
|
1134
636
|
|
|
1135
637
|
# Construct the final fields list from the possible field objects.
|
|
1136
638
|
final_fields: list[AbstractVeloxFieldDefinition] = []
|
|
1137
639
|
# Keep track of whether any given field name appears more than once, as two fields could have the same
|
|
1138
640
|
# field name but different data types. In this case, the user should provide a field modifier or field
|
|
1139
641
|
# definition that changes one of the field names.
|
|
1140
|
-
|
|
1141
|
-
field_names: set[str] = set()
|
|
642
|
+
field_names: list[str] = []
|
|
1142
643
|
for field in fields:
|
|
1143
644
|
# Find the field definition for this field object.
|
|
1144
645
|
if isinstance(field, tuple):
|
|
1145
|
-
|
|
1146
|
-
fld: str = AliasUtil.to_data_field_name(field[1])
|
|
1147
|
-
field_def: AbstractVeloxFieldDefinition = self.__get_field_def(dt, fld)
|
|
646
|
+
field_def: AbstractVeloxFieldDefinition = self.dt_cache.get_fields_for_type(field[0]).get(field[1])
|
|
1148
647
|
elif isinstance(field, AbstractVeloxFieldDefinition):
|
|
1149
648
|
field_def: AbstractVeloxFieldDefinition = field
|
|
1150
649
|
else:
|
|
@@ -1152,21 +651,7 @@ class CallbackUtil:
|
|
|
1152
651
|
|
|
1153
652
|
# Locate the modifier for this field and store the modified field.
|
|
1154
653
|
name: str = field_def.data_field_name
|
|
1155
|
-
|
|
1156
|
-
# which field modifier to apply to each field name.
|
|
1157
|
-
duplicate: bool = name in raw_field_names
|
|
1158
|
-
if duplicate and name in field_modifiers:
|
|
1159
|
-
raise SapioException(f"The field name \"{name}\" appears more than once in the given fields while also "
|
|
1160
|
-
f"having a field_modifiers dictionary key of the same name. This function is "
|
|
1161
|
-
f"unable to distinguish which field the field modifier should be applied to. "
|
|
1162
|
-
f"Update your field_modifiers dictionary to provide keys in the form "
|
|
1163
|
-
f"[Data Type Name].[Data Field Name] for this field name.")
|
|
1164
|
-
raw_field_names.add(name)
|
|
1165
|
-
full_name = f"{field_def.data_type_name}.{name}"
|
|
1166
|
-
if full_name in field_modifiers:
|
|
1167
|
-
modifier: FieldModifier = field_modifiers.get(full_name)
|
|
1168
|
-
else:
|
|
1169
|
-
modifier: FieldModifier = field_modifiers.get(name, default_modifier)
|
|
654
|
+
modifier: FieldModifier = field_modifiers.get(name, default_modifier)
|
|
1170
655
|
field_def: AbstractVeloxFieldDefinition = modifier.modify_field(field_def)
|
|
1171
656
|
final_fields.append(field_def)
|
|
1172
657
|
|
|
@@ -1176,9 +661,9 @@ class CallbackUtil:
|
|
|
1176
661
|
if name in field_names:
|
|
1177
662
|
raise SapioException(f"The field name \"{name}\" appears more than once in the given fields. "
|
|
1178
663
|
f"If you have provided two fields with the same name but different data types, "
|
|
1179
|
-
f"consider providing a FieldModifier where prepend_data_type is true for "
|
|
1180
|
-
f"
|
|
1181
|
-
field_names.
|
|
664
|
+
f"consider providing a FieldModifier where prepend_data_type is true for one of "
|
|
665
|
+
f"the fields so that the field names will be different.")
|
|
666
|
+
field_names.append(name)
|
|
1182
667
|
|
|
1183
668
|
# Get the values for each row.
|
|
1184
669
|
values: list[dict[str, FieldValue]] = []
|
|
@@ -1193,7 +678,7 @@ class CallbackUtil:
|
|
|
1193
678
|
if rec is None:
|
|
1194
679
|
continue
|
|
1195
680
|
# Map records to their data type name. Map field maps to Default.
|
|
1196
|
-
dt: str = "Default" if isinstance(rec, dict) else
|
|
681
|
+
dt: str = "Default" if isinstance(rec, dict) else rec.data_type_name
|
|
1197
682
|
# Warn if the same data type name appears more than once.
|
|
1198
683
|
if dt in row_records:
|
|
1199
684
|
raise SapioException(f"The data type \"{dt}\" appears more than once in the given row contents.")
|
|
@@ -1205,7 +690,7 @@ class CallbackUtil:
|
|
|
1205
690
|
record: SapioRecord | FieldMap | None = row_records.get(field.data_type_name)
|
|
1206
691
|
# This could be either a record, a field map, or null. Convert any records to field maps.
|
|
1207
692
|
if not isinstance(record, dict) and record is not None:
|
|
1208
|
-
record: FieldMap | None = AliasUtil.
|
|
693
|
+
record: FieldMap | None = AliasUtil.to_field_map_lists([record])[0]
|
|
1209
694
|
|
|
1210
695
|
# Find out if this field had its data type prepended to it. If this is the case, then we need to find
|
|
1211
696
|
# the true data field name before retrieving the value from the field map.
|
|
@@ -1217,22 +702,35 @@ class CallbackUtil:
|
|
|
1217
702
|
row_values[field.data_field_name] = record.get(name) if record else None
|
|
1218
703
|
values.append(row_values)
|
|
1219
704
|
|
|
1220
|
-
|
|
1221
|
-
|
|
705
|
+
if display_name is None:
|
|
706
|
+
display_name = data_type
|
|
707
|
+
if plural_display_name is None:
|
|
708
|
+
plural_display_name = display_name + "s"
|
|
709
|
+
|
|
710
|
+
builder = FormBuilder(data_type, display_name, plural_display_name)
|
|
711
|
+
for field in final_fields:
|
|
712
|
+
builder.add_field(field)
|
|
1222
713
|
|
|
1223
|
-
|
|
1224
|
-
request = TableEntryDialogRequest(title, msg, temp_dt, values,
|
|
714
|
+
request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
|
|
1225
715
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
1226
|
-
|
|
716
|
+
try:
|
|
717
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
718
|
+
response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
|
|
719
|
+
except ReadTimeout:
|
|
720
|
+
raise SapioDialogTimeoutException()
|
|
721
|
+
finally:
|
|
722
|
+
self.user.timeout_seconds = self._original_timeout
|
|
723
|
+
if response is None:
|
|
724
|
+
raise SapioUserCancelledException()
|
|
1227
725
|
return response
|
|
1228
|
-
|
|
726
|
+
|
|
1229
727
|
def record_view_dialog(self,
|
|
1230
728
|
title: str,
|
|
1231
729
|
record: SapioRecord,
|
|
1232
|
-
layout:
|
|
730
|
+
layout: str | DataTypeLayout | None = None,
|
|
1233
731
|
minimized: bool = False,
|
|
1234
732
|
access_level: FormAccessLevel | None = None,
|
|
1235
|
-
plugin_path_list:
|
|
733
|
+
plugin_path_list: list[str] | None = None) -> None:
|
|
1236
734
|
"""
|
|
1237
735
|
Create an IDV dialog for the given record. This IDV may use an existing layout already defined in the system,
|
|
1238
736
|
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.
|
|
@@ -1253,219 +751,163 @@ class CallbackUtil:
|
|
|
1253
751
|
:param plugin_path_list: A white list of plugins that should be displayed in the dialog. This white list
|
|
1254
752
|
includes plugins that would be displayed on sub-tables/components in the layout.
|
|
1255
753
|
"""
|
|
1256
|
-
#
|
|
754
|
+
# Ensure that the given record is a DataRecord.
|
|
1257
755
|
record: DataRecord = AliasUtil.to_data_record(record)
|
|
1258
|
-
layout: DataTypeLayout | None = self.__to_layout(AliasUtil.to_data_type_name(record), layout)
|
|
1259
756
|
|
|
1260
|
-
#
|
|
757
|
+
# Get the corresponding DataTypeLayout for the provided name.
|
|
758
|
+
if isinstance(layout, str):
|
|
759
|
+
# TODO: Replace with dt_cache if the DataTypeCacheManager ever starts caching layouts.
|
|
760
|
+
dt_man = DataMgmtServer.get_data_type_manager(self.user)
|
|
761
|
+
data_type: str = record.get_data_type_name()
|
|
762
|
+
layouts: dict[str, DataTypeLayout] = {x.layout_name: x for x in dt_man.get_data_type_layout_list(data_type)}
|
|
763
|
+
layout_name: str = layout
|
|
764
|
+
layout: DataTypeLayout | None = layouts.get(layout_name)
|
|
765
|
+
# If a name was provided then the caller expects that name to exist. Throw an exception if it doesn't.
|
|
766
|
+
if not layout:
|
|
767
|
+
raise SapioException(f"The data type \"{data_type}\" does not have a layout by the name "
|
|
768
|
+
f"\"{layout_name}\" in the system.")
|
|
769
|
+
|
|
1261
770
|
request = DataRecordDialogRequest(title, record, layout, minimized, access_level, plugin_path_list,
|
|
1262
771
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
772
|
+
try:
|
|
773
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
774
|
+
response: bool = self.callback.data_record_form_view_dialog(request)
|
|
775
|
+
except ReadTimeout:
|
|
776
|
+
raise SapioDialogTimeoutException()
|
|
777
|
+
finally:
|
|
778
|
+
self.user.timeout_seconds = self._original_timeout
|
|
1266
779
|
if not response:
|
|
1267
780
|
raise SapioUserCancelledException()
|
|
1268
|
-
|
|
1269
|
-
# CR-47326: Allow the selection dialog functions to preselect rows/records in the table.
|
|
781
|
+
|
|
1270
782
|
def selection_dialog(self,
|
|
1271
783
|
msg: str,
|
|
1272
|
-
fields:
|
|
1273
|
-
values:
|
|
784
|
+
fields: list[AbstractVeloxFieldDefinition],
|
|
785
|
+
values: list[FieldMap],
|
|
1274
786
|
multi_select: bool = True,
|
|
1275
|
-
preselected_rows: Iterable[FieldMap | RecordIdentifier] | None = None,
|
|
1276
787
|
*,
|
|
1277
|
-
data_type:
|
|
788
|
+
data_type: str = "Default",
|
|
1278
789
|
display_name: str | None = None,
|
|
1279
|
-
plural_display_name: str | None = None
|
|
1280
|
-
image_data: Iterable[bytes] | None = None,
|
|
1281
|
-
require_selection: bool = False,
|
|
1282
|
-
repeat_message: str | None = "Please provide a selection to continue.") -> list[FieldMap]:
|
|
790
|
+
plural_display_name: str | None = None) -> list[FieldMap]:
|
|
1283
791
|
"""
|
|
1284
792
|
Create a selection dialog for a list of field maps for the user to choose from. Requires that the caller
|
|
1285
793
|
provide the definitions of every field in the table.
|
|
1286
|
-
The title of a selection dialog will always be "Select [plural display name]".
|
|
1287
794
|
|
|
1288
|
-
:param msg: The message to display in the dialog.
|
|
795
|
+
:param msg: The message to display in the dialog.
|
|
1289
796
|
:param fields: The definitions of the fields to display as table columns. Fields will be displayed in the order
|
|
1290
797
|
they are provided in this list.
|
|
1291
798
|
:param values: The values to set for each row of the table.
|
|
1292
799
|
:param multi_select: Whether the user is able to select multiple rows from the list.
|
|
1293
|
-
:param preselected_rows: The rows that should be selected in the dialog when it is initially
|
|
1294
|
-
displayed to the user. The user will be allowed to deselect these records if they so wish. If preselected
|
|
1295
|
-
rows are provided, the dialog will automatically allow multi-selection of records. Note that in order for
|
|
1296
|
-
preselected rows to be identified, they MUST contain a "RecordId" field with a numeric value that is unique
|
|
1297
|
-
across all provided values.
|
|
1298
800
|
:param data_type: The data type name for the temporary data type that will be created for this table.
|
|
1299
801
|
:param display_name: The display name for the temporary data type. If not provided, defaults to the data type
|
|
1300
802
|
name.
|
|
1301
803
|
:param plural_display_name: The plural display name for the temporary data type. If not provided, defaults to
|
|
1302
804
|
the display name + "s".
|
|
1303
|
-
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
1304
|
-
the image data list corresponds to the element at the same index in the values list.
|
|
1305
|
-
:param require_selection: If true, the request will be re-sent if the user submits the dialog without making
|
|
1306
|
-
a selection.
|
|
1307
|
-
:param repeat_message: If require_selection is true and a repeat_message is provided, then that message appears
|
|
1308
|
-
as toaster text if the dialog is repeated.
|
|
1309
805
|
:return: A list of field maps corresponding to the chosen input field maps.
|
|
1310
806
|
"""
|
|
1311
|
-
if
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
# Confirm that the provided field maps are validly configured to allow the use of preselected rows.
|
|
1316
|
-
encountered_ids: set[int] = set()
|
|
1317
|
-
for row in values:
|
|
1318
|
-
if "RecordId" not in row or row["RecordId"] is None:
|
|
1319
|
-
raise SapioException("When using preselected_rows, the provided field map values must have a "
|
|
1320
|
-
"RecordId field.")
|
|
1321
|
-
row_id: int = row["RecordId"]
|
|
1322
|
-
if row_id in encountered_ids:
|
|
1323
|
-
raise SapioException(f"Not all RecordId values in the provided field maps are unique. "
|
|
1324
|
-
f"{row_id} was encountered more than once.")
|
|
1325
|
-
encountered_ids.add(row_id)
|
|
1326
|
-
|
|
1327
|
-
# Convert the preselected rows to a list of integers.
|
|
1328
|
-
new_list: list[int] = []
|
|
1329
|
-
for value in preselected_rows:
|
|
1330
|
-
if isinstance(value, dict):
|
|
1331
|
-
new_list.append(value["RecordId"])
|
|
1332
|
-
else:
|
|
1333
|
-
new_list.append(AliasUtil.to_record_id(value))
|
|
1334
|
-
preselected_rows: list[int] = new_list
|
|
1335
|
-
|
|
1336
|
-
# Add a RecordId definition to the fields if one is not already present. This is necessary for the
|
|
1337
|
-
# pre-selected records parameter to function.
|
|
1338
|
-
fields = list(fields)
|
|
1339
|
-
if "RecordId" not in [x.data_field_name for x in fields]:
|
|
1340
|
-
builder = FieldBuilder(data_type)
|
|
1341
|
-
fields.append(builder.long_field("RecordId", abstract_info=AnyFieldInfo(visible=False)))
|
|
1342
|
-
|
|
1343
|
-
# Build a temporary data type for the request.
|
|
1344
|
-
temp_dt = self.__temp_dt_from_field_defs(data_type, display_name, plural_display_name, fields, None)
|
|
1345
|
-
temp_dt.record_image_assignable = bool(image_data)
|
|
1346
|
-
|
|
1347
|
-
# Send the request to the user.
|
|
1348
|
-
request = TempTableSelectionRequest(temp_dt, msg, list(values), image_data, preselected_rows, multi_select)
|
|
1349
|
-
# If require_selection is true, repeat the request if the user didn't make a selection.
|
|
1350
|
-
while True:
|
|
1351
|
-
response: list[FieldMap] = self.__handle_dialog_request(request,
|
|
1352
|
-
self.callback.show_temp_table_selection_dialog)
|
|
1353
|
-
if not require_selection or response:
|
|
1354
|
-
break
|
|
1355
|
-
if repeat_message:
|
|
1356
|
-
self.toaster_popup(repeat_message, popup_type=PopupType.Warning)
|
|
1357
|
-
return response
|
|
807
|
+
if display_name is None:
|
|
808
|
+
display_name = data_type
|
|
809
|
+
if plural_display_name is None:
|
|
810
|
+
plural_display_name = display_name + "s"
|
|
1358
811
|
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
812
|
+
builder = FormBuilder(data_type, display_name, plural_display_name)
|
|
813
|
+
for field in fields:
|
|
814
|
+
builder.add_field(field)
|
|
815
|
+
|
|
816
|
+
request = TempTableSelectionRequest(builder.get_temporary_data_type(), msg, values,
|
|
817
|
+
multi_select=multi_select)
|
|
818
|
+
try:
|
|
819
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
820
|
+
response: list[FieldMap] | None = self.callback.show_temp_table_selection_dialog(request)
|
|
821
|
+
except ReadTimeout:
|
|
822
|
+
raise SapioDialogTimeoutException()
|
|
823
|
+
finally:
|
|
824
|
+
self.user.timeout_seconds = self._original_timeout
|
|
825
|
+
if response is None:
|
|
826
|
+
raise SapioUserCancelledException()
|
|
827
|
+
return response
|
|
828
|
+
|
|
829
|
+
def record_selection_dialog(self, msg: str, fields: list[str], records: list[SapioRecord],
|
|
830
|
+
multi_select: bool = True) -> list[SapioRecord]:
|
|
1370
831
|
"""
|
|
1371
832
|
Create a record selection dialog for a list of records for the user to choose from. Provided field names must
|
|
1372
833
|
match fields on the definition of the data type of the given records.
|
|
1373
|
-
The title of a selection dialog will always be "Select [plural display name]".
|
|
1374
834
|
|
|
1375
835
|
Makes webservice calls to get the data type definition and fields of the given records if they weren't
|
|
1376
836
|
previously cached.
|
|
1377
837
|
|
|
1378
|
-
:param msg: The message to display in the dialog.
|
|
838
|
+
:param msg: The message to display in the dialog.
|
|
1379
839
|
:param fields: The names of the fields to display as columns in the table. Fields will be displayed in the order
|
|
1380
|
-
they are provided in this list.
|
|
1381
|
-
data type of the provided records. If None, then the layout assigned to the current user's group for this
|
|
1382
|
-
data type will be used.
|
|
840
|
+
they are provided in this list.
|
|
1383
841
|
:param records: The records to display as rows in the table.
|
|
1384
842
|
:param multi_select: Whether the user is able to select multiple records from the list.
|
|
1385
|
-
:param preselected_records: The records that should be selected in the dialog when it is initially
|
|
1386
|
-
displayed to the user. The user will be allowed to deselect these records if they so wish. If preselected
|
|
1387
|
-
record IDs are provided, the dialog will automatically allow multi-selection of records.
|
|
1388
|
-
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
1389
|
-
the image data list corresponds to the element at the same index in the records list.
|
|
1390
|
-
:param require_selection: If true, the request will be re-sent if the user submits the dialog without making
|
|
1391
|
-
a selection.
|
|
1392
|
-
:param repeat_message: If require_selection is true and a repeat_message is provided, then that message appears
|
|
1393
|
-
as toaster text if the dialog is repeated.
|
|
1394
843
|
:return: A list of the selected records.
|
|
1395
844
|
"""
|
|
1396
|
-
# Get the data type name and field values from the provided records.
|
|
1397
845
|
if not records:
|
|
1398
846
|
raise SapioException("No records provided.")
|
|
1399
|
-
|
|
1400
|
-
|
|
847
|
+
data_types: set[str] = {x.data_type_name for x in records}
|
|
848
|
+
if len(data_types) > 1:
|
|
849
|
+
raise SapioException("Multiple data type names encountered in records list for record table popup.")
|
|
850
|
+
data_type: str = data_types.pop()
|
|
851
|
+
# Get the field maps from the records.
|
|
852
|
+
field_map_list: list[FieldMap] = AliasUtil.to_field_map_lists(records)
|
|
853
|
+
# Put the record ID of each record in its corresponding field map so that we can map the field maps back to
|
|
854
|
+
# the records when we return them to the caller.
|
|
855
|
+
for record, field_map in zip(records, field_map_list):
|
|
856
|
+
field_map.update({"RecId": record.record_id})
|
|
857
|
+
# Get the field definitions of the data type.
|
|
858
|
+
type_def: DataTypeDefinition = self.dt_cache.get_data_type(data_type)
|
|
859
|
+
field_defs: dict[str, AbstractVeloxFieldDefinition] = self.dt_cache.get_fields_for_type(data_type)
|
|
1401
860
|
|
|
1402
861
|
# Key fields display their columns in order before all non-key fields.
|
|
1403
862
|
# Unmark key fields so that the column order is respected exactly as the caller provides it.
|
|
1404
863
|
# Also make everything visible, because presumably the caller give a field name because they want it to be seen.
|
|
1405
864
|
modifier = FieldModifier(visible=True, key_field=False)
|
|
1406
865
|
|
|
1407
|
-
# Build
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
if preselected_records:
|
|
1415
|
-
# Convert the preselected records to a list of integers.
|
|
1416
|
-
preselected_records: list[int] = AliasUtil.to_record_ids(preselected_records)
|
|
1417
|
-
# Add a RecordId definition to the fields if one is not already present. This is necessary for the
|
|
1418
|
-
# pre-selected records parameter to function.
|
|
1419
|
-
if "RecordId" not in [x.data_field_name for x in temp_dt.get_field_def_list()]:
|
|
1420
|
-
builder = FieldBuilder(data_type)
|
|
1421
|
-
temp_dt.set_field_definition(builder.long_field("RecordId", abstract_info=AnyFieldInfo(visible=False)))
|
|
1422
|
-
|
|
1423
|
-
# Send the request to the user.
|
|
1424
|
-
request = TempTableSelectionRequest(temp_dt, msg, field_map_list, image_data, preselected_records, multi_select)
|
|
1425
|
-
# If require_selection is true, repeat the request if the user didn't make a selection.
|
|
1426
|
-
while True:
|
|
1427
|
-
response: list[FieldMap] = self.__handle_dialog_request(request,
|
|
1428
|
-
self.callback.show_temp_table_selection_dialog)
|
|
1429
|
-
if not require_selection or response:
|
|
1430
|
-
break
|
|
1431
|
-
if repeat_message:
|
|
1432
|
-
self.toaster_popup(repeat_message, popup_type=PopupType.Warning)
|
|
866
|
+
# Build the form using only those fields that are desired.
|
|
867
|
+
builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
|
|
868
|
+
for field_name in fields:
|
|
869
|
+
field_def = field_defs.get(field_name)
|
|
870
|
+
if field_def is None:
|
|
871
|
+
raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
|
|
872
|
+
builder.add_field(modifier.modify_field(field_def))
|
|
1433
873
|
|
|
874
|
+
request = TempTableSelectionRequest(builder.get_temporary_data_type(), msg, field_map_list,
|
|
875
|
+
multi_select=multi_select)
|
|
876
|
+
try:
|
|
877
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
878
|
+
response: list[FieldMap] | None = self.callback.show_temp_table_selection_dialog(request)
|
|
879
|
+
except ReadTimeout:
|
|
880
|
+
raise SapioDialogTimeoutException()
|
|
881
|
+
finally:
|
|
882
|
+
self.user.timeout_seconds = self._original_timeout
|
|
883
|
+
if response is None:
|
|
884
|
+
raise SapioUserCancelledException()
|
|
1434
885
|
# Map the field maps in the response back to the record they come from, returning the chosen record instead of
|
|
1435
886
|
# the chosen field map.
|
|
1436
887
|
records_by_id: dict[int, SapioRecord] = RecordHandler.map_by_id(records)
|
|
1437
888
|
ret_list: list[SapioRecord] = []
|
|
1438
889
|
for field_map in response:
|
|
1439
|
-
ret_list.append(records_by_id.get(field_map.get("
|
|
890
|
+
ret_list.append(records_by_id.get(field_map.get("RecId")))
|
|
1440
891
|
return ret_list
|
|
1441
892
|
|
|
1442
|
-
# CR-47377: Add allow_creation and default_creation_number to cover new parameters of this request type from 24.12.
|
|
1443
893
|
def input_selection_dialog(self,
|
|
1444
|
-
wrapper_type: type[WrappedType]
|
|
894
|
+
wrapper_type: type[WrappedType],
|
|
1445
895
|
msg: str,
|
|
1446
896
|
multi_select: bool = True,
|
|
1447
897
|
only_key_fields: bool = False,
|
|
1448
|
-
search_types:
|
|
898
|
+
search_types: list[SearchType] | None = None,
|
|
1449
899
|
scan_criteria: ScanToSelectCriteria | None = None,
|
|
1450
900
|
custom_search: CustomReport | CustomReportCriteria | str | None = None,
|
|
1451
|
-
preselected_records:
|
|
1452
|
-
record_blacklist:
|
|
1453
|
-
record_whitelist:
|
|
1454
|
-
allow_creation: bool = False,
|
|
1455
|
-
default_creation_number: int = 1,
|
|
1456
|
-
*,
|
|
1457
|
-
require_selection: bool = False,
|
|
1458
|
-
repeat_message: str | None = "Please provide a selection to continue.") \
|
|
1459
|
-
-> list[WrappedType] | list[PyRecordModel]:
|
|
901
|
+
preselected_records: list[RecordIdentifier] | None = None,
|
|
902
|
+
record_blacklist: list[RecordIdentifier] | None = None,
|
|
903
|
+
record_whitelist: list[RecordIdentifier] | None = None) -> list[WrappedType]:
|
|
1460
904
|
"""
|
|
1461
905
|
Display a table of records that exist in the system matching the given data type and filter criteria and have
|
|
1462
906
|
the user select one or more records from the table.
|
|
1463
|
-
The title of a selection dialog will always be "Select [plural display name]".
|
|
1464
907
|
|
|
1465
|
-
:param wrapper_type: The record model wrapper
|
|
1466
|
-
a data type name is provided, the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
908
|
+
:param wrapper_type: The record model wrapper for the records to display in the dialog.
|
|
1467
909
|
:param msg: The message to show near the top of the dialog, below the title. This can be used to
|
|
1468
|
-
instruct the user on what is desired from the dialog.
|
|
910
|
+
instruct the user on what is desired from the dialog.
|
|
1469
911
|
:param multi_select: Whether the user may select multiple items at once in this dialog.
|
|
1470
912
|
:param only_key_fields: Whether only key fields of the selected data type should be displayed in the table
|
|
1471
913
|
of data in the dialog. If false, allows all possible fields to be displayed as columns in the table.
|
|
@@ -1489,22 +931,10 @@ class CallbackUtil:
|
|
|
1489
931
|
:param record_blacklist: A list of records that should not be seen as possible options in the dialog.
|
|
1490
932
|
:param record_whitelist: A list of records that will be seen as possible options in the dialog. Records not in
|
|
1491
933
|
this whitelist will not be displayed if a whitelist is provided.
|
|
1492
|
-
:param allow_creation: Whether the "Create New" button will be visible to the user to create new records of the
|
|
1493
|
-
given type. The user must also have group access to be able to create the records.
|
|
1494
|
-
:param default_creation_number: If the user clicks the "Create New" button, then this is the value that will
|
|
1495
|
-
appear by default in the dialog that prompts the user to select how many new records to create. The value
|
|
1496
|
-
must be between 1 and 500, with values outside of that range being clamped to it. If this value is greater
|
|
1497
|
-
than 1, then multi-selection must be true. The data type definition of the records being created must have
|
|
1498
|
-
"Prompt for Number to Add" set to true in order to allow the user to select how many records to create, as
|
|
1499
|
-
otherwise user will only ever be able to create one record at a time.
|
|
1500
|
-
:param require_selection: If true, the request will be re-sent if the user submits the dialog without making
|
|
1501
|
-
a selection.
|
|
1502
|
-
:param repeat_message: If require_selection is true and a repeat_message is provided, then that message appears
|
|
1503
|
-
as toaster text if the dialog is repeated.
|
|
1504
934
|
:return: A list of the records selected by the user in the dialog, wrapped as record models using the provided
|
|
1505
935
|
wrapper.
|
|
1506
936
|
"""
|
|
1507
|
-
data_type: str =
|
|
937
|
+
data_type: str = wrapper_type.get_wrapper_data_type_name()
|
|
1508
938
|
|
|
1509
939
|
# Reduce the provided lists of records down to lists of record IDs.
|
|
1510
940
|
if preselected_records:
|
|
@@ -1522,64 +952,53 @@ class CallbackUtil:
|
|
|
1522
952
|
if isinstance(custom_search, str):
|
|
1523
953
|
custom_search: CustomReport = CustomReportUtil.get_system_report_criteria(self.user, custom_search)
|
|
1524
954
|
|
|
1525
|
-
# Send the request to the user.
|
|
1526
955
|
request = InputSelectionRequest(data_type, msg, search_types, only_key_fields, record_blacklist,
|
|
1527
956
|
record_whitelist, preselected_records, custom_search, scan_criteria,
|
|
1528
|
-
multi_select
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
response: list[DataRecord] = self.
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
show_comment: bool = True,
|
|
1543
|
-
additional_fields: Iterable[AbstractVeloxFieldDefinition] | None = None,
|
|
1544
|
-
*,
|
|
1545
|
-
require_authentication: bool = False) -> ESigningResponsePojo:
|
|
957
|
+
multi_select)
|
|
958
|
+
try:
|
|
959
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
960
|
+
response: list[DataRecord] | None = self.callback.show_input_selection_dialog(request)
|
|
961
|
+
except ReadTimeout:
|
|
962
|
+
raise SapioDialogTimeoutException()
|
|
963
|
+
finally:
|
|
964
|
+
self.user.timeout_seconds = self._original_timeout
|
|
965
|
+
if response is None:
|
|
966
|
+
raise SapioUserCancelledException()
|
|
967
|
+
return RecordHandler(self.user).wrap_models(response, wrapper_type)
|
|
968
|
+
|
|
969
|
+
def esign_dialog(self, title: str, msg: str, show_comment: bool = True,
|
|
970
|
+
additional_fields: list[AbstractVeloxFieldDefinition] = None) -> ESigningResponsePojo:
|
|
1546
971
|
"""
|
|
1547
972
|
Create an e-sign dialog for the user to interact with.
|
|
1548
|
-
|
|
973
|
+
|
|
1549
974
|
:param title: The title of the dialog.
|
|
1550
|
-
:param msg: The message to display in the dialog.
|
|
975
|
+
:param msg: The message to display in the dialog.
|
|
1551
976
|
:param show_comment: Whether the "Meaning of Action" field should appear in the e-sign dialog. If true, the
|
|
1552
977
|
user is required to provide an action.
|
|
1553
978
|
:param additional_fields: Field definitions for additional fields to display in the dialog, for if there is
|
|
1554
979
|
other information you wish to gather from the user alongside the e-sign.
|
|
1555
|
-
:param require_authentication: If true, the request will be re-sent if the user submits the dialog with invalid
|
|
1556
|
-
credentials.
|
|
1557
980
|
:return: An e-sign response object containing information about the e-sign attempt.
|
|
1558
981
|
"""
|
|
1559
|
-
# Construct a temporary data type if any additional fields are provided.
|
|
1560
982
|
temp_dt = None
|
|
1561
983
|
if additional_fields:
|
|
1562
984
|
builder = FormBuilder()
|
|
1563
985
|
for field in additional_fields:
|
|
1564
986
|
builder.add_field(field)
|
|
1565
987
|
temp_dt = builder.get_temporary_data_type()
|
|
1566
|
-
|
|
1567
|
-
# Send the request to the user.
|
|
1568
988
|
request = ESigningRequestPojo(title, msg, show_comment, temp_dt,
|
|
1569
989
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
response: ESigningResponsePojo = self.
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
self.
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
popup_type=PopupType.Error)
|
|
990
|
+
try:
|
|
991
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
992
|
+
response: ESigningResponsePojo | None = self.callback.show_esign_dialog(request)
|
|
993
|
+
except ReadTimeout:
|
|
994
|
+
raise SapioDialogTimeoutException()
|
|
995
|
+
finally:
|
|
996
|
+
self.user.timeout_seconds = self._original_timeout
|
|
997
|
+
if response is None:
|
|
998
|
+
raise SapioUserCancelledException()
|
|
1580
999
|
return response
|
|
1581
1000
|
|
|
1582
|
-
def request_file(self, title: str, exts:
|
|
1001
|
+
def request_file(self, title: str, exts: list[str] | None = None,
|
|
1583
1002
|
show_image_editor: bool = False, show_camera_button: bool = False) -> tuple[str, bytes]:
|
|
1584
1003
|
"""
|
|
1585
1004
|
Request a single file from the user.
|
|
@@ -1604,15 +1023,21 @@ class CallbackUtil:
|
|
|
1604
1023
|
def do_consume(chunk: bytes) -> None:
|
|
1605
1024
|
return sink.consume_data(chunk, io_obj)
|
|
1606
1025
|
|
|
1607
|
-
# Send the request to the user.
|
|
1608
1026
|
request = FilePromptRequest(title, show_image_editor, ",".join(exts), show_camera_button)
|
|
1609
|
-
|
|
1027
|
+
try:
|
|
1028
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
1029
|
+
file_path: str | None = self.callback.show_file_dialog(request, do_consume)
|
|
1030
|
+
except ReadTimeout:
|
|
1031
|
+
raise SapioDialogTimeoutException()
|
|
1032
|
+
finally:
|
|
1033
|
+
self.user.timeout_seconds = self._original_timeout
|
|
1034
|
+
if file_path is None:
|
|
1035
|
+
raise SapioUserCancelledException()
|
|
1610
1036
|
|
|
1611
|
-
# Verify that each of the file given matches the expected extension(s).
|
|
1612
1037
|
self.__verify_file(file_path, sink.data, exts)
|
|
1613
1038
|
return file_path, sink.data
|
|
1614
1039
|
|
|
1615
|
-
def request_files(self, title: str, exts:
|
|
1040
|
+
def request_files(self, title: str, exts: list[str] | None = None,
|
|
1616
1041
|
show_image_editor: bool = False, show_camera_button: bool = False) -> dict[str, bytes]:
|
|
1617
1042
|
"""
|
|
1618
1043
|
Request multiple files from the user.
|
|
@@ -1629,11 +1054,17 @@ class CallbackUtil:
|
|
|
1629
1054
|
if exts is None:
|
|
1630
1055
|
exts: list[str] = []
|
|
1631
1056
|
|
|
1632
|
-
# Send the request to the user.
|
|
1633
1057
|
request = MultiFilePromptRequest(title, show_image_editor, ",".join(exts), show_camera_button)
|
|
1634
|
-
|
|
1058
|
+
try:
|
|
1059
|
+
self.user.timeout_seconds = self.timeout_seconds
|
|
1060
|
+
file_paths: list[str] | None = self.callback.show_multi_file_dialog(request)
|
|
1061
|
+
except ReadTimeout:
|
|
1062
|
+
raise SapioDialogTimeoutException()
|
|
1063
|
+
finally:
|
|
1064
|
+
self.user.timeout_seconds = self._original_timeout
|
|
1065
|
+
if not file_paths:
|
|
1066
|
+
raise SapioUserCancelledException()
|
|
1635
1067
|
|
|
1636
|
-
# Verify that each of the files given match the expected extension(s).
|
|
1637
1068
|
ret_dict: dict[str, bytes] = {}
|
|
1638
1069
|
for file_path in file_paths:
|
|
1639
1070
|
sink = InMemoryRecordDataSink(self.user)
|
|
@@ -1644,7 +1075,7 @@ class CallbackUtil:
|
|
|
1644
1075
|
return ret_dict
|
|
1645
1076
|
|
|
1646
1077
|
@staticmethod
|
|
1647
|
-
def __verify_file(file_path: str, file_bytes: bytes, allowed_extensions:
|
|
1078
|
+
def __verify_file(file_path: str, file_bytes: bytes, allowed_extensions: list[str]) -> None:
|
|
1648
1079
|
"""
|
|
1649
1080
|
Verify that the provided file was read (i.e. the file path and file bytes aren't None or empty) and that it
|
|
1650
1081
|
has the correct file extension. Raises a user error exception if something about the file is incorrect.
|
|
@@ -1655,7 +1086,7 @@ class CallbackUtil:
|
|
|
1655
1086
|
"""
|
|
1656
1087
|
if file_path is None or len(file_path) == 0 or file_bytes is None or len(file_bytes) == 0:
|
|
1657
1088
|
raise SapioUserErrorException("Empty file provided or file unable to be read.")
|
|
1658
|
-
if allowed_extensions:
|
|
1089
|
+
if len(allowed_extensions) != 0:
|
|
1659
1090
|
matches: bool = False
|
|
1660
1091
|
for ext in allowed_extensions:
|
|
1661
1092
|
if file_path.endswith("." + ext.lstrip(".")):
|
|
@@ -1672,8 +1103,8 @@ class CallbackUtil:
|
|
|
1672
1103
|
:param file_name: The name of the file.
|
|
1673
1104
|
:param file_data: The data of the file, provided as either a string or as a bytes array.
|
|
1674
1105
|
"""
|
|
1675
|
-
|
|
1676
|
-
|
|
1106
|
+
data = io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data)
|
|
1107
|
+
self.callback.send_file(file_name, False, data)
|
|
1677
1108
|
|
|
1678
1109
|
def write_zip_file(self, zip_name: str, files: dict[str, str | bytes]) -> None:
|
|
1679
1110
|
"""
|
|
@@ -1682,239 +1113,8 @@ class CallbackUtil:
|
|
|
1682
1113
|
:param zip_name: The name of the zip file.
|
|
1683
1114
|
:param files: A dictionary of the files to add to the zip file.
|
|
1684
1115
|
"""
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
@staticmethod
|
|
1689
|
-
def __temp_dt_from_field_defs(data_type: DataTypeIdentifier, display_name: str | None,
|
|
1690
|
-
plural_display_name: str | None, fields: Iterable[AbstractVeloxFieldDefinition],
|
|
1691
|
-
column_positions: dict[str, tuple[int, int]] | None) -> TemporaryDataType:
|
|
1692
|
-
"""
|
|
1693
|
-
Construct a Temporary Data Type definition from a provided list of field definitions for use in a callback.
|
|
1694
|
-
"""
|
|
1695
|
-
# Get the data type name as a string from the parameters, and set the display name and plural display name if
|
|
1696
|
-
# they haven't been set.
|
|
1697
|
-
data_type: str = AliasUtil.to_data_type_name(data_type)
|
|
1698
|
-
if display_name is None:
|
|
1699
|
-
display_name = data_type
|
|
1700
|
-
if plural_display_name is None:
|
|
1701
|
-
plural_display_name = display_name + "s"
|
|
1702
|
-
|
|
1703
|
-
# Key fields display their columns in order before all non-key fields.
|
|
1704
|
-
# Unmark key fields so that the column order is respected exactly as the caller provides it.
|
|
1705
|
-
modifier = FieldModifier(key_field=False)
|
|
1706
|
-
|
|
1707
|
-
builder = FormBuilder(data_type, display_name, plural_display_name)
|
|
1708
|
-
for field_def in fields:
|
|
1709
|
-
# Determine the column and span for each field in the form.
|
|
1710
|
-
# If this isn't a form dialog, then adding the column and span to the FormBuilder has no effect.
|
|
1711
|
-
field_name = field_def.data_field_name
|
|
1712
|
-
column: int = 0
|
|
1713
|
-
span: int = 4
|
|
1714
|
-
if column_positions and field_name in column_positions:
|
|
1715
|
-
position = column_positions.get(field_name)
|
|
1716
|
-
column = position[0]
|
|
1717
|
-
span = position[1]
|
|
1718
|
-
# Apply the field modifier to each key field in the form.
|
|
1719
|
-
if field_def.key_field:
|
|
1720
|
-
field_def = modifier.modify_field(field_def)
|
|
1721
|
-
builder.add_field(field_def, column, span)
|
|
1722
|
-
return builder.get_temporary_data_type()
|
|
1723
|
-
|
|
1724
|
-
def __temp_dt_from_field_names(self, data_type: str, fields: Iterable[FieldIdentifier | FieldFilterCriteria],
|
|
1725
|
-
column_positions: dict[str, tuple[int, int]] | None,
|
|
1726
|
-
default_modifier: FieldModifier, field_modifiers: dict[str, FieldModifier]) \
|
|
1727
|
-
-> TemporaryDataType:
|
|
1728
|
-
"""
|
|
1729
|
-
Construct a Temporary Data Type definition from a given data type name and list of field identifiers for that
|
|
1730
|
-
data type. Queries for the data type's definition to get the display name and plural display name, as well as
|
|
1731
|
-
the data field definitions of the data type to map the given field identifiers to field definitions. If an
|
|
1732
|
-
extension field is provided, then the extension data type's fields will be queried. Finally, applies the
|
|
1733
|
-
provided field modifiers to the field definitions to alter them from their system-set values
|
|
1734
|
-
"""
|
|
1735
|
-
# Get the definition of the data type to construct the form builder with the proper values.
|
|
1736
|
-
type_def: DataTypeDefinition = self.dt_cache.get_data_type(data_type)
|
|
1737
|
-
builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
|
|
1738
|
-
|
|
1739
|
-
# Determine if any FieldFilterCriteria were provided. If so, remove them from the fields list so that it
|
|
1740
|
-
# contains only field identifiers.
|
|
1741
|
-
fields = list(fields)
|
|
1742
|
-
filter_criteria: list[FieldFilterCriteria] = [x for x in fields if isinstance(x, FieldFilterCriteria)]
|
|
1743
|
-
for criteria in filter_criteria:
|
|
1744
|
-
fields.remove(criteria)
|
|
1745
|
-
|
|
1746
|
-
# Build the form using only those fields that are desired.
|
|
1747
|
-
field_names: list[str] = AliasUtil.to_data_field_names(fields)
|
|
1748
|
-
for field_name in field_names:
|
|
1749
|
-
field_def: AbstractVeloxFieldDefinition = self.__get_field_def(data_type, field_name)
|
|
1750
|
-
|
|
1751
|
-
# Determine the column and span for each field in the form.
|
|
1752
|
-
# If this isn't a form dialog, then adding the column and span to the FormBuilder has no effect.
|
|
1753
|
-
column: int = 0
|
|
1754
|
-
span: int = 4
|
|
1755
|
-
if column_positions and field_name in column_positions:
|
|
1756
|
-
position = column_positions.get(field_name)
|
|
1757
|
-
column = position[0]
|
|
1758
|
-
span = position[1]
|
|
1759
|
-
|
|
1760
|
-
# Apply the field modifiers to each field in the form.
|
|
1761
|
-
modifier: FieldModifier = field_modifiers.get(field_name, default_modifier)
|
|
1762
|
-
builder.add_field(modifier.modify_field(field_def), column, span)
|
|
1763
|
-
|
|
1764
|
-
# Now determine if any fields match the provided filter criteria.
|
|
1765
|
-
all_fields: dict[str, AbstractVeloxFieldDefinition] = self.dt_cache.get_fields_for_type(data_type)
|
|
1766
|
-
current_column: int = 0
|
|
1767
|
-
for criteria in filter_criteria:
|
|
1768
|
-
for field_name, field_def in all_fields.items():
|
|
1769
|
-
# Don't add fields that have already been added.
|
|
1770
|
-
if field_name in field_names or not criteria.field_matches(field_def):
|
|
1771
|
-
continue
|
|
1772
|
-
field_names.append(field_name)
|
|
1773
|
-
|
|
1774
|
-
# The caller can't know what fields are present, so the column positions dictionary can't be used.
|
|
1775
|
-
# Still come up with spans for each field to minimize wasted space.
|
|
1776
|
-
# Give boolean fields a span of 1 and HTML or multi-line string fields a span of 4.
|
|
1777
|
-
# Give all other fields a span of 2.
|
|
1778
|
-
if field_def.data_field_type == FieldType.BOOLEAN:
|
|
1779
|
-
span = 1
|
|
1780
|
-
elif (isinstance(field_def, VeloxStringFieldDefinition)
|
|
1781
|
-
and (field_def.html_editor or field_def.num_lines > 1)):
|
|
1782
|
-
span = 4
|
|
1783
|
-
else:
|
|
1784
|
-
span = 2
|
|
1785
|
-
# Wrap the column position if necessary.
|
|
1786
|
-
if current_column + span > 4:
|
|
1787
|
-
current_column = 0
|
|
1788
|
-
|
|
1789
|
-
# Apply the field modifiers to each field in the form.
|
|
1790
|
-
modifier: FieldModifier = field_modifiers.get(field_name, default_modifier)
|
|
1791
|
-
builder.add_field(modifier.modify_field(field_def), current_column, span)
|
|
1792
|
-
current_column += span
|
|
1793
|
-
|
|
1794
|
-
return builder.get_temporary_data_type()
|
|
1795
|
-
|
|
1796
|
-
# CR-47309: Allow layouts to be provided in place of field names for record dialogs.
|
|
1797
|
-
def __temp_dt_from_layout(self, data_type: str, layout: DataTypeLayoutIdentifier,
|
|
1798
|
-
default_modifier: FieldModifier, field_modifiers: dict[str, FieldModifier]) \
|
|
1799
|
-
-> TemporaryDataType:
|
|
1800
|
-
"""
|
|
1801
|
-
Construct a Temporary Data Type definition from a given data type name and layout identifier.
|
|
1802
|
-
Applies the provided field modifiers to the field definitions from the layout's temp data type to alter them
|
|
1803
|
-
from their system-set values
|
|
1804
|
-
"""
|
|
1805
|
-
# Get the temp data type for the provided layout.
|
|
1806
|
-
temp_dt = self.dt_man.get_temporary_data_type(data_type, self.__to_layout_name(layout))
|
|
1807
|
-
# Apply the field modifiers to each field in the layout.
|
|
1808
|
-
for field_def in temp_dt.get_field_def_list():
|
|
1809
|
-
field_name: str = field_def.data_field_name
|
|
1810
|
-
modifier: FieldModifier = field_modifiers.get(field_name, default_modifier)
|
|
1811
|
-
temp_dt.set_field_definition(modifier.modify_field(field_def))
|
|
1812
|
-
return temp_dt
|
|
1813
|
-
|
|
1814
|
-
def __prompt_for_count(self, count: tuple[int, int] | int, wrapper_type: type[WrappedType] | str,
|
|
1815
|
-
require_input: bool, repeat_message: str) -> int:
|
|
1816
|
-
"""
|
|
1817
|
-
Given a count value, if it is a tuple representing an allowable range of values for a number of records to
|
|
1818
|
-
create, prompt the user to input the exact count to use. If the count is already a single integer, simply
|
|
1819
|
-
return that.
|
|
1820
|
-
"""
|
|
1821
|
-
if isinstance(count, tuple):
|
|
1822
|
-
if hasattr(wrapper_type, "PLURAL_DISPLAY_NAME"):
|
|
1823
|
-
plural: str = wrapper_type.PLURAL_DISPLAY_NAME
|
|
1824
|
-
else:
|
|
1825
|
-
plural: str = self.dt_cache.get_plural_display_name(AliasUtil.to_data_type_name(wrapper_type))
|
|
1826
|
-
min_val, max_val = count
|
|
1827
|
-
msg: str = f"How many {plural} should be created? ({min_val} to {max_val})"
|
|
1828
|
-
count: int = self.integer_input_dialog(f"Create {plural}", msg, "Count", min_val, min_val, max_val,
|
|
1829
|
-
require_input=require_input, repeat_message=repeat_message)
|
|
1830
|
-
return count
|
|
1831
|
-
|
|
1832
|
-
def __to_layout(self, data_type: str, layout: DataTypeLayoutIdentifier) -> DataTypeLayout | None:
|
|
1833
|
-
"""
|
|
1834
|
-
Convert a data type layout identifier to a data type layout.
|
|
1835
|
-
"""
|
|
1836
|
-
if layout is None:
|
|
1837
|
-
return None
|
|
1838
|
-
if isinstance(layout, DataTypeLayout):
|
|
1839
|
-
return layout
|
|
1840
|
-
layout_name: str = layout
|
|
1841
|
-
layout: DataTypeLayout | None = self.__get_data_type_layout(data_type, layout_name)
|
|
1842
|
-
# If a name was provided then the caller expects that name to exist. Throw an exception if it doesn't.
|
|
1843
|
-
if not layout:
|
|
1844
|
-
raise SapioException(f"The data type \"{data_type}\" does not have a layout by the name "
|
|
1845
|
-
f"\"{layout_name}\" in the system.")
|
|
1846
|
-
return layout
|
|
1847
|
-
|
|
1848
|
-
@staticmethod
|
|
1849
|
-
def __to_layout_name(layout: DataTypeLayoutIdentifier) -> str | None:
|
|
1850
|
-
"""
|
|
1851
|
-
Convert a data type layout identifier to a layout name.
|
|
1852
|
-
"""
|
|
1853
|
-
if layout is None:
|
|
1854
|
-
return None
|
|
1855
|
-
if isinstance(layout, DataTypeLayout):
|
|
1856
|
-
return layout.layout_name
|
|
1857
|
-
return layout
|
|
1858
|
-
|
|
1859
|
-
def __get_data_type_layout(self, data_type: str, layout: str) -> DataTypeLayout:
|
|
1860
|
-
"""
|
|
1861
|
-
Get a data type layout from the cache given its name.
|
|
1862
|
-
"""
|
|
1863
|
-
if data_type in self.__layouts:
|
|
1864
|
-
return self.__layouts[data_type].get(layout)
|
|
1865
|
-
self.__layouts[data_type] = {x.layout_name: x for x in self.dt_man.get_data_type_layout_list(data_type)}
|
|
1866
|
-
return self.__layouts[data_type].get(layout)
|
|
1867
|
-
|
|
1868
|
-
def __get_field_def(self, data_type: str, field_name: str) -> AbstractVeloxFieldDefinition:
|
|
1869
|
-
"""
|
|
1870
|
-
Given a data type name and a data field name, return the field definition for that field on that data type.
|
|
1871
|
-
If the field name is an extension field, properly gets the field definition from the extension data type instead
|
|
1872
|
-
of the given data type and updates the extension field def to have its data field name match the given field
|
|
1873
|
-
name.
|
|
1874
|
-
"""
|
|
1875
|
-
# CR-47311: Support displaying extension fields with single-data-type record dialogs.
|
|
1876
|
-
if "." in field_name:
|
|
1877
|
-
# If there is a period in the given field name, then this is an extension field.
|
|
1878
|
-
ext_dt, ext_fld = field_name.split(".")
|
|
1879
|
-
# Locate the extension data type's field definitions.
|
|
1880
|
-
field_def = self.dt_cache.get_fields_for_type(ext_dt).get(ext_fld)
|
|
1881
|
-
if field_def is None:
|
|
1882
|
-
raise SapioException(f"No field of name \"{ext_fld}\" in field definitions of extension type \"{ext_dt}\"")
|
|
1883
|
-
# Copy the field definition and set its field name to match the extension field name so that the record
|
|
1884
|
-
# field maps properly map the field value to the field definition.
|
|
1885
|
-
field_def = copy(field_def)
|
|
1886
|
-
field_def._data_field_name = field_name
|
|
1887
|
-
else:
|
|
1888
|
-
# If there is no period in the given field name, then this is a field on the base data type.
|
|
1889
|
-
field_def = self.dt_cache.get_fields_for_type(data_type).get(field_name)
|
|
1890
|
-
if field_def is None:
|
|
1891
|
-
raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
|
|
1892
|
-
return field_def
|
|
1893
|
-
|
|
1894
|
-
def __handle_dialog_request(self, request: Any, func: Callable, **kwargs) -> Any:
|
|
1895
|
-
"""
|
|
1896
|
-
Send a client callback request to the user that creates a dialog.
|
|
1897
|
-
|
|
1898
|
-
This function handles updating the user object's request timeout to match the request timeout of this
|
|
1899
|
-
CallbackUtil for the duration of the dialog.
|
|
1900
|
-
If the dialog times out then a SapioDialogTimeoutException is thrown.
|
|
1901
|
-
If the user cancels the dialog then a SapioUserCancelledException is thrown.
|
|
1902
|
-
|
|
1903
|
-
:param request: The client callback request to send to the user.
|
|
1904
|
-
:param func: The ClientCallback function to call with the given request as input.
|
|
1905
|
-
:param kwargs: Additional keywords for the provided function call.
|
|
1906
|
-
:return: The response from the client callback, if one was received.
|
|
1907
|
-
"""
|
|
1908
|
-
try:
|
|
1909
|
-
self.user.timeout_seconds = self.timeout_seconds
|
|
1910
|
-
response: Any | None = func(request, **kwargs)
|
|
1911
|
-
except ReadTimeout:
|
|
1912
|
-
raise SapioDialogTimeoutException()
|
|
1913
|
-
finally:
|
|
1914
|
-
self.user.timeout_seconds = self._original_timeout
|
|
1915
|
-
if response is None:
|
|
1916
|
-
raise SapioUserCancelledException()
|
|
1917
|
-
return response
|
|
1116
|
+
data = io.BytesIO(FileUtil.zip_files(files))
|
|
1117
|
+
self.callback.send_file(zip_name, False, data)
|
|
1918
1118
|
|
|
1919
1119
|
|
|
1920
1120
|
class FieldModifier:
|
|
@@ -1959,99 +1159,29 @@ class FieldModifier:
|
|
|
1959
1159
|
Apply modifications to a given field.
|
|
1960
1160
|
|
|
1961
1161
|
:param field: The field to modify.
|
|
1962
|
-
:return: A copy of the input field with the modifications applied.
|
|
1162
|
+
:return: A copy of the input field with the modifications applied.
|
|
1963
1163
|
"""
|
|
1964
|
-
|
|
1164
|
+
field = copy_field(field)
|
|
1965
1165
|
if self.prepend_data_type is True:
|
|
1966
|
-
|
|
1166
|
+
field._data_field_name = field.data_type_name + "." + field.data_field_name
|
|
1967
1167
|
if self.display_name is not None:
|
|
1968
|
-
|
|
1168
|
+
field.display_name = self.display_name
|
|
1969
1169
|
if self.required is not None:
|
|
1970
|
-
|
|
1170
|
+
field.required = self.required
|
|
1971
1171
|
if self.editable is not None:
|
|
1972
|
-
|
|
1172
|
+
field.editable = self.editable
|
|
1973
1173
|
if self.visible is not None:
|
|
1974
|
-
|
|
1174
|
+
field.visible = self.visible
|
|
1975
1175
|
if self.key_field is not None:
|
|
1976
|
-
|
|
1176
|
+
field.key_field = self.key_field
|
|
1977
1177
|
if self.column_width is not None:
|
|
1978
|
-
|
|
1979
|
-
return
|
|
1178
|
+
field.default_table_column_width = self.column_width
|
|
1179
|
+
return field
|
|
1980
1180
|
|
|
1981
1181
|
|
|
1982
|
-
|
|
1983
|
-
# based on the attributes of the field definitions of the data type instead of requiring that the caller know the
|
|
1984
|
-
# names of the fields to be displayed.
|
|
1985
|
-
class FieldFilterCriteria:
|
|
1182
|
+
def copy_field(field: AbstractVeloxFieldDefinition) -> AbstractVeloxFieldDefinition:
|
|
1986
1183
|
"""
|
|
1987
|
-
|
|
1184
|
+
Create a copy of a given field definition. This is used to modify field definitions from the server for existing
|
|
1185
|
+
data types without also modifying the field definition in the cache.
|
|
1988
1186
|
"""
|
|
1989
|
-
|
|
1990
|
-
editable: bool | None
|
|
1991
|
-
key_field: bool | None
|
|
1992
|
-
identifier: bool | None
|
|
1993
|
-
system_field: bool | None
|
|
1994
|
-
field_types: Container[FieldType] | None
|
|
1995
|
-
not_field_types: Container[FieldType] | None
|
|
1996
|
-
matches_tag: str | None
|
|
1997
|
-
contains_tag: str | None
|
|
1998
|
-
regex_tag: str | re.Pattern[str] | None
|
|
1999
|
-
|
|
2000
|
-
def __init__(self, *, required: bool | None = None, editable: bool | None = None, key_field: bool | None = None,
|
|
2001
|
-
identifier: bool | None = None, system_field: bool | None = None,
|
|
2002
|
-
field_types: Container[FieldType] | None = None, not_field_types: Container[FieldType] | None = None,
|
|
2003
|
-
matches_tag: str | None = None, contains_tag: str | None = None,
|
|
2004
|
-
regex_tag: str | re.Pattern[str] | None = None):
|
|
2005
|
-
"""
|
|
2006
|
-
Values that are left as None have no effect on the filtering. A field must match all non-None values in order
|
|
2007
|
-
to count as matching this filter.
|
|
2008
|
-
|
|
2009
|
-
:param required: Whether the field is required.
|
|
2010
|
-
:param editable: Whether the field is editable.
|
|
2011
|
-
:param key_field: Whether the field is a key field.
|
|
2012
|
-
:param identifier: Whether the field is an identifier field.
|
|
2013
|
-
:param system_field: Whether the field is a system field.
|
|
2014
|
-
:param field_types: Include fields matching these types.
|
|
2015
|
-
:param not_field_types: Exclude fields matching these types.
|
|
2016
|
-
:param matches_tag: If provided, the field's tag must exactly match this value.
|
|
2017
|
-
:param contains_tag: If provided, the field's tag must contain this value.
|
|
2018
|
-
:param regex_tag: If provided, the field's tag must match this regex.
|
|
2019
|
-
"""
|
|
2020
|
-
self.required = required
|
|
2021
|
-
self.editable = editable
|
|
2022
|
-
self.key_field = key_field
|
|
2023
|
-
self.identifier = identifier
|
|
2024
|
-
self.system_field = system_field
|
|
2025
|
-
self.field_types = field_types
|
|
2026
|
-
self.not_field_types = not_field_types
|
|
2027
|
-
self.matches_tag = matches_tag
|
|
2028
|
-
self.contains_tag = contains_tag
|
|
2029
|
-
self.regex_tag = regex_tag
|
|
2030
|
-
|
|
2031
|
-
def field_matches(self, field: AbstractVeloxFieldDefinition) -> bool:
|
|
2032
|
-
"""
|
|
2033
|
-
:param field: A field definition from a data type.
|
|
2034
|
-
:return: Whether the field definition matches the filter criteria.
|
|
2035
|
-
"""
|
|
2036
|
-
ret_val: bool = True
|
|
2037
|
-
if self.required is not None:
|
|
2038
|
-
ret_val = ret_val and self.required == field.required
|
|
2039
|
-
if self.editable is not None:
|
|
2040
|
-
ret_val = ret_val and self.editable == field.editable
|
|
2041
|
-
if self.key_field is not None:
|
|
2042
|
-
ret_val = ret_val and self.key_field == field.key_field
|
|
2043
|
-
if self.identifier is not None:
|
|
2044
|
-
ret_val = ret_val and self.identifier == field.identifier
|
|
2045
|
-
if self.system_field is not None:
|
|
2046
|
-
ret_val = ret_val and self.system_field == field.system_field
|
|
2047
|
-
if self.field_types is not None:
|
|
2048
|
-
ret_val = ret_val and field.data_field_type in self.field_types
|
|
2049
|
-
if self.not_field_types is not None:
|
|
2050
|
-
ret_val = ret_val and field.data_field_type not in self.not_field_types
|
|
2051
|
-
if self.matches_tag is not None:
|
|
2052
|
-
ret_val = ret_val and field.tag is not None and self.matches_tag == field.tag
|
|
2053
|
-
if self.contains_tag is not None:
|
|
2054
|
-
ret_val = ret_val and field.tag is not None and self.contains_tag in field.tag
|
|
2055
|
-
if self.regex_tag is not None:
|
|
2056
|
-
ret_val = ret_val and field.tag is not None and bool(re.match(self.regex_tag, field.tag))
|
|
2057
|
-
return ret_val
|
|
1187
|
+
return FieldDefinitionParser.to_field_definition(field.to_json())
|