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