sapiopycommons 2025.3.6a453__py3-none-any.whl → 2025.3.10a455__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.

Files changed (33) hide show
  1. sapiopycommons/callbacks/callback_util.py +366 -1220
  2. sapiopycommons/chem/Molecules.py +2 -0
  3. sapiopycommons/datatype/data_fields.py +1 -1
  4. sapiopycommons/eln/experiment_handler.py +1 -2
  5. sapiopycommons/eln/experiment_report_util.py +7 -7
  6. sapiopycommons/files/file_bridge.py +0 -76
  7. sapiopycommons/files/file_bridge_handler.py +110 -325
  8. sapiopycommons/files/file_data_handler.py +2 -2
  9. sapiopycommons/files/file_util.py +11 -36
  10. sapiopycommons/files/file_validator.py +5 -6
  11. sapiopycommons/files/file_writer.py +1 -1
  12. sapiopycommons/flowcyto/flow_cyto.py +1 -1
  13. sapiopycommons/general/accession_service.py +1 -1
  14. sapiopycommons/general/aliases.py +28 -48
  15. sapiopycommons/general/audit_log.py +2 -2
  16. sapiopycommons/general/custom_report_util.py +1 -24
  17. sapiopycommons/general/exceptions.py +2 -41
  18. sapiopycommons/general/popup_util.py +2 -2
  19. sapiopycommons/general/sapio_links.py +4 -12
  20. sapiopycommons/multimodal/multimodal.py +0 -1
  21. sapiopycommons/processtracking/custom_workflow_handler.py +3 -3
  22. sapiopycommons/recordmodel/record_handler.py +108 -156
  23. sapiopycommons/webhook/webhook_handlers.py +55 -445
  24. {sapiopycommons-2025.3.6a453.dist-info → sapiopycommons-2025.3.10a455.dist-info}/METADATA +1 -1
  25. {sapiopycommons-2025.3.6a453.dist-info → sapiopycommons-2025.3.10a455.dist-info}/RECORD +27 -33
  26. sapiopycommons/ai/__init__.py +0 -0
  27. sapiopycommons/ai/tool_of_tools.py +0 -917
  28. sapiopycommons/customreport/auto_pagers.py +0 -278
  29. sapiopycommons/general/directive_util.py +0 -86
  30. sapiopycommons/general/html_formatter.py +0 -456
  31. sapiopycommons/samples/aliquot.py +0 -48
  32. {sapiopycommons-2025.3.6a453.dist-info → sapiopycommons-2025.3.10a455.dist-info}/WHEEL +0 -0
  33. {sapiopycommons-2025.3.6a453.dist-info → sapiopycommons-2025.3.10a455.dist-info}/licenses/LICENSE +0 -0
@@ -1,24 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import io
4
- import re
5
- import warnings
6
- from copy import copy
7
- from typing import Iterable, TypeAlias, Any, Callable
8
4
  from weakref import WeakValueDictionary
9
5
 
10
6
  from requests import ReadTimeout
11
7
  from sapiopylib.rest.ClientCallbackService import ClientCallback
12
8
  from sapiopylib.rest.DataMgmtService import DataMgmtServer
13
- from sapiopylib.rest.DataTypeService import DataTypeManager
14
9
  from sapiopylib.rest.User import SapioUser
15
10
  from sapiopylib.rest.pojo.CustomReport import CustomReport, CustomReportCriteria
16
11
  from sapiopylib.rest.pojo.DataRecord import DataRecord
17
12
  from sapiopylib.rest.pojo.datatype.DataType import DataTypeDefinition
18
13
  from sapiopylib.rest.pojo.datatype.DataTypeLayout import DataTypeLayout
19
14
  from sapiopylib.rest.pojo.datatype.FieldDefinition import AbstractVeloxFieldDefinition, VeloxStringFieldDefinition, \
20
- VeloxIntegerFieldDefinition, VeloxDoubleFieldDefinition, FieldType
21
- from sapiopylib.rest.pojo.datatype.TemporaryDataType import TemporaryDataType
15
+ VeloxIntegerFieldDefinition, VeloxDoubleFieldDefinition, FieldDefinitionParser
22
16
  from sapiopylib.rest.pojo.webhook.ClientCallbackRequest import OptionDialogRequest, ListDialogRequest, \
23
17
  FormEntryDialogRequest, InputDialogCriteria, TableEntryDialogRequest, ESigningRequestPojo, \
24
18
  DataRecordDialogRequest, InputSelectionRequest, FilePromptRequest, MultiFilePromptRequest, \
@@ -30,23 +24,18 @@ from sapiopylib.rest.utils.FormBuilder import FormBuilder
30
24
  from sapiopylib.rest.utils.recorddatasinks import InMemoryRecordDataSink
31
25
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
32
26
 
33
- from sapiopycommons.callbacks.field_builder import FieldBuilder, AnyFieldInfo
34
27
  from sapiopycommons.files.file_util import FileUtil
35
28
  from sapiopycommons.general.aliases import FieldMap, SapioRecord, AliasUtil, RecordIdentifier, FieldValue, \
36
- UserIdentifier, FieldIdentifier, DataTypeIdentifier
29
+ UserIdentifier
37
30
  from sapiopycommons.general.custom_report_util import CustomReportUtil
38
31
  from sapiopycommons.general.exceptions import SapioUserCancelledException, SapioException, SapioUserErrorException, \
39
32
  SapioDialogTimeoutException
40
33
  from sapiopycommons.recordmodel.record_handler import RecordHandler
41
34
 
42
- DataTypeLayoutIdentifier: TypeAlias = DataTypeLayout | str | None
43
-
44
35
 
45
36
  class CallbackUtil:
46
37
  user: SapioUser
47
38
  callback: ClientCallback
48
- rec_handler: RecordHandler
49
- dt_man: DataTypeManager
50
39
  dt_cache: DataTypeCacheManager
51
40
  _original_timeout: int
52
41
  timeout_seconds: int
@@ -56,10 +45,6 @@ class CallbackUtil:
56
45
  __instances: WeakValueDictionary[SapioUser, CallbackUtil] = WeakValueDictionary()
57
46
  __initialized: bool
58
47
 
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
48
  def __new__(cls, context: UserIdentifier):
64
49
  """
65
50
  :param context: The current webhook context or a user object to send requests from.
@@ -82,14 +67,11 @@ class CallbackUtil:
82
67
 
83
68
  self.user = AliasUtil.to_sapio_user(context)
84
69
  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
70
  self.dt_cache = DataTypeCacheManager(self.user)
88
71
  self._original_timeout = self.user.timeout_seconds
89
72
  self.timeout_seconds = self.user.timeout_seconds
90
73
  self.width_pixels = None
91
74
  self.width_percent = None
92
- self.__layouts = {}
93
75
 
94
76
  def set_dialog_width(self, width_pixels: int | None = None, width_percent: float | None = None):
95
77
  """
@@ -119,7 +101,7 @@ class CallbackUtil:
119
101
  """
120
102
  Display a toaster popup in the bottom right corner of the user's screen.
121
103
 
122
- :param message: The message to display in the toaster. This can be formatted using HTML elements.
104
+ :param message: The message to display in the toaster.
123
105
  :param title: The title to display at the top of the toaster.
124
106
  :param popup_type: The popup type to use for the toaster. This controls the color that the toaster appears with.
125
107
  Info is blue, Success is green, Warning is yellow, and Error is red
@@ -131,7 +113,7 @@ class CallbackUtil:
131
113
  Display an info message to the user in a dialog. Repeated calls to this function will append the new messages
132
114
  to the same dialog if it is still opened by the user.
133
115
 
134
- :param message: The message to display to the user. This can be formatted using HTML elements.
116
+ :param message: The message to display to the user.
135
117
  """
136
118
  self.callback.display_info(message)
137
119
 
@@ -140,7 +122,7 @@ class CallbackUtil:
140
122
  Display a warning message to the user in a dialog. Repeated calls to this function will append the new messages
141
123
  to the same dialog if it is still opened by the user.
142
124
 
143
- :param message: The message to display to the user. This can be formatted using HTML elements.
125
+ :param message: The message to display to the user.
144
126
  """
145
127
  self.callback.display_warning(message)
146
128
 
@@ -149,17 +131,17 @@ class CallbackUtil:
149
131
  Display an error message to the user in a dialog. Repeated calls to this function will append the new messages
150
132
  to the same dialog if it is still opened by the user.
151
133
 
152
- :param message: The message to display to the user. This can be formatted using HTML elements.
134
+ :param message: The message to display to the user.
153
135
  """
154
136
  self.callback.display_error(message)
155
137
 
156
- def option_dialog(self, title: str, msg: str, options: Iterable[str], default_option: int = 0,
138
+ def option_dialog(self, title: str, msg: str, options: list[str], default_option: int = 0,
157
139
  user_can_cancel: bool = False) -> str:
158
140
  """
159
141
  Create an option dialog with the given options for the user to choose from.
160
142
 
161
143
  :param title: The title of the dialog.
162
- :param msg: The message to display in the dialog. This can be formatted using HTML elements.
144
+ :param msg: The message to display in the dialog.
163
145
  :param options: The button options that the user has to choose from.
164
146
  :param default_option: The index of the option in the options list that defaults as the first choice.
165
147
  :param user_can_cancel: True if the user is able to click the X to close the dialog. False if the user cannot
@@ -167,16 +149,17 @@ class CallbackUtil:
167
149
  SapioUserCancelledException is thrown.
168
150
  :return: The name of the button that the user selected.
169
151
  """
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
152
  request = OptionDialogRequest(title, msg, options, default_option, user_can_cancel,
178
153
  width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
179
- response: int = self.__handle_dialog_request(request, self.callback.show_option_dialog)
154
+ try:
155
+ self.user.timeout_seconds = self.timeout_seconds
156
+ response: int | None = self.callback.show_option_dialog(request)
157
+ except ReadTimeout:
158
+ raise SapioDialogTimeoutException()
159
+ finally:
160
+ self.user.timeout_seconds = self._original_timeout
161
+ if response is None:
162
+ raise SapioUserCancelledException()
180
163
  return options[response]
181
164
 
182
165
  def ok_dialog(self, title: str, msg: str) -> None:
@@ -185,7 +168,7 @@ class CallbackUtil:
185
168
  dialog using the X at the top right corner. Returns nothing.
186
169
 
187
170
  :param title: The title of the dialog.
188
- :param msg: The message to display in the dialog. This can be formatted using HTML elements.
171
+ :param msg: The message to display in the dialog.
189
172
  """
190
173
  self.option_dialog(title, msg, ["OK"], 0, False)
191
174
 
@@ -195,7 +178,7 @@ class CallbackUtil:
195
178
  dialog using the X at the top right corner.
196
179
 
197
180
  :param title: The title of the dialog.
198
- :param msg: The message to display in the dialog. This can be formatted using HTML elements.
181
+ :param msg: The message to display in the dialog.
199
182
  :param default_ok: If true, "OK" is the default choice. Otherwise, the default choice is "Cancel".
200
183
  :return: True if the user selected OK. False if the user selected Cancel.
201
184
  """
@@ -207,22 +190,14 @@ class CallbackUtil:
207
190
  dialog using the X at the top right corner.
208
191
 
209
192
  :param title: The title of the dialog.
210
- :param msg: The message to display in the dialog. This can be formatted using HTML elements.
193
+ :param msg: The message to display in the dialog.
211
194
  :param default_yes: If true, "Yes" is the default choice. Otherwise, the default choice is "No".
212
195
  :return: True if the user selected Yes. False if the user selected No.
213
196
  """
214
197
  return self.option_dialog(title, msg, ["Yes", "No"], 0 if default_yes else 1, False) == "Yes"
215
198
 
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]:
199
+ def list_dialog(self, title: str, options: list[str], multi_select: bool = False,
200
+ preselected_values: list[str] | None = None) -> list[str]:
226
201
  """
227
202
  Create a list dialog with the given options for the user to choose from.
228
203
 
@@ -231,27 +206,19 @@ class CallbackUtil:
231
206
  :param multi_select: Whether the user is able to select multiple options from the list.
232
207
  :param preselected_values: A list of values that will already be selected when the list dialog is created. The
233
208
  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
209
  :return: The list of options that the user selected.
239
210
  """
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,
211
+ request = ListDialogRequest(title, multi_select, options, preselected_values,
246
212
  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)
213
+ try:
214
+ self.user.timeout_seconds = self.timeout_seconds
215
+ response: list[str] | None = self.callback.show_list_dialog(request)
216
+ except ReadTimeout:
217
+ raise SapioDialogTimeoutException()
218
+ finally:
219
+ self.user.timeout_seconds = self._original_timeout
220
+ if response is None:
221
+ raise SapioUserCancelledException()
255
222
  return response
256
223
 
257
224
  def form_dialog(self,
@@ -261,7 +228,7 @@ class CallbackUtil:
261
228
  values: FieldMap = None,
262
229
  column_positions: dict[str, tuple[int, int]] = None,
263
230
  *,
264
- data_type: DataTypeIdentifier = "Default",
231
+ data_type: str = "Default",
265
232
  display_name: str | None = None,
266
233
  plural_display_name: str | None = None) -> FieldMap:
267
234
  """
@@ -269,7 +236,7 @@ class CallbackUtil:
269
236
  provide the definitions of every field in the form.
270
237
 
271
238
  :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.
239
+ :param msg: The message to display at the top of the form.
273
240
  :param fields: The definitions of the fields to display in the form. Fields will be displayed in the order they
274
241
  are provided in this list.
275
242
  :param values: Sets the default values of the fields.
@@ -283,249 +250,133 @@ class CallbackUtil:
283
250
  :return: A dictionary mapping the data field names of the given field definitions to the response value from
284
251
  the user for that field.
285
252
  """
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)
253
+ if display_name is None:
254
+ display_name = data_type
255
+ if plural_display_name is None:
256
+ plural_display_name = display_name + "s"
288
257
 
289
- # Send the request to the user.
290
- request = FormEntryDialogRequest(title, msg, temp_dt, values,
258
+ builder = FormBuilder(data_type, display_name, plural_display_name)
259
+ for field_def in fields:
260
+ field_name = field_def.data_field_name
261
+ column: int = 0
262
+ span: int = 4
263
+ if column_positions and field_name in column_positions:
264
+ position = column_positions.get(field_name)
265
+ column = position[0]
266
+ span = position[1]
267
+ builder.add_field(field_def, column, span)
268
+
269
+ request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
291
270
  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)
271
+ try:
272
+ self.user.timeout_seconds = self.timeout_seconds
273
+ response: FieldMap | None = self.callback.show_form_entry_dialog(request)
274
+ except ReadTimeout:
275
+ raise SapioDialogTimeoutException()
276
+ finally:
277
+ self.user.timeout_seconds = self._original_timeout
278
+ if response is None:
279
+ raise SapioUserCancelledException()
293
280
  return response
294
281
 
295
282
  def record_form_dialog(self,
296
283
  title: str,
297
284
  msg: str,
298
- fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
285
+ fields: list[str],
299
286
  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:
287
+ column_positions: dict[str, tuple[int, int]] = None,
288
+ editable: bool | None = True) -> FieldMap:
305
289
  """
306
290
  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.
291
+ a given record. Provided field names must match fields on the definition of the data type of the given record.
292
+ The fields that are displayed will have their default value be that of the fields on the given record.
308
293
 
309
294
  Makes webservice calls to get the data type definition and fields of the given record if they weren't
310
295
  previously cached.
311
296
 
312
297
  :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.
298
+ :param msg: The message to display in the dialog.
299
+ :param fields: The data field names of the fields from the record to display in the form. Fields will be
300
+ displayed in the order they are provided in this list.
319
301
  :param record: The record to display the values of.
320
302
  :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.
303
+ span. (Field order is still determined by the fields list.)
304
+ :param editable: If true, all fields are displayed as editable. If false, all fields are displayed as
305
+ uneditable. If none, only those fields that are defined as editable by the data designer will be editable.
333
306
  :return: A dictionary mapping the data field names of the given field definitions to the response value from
334
307
  the user for that field.
335
308
  """
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)
309
+ # Get the field definitions of the data type.
310
+ data_type: str = record.data_type_name
311
+ type_def: DataTypeDefinition = self.dt_cache.get_data_type(data_type)
312
+ field_defs: dict[str, AbstractVeloxFieldDefinition] = self.dt_cache.get_fields_for_type(data_type)
340
313
 
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)
314
+ # Make everything visible, because presumably the caller gave a field name because they want it to be seen.
315
+ modifier = FieldModifier(visible=True, editable=editable)
344
316
 
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,
317
+ # Build the form using only those fields that are desired.
318
+ values: dict[str, FieldValue] = {}
319
+ builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
320
+ for field_name in fields:
321
+ field_def = field_defs.get(field_name)
322
+ if field_def is None:
323
+ raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
324
+ values[field_name] = record.get_field_value(field_name)
325
+ column: int = 0
326
+ span: int = 4
327
+ if column_positions and field_name in column_positions:
328
+ position = column_positions.get(field_name)
329
+ column = position[0]
330
+ span = position[1]
331
+ builder.add_field(modifier.modify_field(field_def), column, span)
332
+
333
+ request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
363
334
  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)
335
+ try:
336
+ self.user.timeout_seconds = self.timeout_seconds
337
+ response: FieldMap | None = self.callback.show_form_entry_dialog(request)
338
+ except ReadTimeout:
339
+ raise SapioDialogTimeoutException()
340
+ finally:
341
+ self.user.timeout_seconds = self._original_timeout
342
+ if response is None:
343
+ raise SapioUserCancelledException()
365
344
  return response
366
345
 
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:
346
+ def input_dialog(self, title: str, msg: str, field: AbstractVeloxFieldDefinition) -> FieldValue:
461
347
  """
462
348
  Create an input dialog where the user must input data for a singular field.
463
349
 
464
350
  :param title: The title of the dialog.
465
- :param msg: The message to display in the dialog. This can be formatted using HTML elements.
351
+ :param msg: The message to display in the dialog.
466
352
  :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
353
  :return: The response value from the user for the given field.
472
354
  """
473
- # Send the request to the user.
474
355
  request = InputDialogCriteria(title, msg, field,
475
356
  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)
357
+ try:
358
+ self.user.timeout_seconds = self.timeout_seconds
359
+ response: FieldValue | None = self.callback.show_input_dialog(request)
360
+ except ReadTimeout:
361
+ raise SapioDialogTimeoutException()
362
+ finally:
363
+ self.user.timeout_seconds = self._original_timeout
364
+ if response is None:
365
+ raise SapioUserCancelledException()
502
366
  return response
503
367
 
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:
368
+ def string_input_dialog(self, title: str, msg: str, field_name: str, default_value: str | None = None,
369
+ max_length: int | None = None, editable: bool = True, **kwargs) -> str:
515
370
  """
516
371
  Create an input dialog where the user must input data for a singular text field.
517
372
 
518
373
  :param title: The title of the dialog.
519
- :param msg: The message to display in the dialog. This can be formatted using HTML elements.
374
+ :param msg: The message to display in the dialog.
520
375
  :param field_name: The name and display name of the string field.
521
376
  :param default_value: The default value to place into the string field, if any.
522
377
  :param max_length: The max length of the string value. If not provided, uses the length of the default value.
523
378
  If neither this nor a default value are provided, defaults to 100 characters.
524
379
  :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
380
  :param kwargs: Any additional keyword arguments to pass to the field definition.
530
381
  :return: The string that the user input into the dialog.
531
382
  """
@@ -533,36 +384,21 @@ class CallbackUtil:
533
384
  max_length = len(default_value) if default_value else 100
534
385
  field = VeloxStringFieldDefinition("Input", field_name, field_name, default_value=default_value,
535
386
  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:
387
+ return self.input_dialog(title, msg, field)
388
+
389
+ def integer_input_dialog(self, title: str, msg: str, field_name: str, default_value: int = None,
390
+ min_value: int = -10000, max_value: int = 10000, editable: bool = True, **kwargs) -> int:
551
391
  """
552
392
  Create an input dialog where the user must input data for a singular integer field.
553
393
 
554
394
  :param title: The title of the dialog.
555
- :param msg: The message to display in the dialog. This can be formatted using HTML elements.
395
+ :param msg: The message to display in the dialog.
556
396
  :param field_name: The name and display name of the integer field.
557
397
  :param default_value: The default value to place into the integer field. If not provided, defaults to the 0 or
558
398
  the minimum value, whichever is higher.
559
399
  :param min_value: The minimum allowed value of the input.
560
400
  :param max_value: The maximum allowed value of the input.
561
401
  :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
402
  :param kwargs: Any additional keyword arguments to pass to the field definition.
567
403
  :return: The integer that the user input into the dialog.
568
404
  """
@@ -570,36 +406,22 @@ class CallbackUtil:
570
406
  default_value = max(0, min_value)
571
407
  field = VeloxIntegerFieldDefinition("Input", field_name, field_name, default_value=default_value,
572
408
  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)
409
+ return self.input_dialog(title, msg, field)
575
410
 
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.",
411
+ def double_input_dialog(self, title: str, msg: str, field_name: str, default_value: float = None,
412
+ min_value: float = -10000000, max_value: float = 100000000, editable: bool = True,
587
413
  **kwargs) -> float:
588
414
  """
589
415
  Create an input dialog where the user must input data for a singular double field.
590
416
 
591
417
  :param title: The title of the dialog.
592
- :param msg: The message to display in the dialog. This can be formatted using HTML elements.
418
+ :param msg: The message to display in the dialog.
593
419
  :param field_name: The name and display name of the double field.
594
420
  :param default_value: The default value to place into the double field. If not provided, defaults to the 0 or
595
421
  the minimum value, whichever is higher.
596
422
  :param min_value: The minimum allowed value of the input.
597
423
  :param max_value: The maximum allowed value of the input.
598
424
  :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
425
  :param kwargs: Any additional keyword arguments to pass to the field definition.
604
426
  :return: The float that the user input into the dialog.
605
427
  """
@@ -607,26 +429,25 @@ class CallbackUtil:
607
429
  default_value = max(0., min_value)
608
430
  field = VeloxDoubleFieldDefinition("Input", field_name, field_name, default_value=default_value,
609
431
  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)
432
+ return self.input_dialog(title, msg, field)
612
433
 
613
434
  def table_dialog(self,
614
435
  title: str,
615
436
  msg: str,
616
437
  fields: list[AbstractVeloxFieldDefinition],
617
438
  values: list[FieldMap],
439
+ group_by: str | None = None,
440
+ image_data: list[bytes] | None = None,
618
441
  *,
619
- data_type: DataTypeIdentifier = "Default",
442
+ data_type: str = "Default",
620
443
  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]:
444
+ plural_display_name: str | None = None) -> list[FieldMap]:
624
445
  """
625
446
  Create a table dialog where the user may input data into the fields of the table. Requires that the caller
626
447
  provide the definitions of every field in the table.
627
448
 
628
449
  :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.
450
+ :param msg: The message to display at the top of the form.
630
451
  :param fields: The definitions of the fields to display as table columns. Fields will be displayed in the order
631
452
  they are provided in this list.
632
453
  :param values: The values to set for each row of the table.
@@ -642,61 +463,57 @@ class CallbackUtil:
642
463
  :return: A list of dictionaries mapping the data field names of the given field definitions to the response
643
464
  value from the user for that field for each row.
644
465
  """
645
- if not values:
646
- raise SapioException("No values provided.")
466
+ if display_name is None:
467
+ display_name = data_type
468
+ if plural_display_name is None:
469
+ plural_display_name = display_name + "s"
647
470
 
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)
471
+ # Key fields display their columns in order before all non-key fields.
472
+ # Unmark key fields so that the column order is respected exactly as the caller provides it.
473
+ modifier = FieldModifier(key_field=False)
651
474
 
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)
475
+ builder = FormBuilder(data_type, display_name, plural_display_name)
476
+ for field in fields:
477
+ builder.add_field(modifier.modify_field(field))
656
478
 
657
- # Send the request to the user.
658
- request = TableEntryDialogRequest(title, msg, temp_dt, values,
479
+ request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
659
480
  record_image_data_list=image_data, group_by_field=group_by,
660
481
  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)
482
+ try:
483
+ self.user.timeout_seconds = self.timeout_seconds
484
+ response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
485
+ except ReadTimeout:
486
+ raise SapioDialogTimeoutException()
487
+ finally:
488
+ self.user.timeout_seconds = self._original_timeout
489
+ if response is None:
490
+ raise SapioUserCancelledException()
662
491
  return response
663
492
 
664
493
  def record_table_dialog(self,
665
494
  title: str,
666
495
  msg: str,
667
- fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
496
+ fields: list[str],
668
497
  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,
498
+ editable: bool | None = True,
499
+ group_by: str | None = None,
674
500
  image_data: list[bytes] | None = None) -> list[FieldMap]:
675
501
  """
676
502
  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.
503
+ a given list of records of a singular type. Provided field names must match fields on the definition of the data
504
+ type of the given records. The fields that are displayed will have their default value be that of the fields on
505
+ the given records.
678
506
 
679
507
  Makes webservice calls to get the data type definition and fields of the given records if they weren't
680
508
  previously cached.
681
509
 
682
510
  :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.
511
+ :param msg: The message to display in the dialog.
689
512
  :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.
513
+ :param fields: The names of the fields to display as columns in the table. Fields will be displayed in the order
514
+ they are provided in this list.
515
+ :param editable: If true, all fields are displayed as editable. If false, all fields are displayed as
516
+ uneditable. If none, only those fields that are defined as editable by the data designer will be editable.
700
517
  :param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
701
518
  The user may remove this grouping if they want to.
702
519
  :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
@@ -704,351 +521,54 @@ class CallbackUtil:
704
521
  :return: A list of dictionaries mapping the data field names of the given field definitions to the response
705
522
  value from the user for that field for each row.
706
523
  """
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
524
  if not records:
713
525
  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_list(records)
526
+ data_types: set[str] = {x.data_type_name for x in records}
527
+ if len(data_types) > 1:
528
+ raise SapioException("Multiple data type names encountered in records list for record table popup.")
529
+ data_type: str = data_types.pop()
530
+ # Get the field maps from the records.
531
+ field_map_list: list[FieldMap] = AliasUtil.to_field_map_lists(records)
532
+ # Get the field definitions of the data type.
533
+ type_def: DataTypeDefinition = self.dt_cache.get_data_type(data_type)
534
+ field_defs: dict[str, AbstractVeloxFieldDefinition] = self.dt_cache.get_fields_for_type(data_type)
716
535
 
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)
536
+ # Key fields display their columns in order before all non-key fields.
537
+ # Unmark key fields so that the column order is respected exactly as the caller provides it.
538
+ # Also make everything visible, because presumably the caller gave a field name because they want it to be seen.
539
+ modifier = FieldModifier(visible=True, key_field=False, editable=editable)
720
540
 
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,
541
+ # Build the form using only those fields that are desired.
542
+ builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
543
+ for field_name in fields:
544
+ field_def = field_defs.get(field_name)
545
+ if field_def is None:
546
+ raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
547
+ builder.add_field(modifier.modify_field(field_def))
548
+
549
+ request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), field_map_list,
739
550
  record_image_data_list=image_data, group_by_field=group_by,
740
551
  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)
552
+ try:
553
+ self.user.timeout_seconds = self.timeout_seconds
554
+ response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
555
+ except ReadTimeout:
556
+ raise SapioDialogTimeoutException()
557
+ finally:
558
+ self.user.timeout_seconds = self._original_timeout
559
+ if response is None:
560
+ raise SapioUserCancelledException()
742
561
  return response
743
562
 
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
563
  def multi_type_table_dialog(self,
1044
564
  title: str,
1045
565
  msg: str,
1046
- fields: list[tuple[DataTypeIdentifier, FieldIdentifier] | AbstractVeloxFieldDefinition],
566
+ fields: list[(str, str) | AbstractVeloxFieldDefinition],
1047
567
  row_contents: list[list[SapioRecord | FieldMap]],
1048
568
  *,
1049
569
  default_modifier: FieldModifier | None = None,
1050
- field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
1051
- data_type: DataTypeIdentifier = "Default",
570
+ field_modifiers: dict[str, FieldModifier] | None = None,
571
+ data_type: str = "Default",
1052
572
  display_name: str | None = None,
1053
573
  plural_display_name: str | None = None) -> list[FieldMap]:
1054
574
  """
@@ -1061,7 +581,7 @@ class CallbackUtil:
1061
581
  previously cached.
1062
582
 
1063
583
  :param title: The title of the dialog.
1064
- :param msg: The message to display in the dialog. This can be formatted using HTML elements.
584
+ :param msg: The message to display in the dialog.
1065
585
  :param fields: A list of objects representing the fields in the table. This could either be a two-element tuple
1066
586
  where the first element is a data type name and the second is a field name, or it could be a field
1067
587
  definition. If it is the former, a query will be made to find the field definition matching tht data type.
@@ -1107,31 +627,23 @@ class CallbackUtil:
1107
627
  :return: A list of dictionaries mapping the data field names of the given field definitions to the response
1108
628
  value from the user for that field for each row.
1109
629
  """
1110
- if not row_contents:
1111
- raise SapioException("No values provided.")
1112
-
1113
630
  # Set the default modifier to make all fields visible and not key if no default was provided.
1114
631
  if default_modifier is None:
1115
632
  default_modifier = FieldModifier(visible=True, key_field=False)
1116
633
  # To make things simpler, treat null field modifiers as an empty dict.
1117
634
  if field_modifiers is None:
1118
635
  field_modifiers = {}
1119
- else:
1120
- field_modifiers: dict[str, FieldModifier] = AliasUtil.to_data_field_names_dict(field_modifiers)
1121
636
 
1122
637
  # Construct the final fields list from the possible field objects.
1123
638
  final_fields: list[AbstractVeloxFieldDefinition] = []
1124
639
  # Keep track of whether any given field name appears more than once, as two fields could have the same
1125
640
  # field name but different data types. In this case, the user should provide a field modifier or field
1126
641
  # definition that changes one of the field names.
1127
- raw_field_names: set[str] = set()
1128
- field_names: set[str] = set()
642
+ field_names: list[str] = []
1129
643
  for field in fields:
1130
644
  # Find the field definition for this field object.
1131
645
  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)
646
+ field_def: AbstractVeloxFieldDefinition = self.dt_cache.get_fields_for_type(field[0]).get(field[1])
1135
647
  elif isinstance(field, AbstractVeloxFieldDefinition):
1136
648
  field_def: AbstractVeloxFieldDefinition = field
1137
649
  else:
@@ -1139,21 +651,7 @@ class CallbackUtil:
1139
651
 
1140
652
  # Locate the modifier for this field and store the modified field.
1141
653
  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)
654
+ modifier: FieldModifier = field_modifiers.get(name, default_modifier)
1157
655
  field_def: AbstractVeloxFieldDefinition = modifier.modify_field(field_def)
1158
656
  final_fields.append(field_def)
1159
657
 
@@ -1163,9 +661,9 @@ class CallbackUtil:
1163
661
  if name in field_names:
1164
662
  raise SapioException(f"The field name \"{name}\" appears more than once in the given fields. "
1165
663
  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)
664
+ f"consider providing a FieldModifier where prepend_data_type is true for one of "
665
+ f"the fields so that the field names will be different.")
666
+ field_names.append(name)
1169
667
 
1170
668
  # Get the values for each row.
1171
669
  values: list[dict[str, FieldValue]] = []
@@ -1180,7 +678,7 @@ class CallbackUtil:
1180
678
  if rec is None:
1181
679
  continue
1182
680
  # 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)
681
+ dt: str = "Default" if isinstance(rec, dict) else rec.data_type_name
1184
682
  # Warn if the same data type name appears more than once.
1185
683
  if dt in row_records:
1186
684
  raise SapioException(f"The data type \"{dt}\" appears more than once in the given row contents.")
@@ -1192,7 +690,7 @@ class CallbackUtil:
1192
690
  record: SapioRecord | FieldMap | None = row_records.get(field.data_type_name)
1193
691
  # This could be either a record, a field map, or null. Convert any records to field maps.
1194
692
  if not isinstance(record, dict) and record is not None:
1195
- record: FieldMap | None = AliasUtil.to_field_map(record)
693
+ record: FieldMap | None = AliasUtil.to_field_map_lists([record])[0]
1196
694
 
1197
695
  # Find out if this field had its data type prepended to it. If this is the case, then we need to find
1198
696
  # the true data field name before retrieving the value from the field map.
@@ -1204,19 +702,32 @@ class CallbackUtil:
1204
702
  row_values[field.data_field_name] = record.get(name) if record else None
1205
703
  values.append(row_values)
1206
704
 
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)
705
+ if display_name is None:
706
+ display_name = data_type
707
+ if plural_display_name is None:
708
+ plural_display_name = display_name + "s"
709
+
710
+ builder = FormBuilder(data_type, display_name, plural_display_name)
711
+ for field in final_fields:
712
+ builder.add_field(field)
1209
713
 
1210
- # Send the request to the user.
1211
- request = TableEntryDialogRequest(title, msg, temp_dt, values,
714
+ request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
1212
715
  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)
716
+ try:
717
+ self.user.timeout_seconds = self.timeout_seconds
718
+ response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
719
+ except ReadTimeout:
720
+ raise SapioDialogTimeoutException()
721
+ finally:
722
+ self.user.timeout_seconds = self._original_timeout
723
+ if response is None:
724
+ raise SapioUserCancelledException()
1214
725
  return response
1215
-
726
+
1216
727
  def record_view_dialog(self,
1217
728
  title: str,
1218
729
  record: SapioRecord,
1219
- layout: DataTypeLayoutIdentifier = None,
730
+ layout: str | DataTypeLayout | None = None,
1220
731
  minimized: bool = False,
1221
732
  access_level: FormAccessLevel | None = None,
1222
733
  plugin_path_list: list[str] | None = None) -> None:
@@ -1240,192 +751,145 @@ class CallbackUtil:
1240
751
  :param plugin_path_list: A white list of plugins that should be displayed in the dialog. This white list
1241
752
  includes plugins that would be displayed on sub-tables/components in the layout.
1242
753
  """
1243
- # Get the data record and data type layout from the provided parameters.
754
+ # Ensure that the given record is a DataRecord.
1244
755
  record: DataRecord = AliasUtil.to_data_record(record)
1245
- layout: DataTypeLayout | None = self.__to_layout(AliasUtil.to_data_type_name(record), layout)
1246
756
 
1247
- # Send the request to the user.
757
+ # Get the corresponding DataTypeLayout for the provided name.
758
+ if isinstance(layout, str):
759
+ # TODO: Replace with dt_cache if the DataTypeCacheManager ever starts caching layouts.
760
+ dt_man = DataMgmtServer.get_data_type_manager(self.user)
761
+ data_type: str = record.get_data_type_name()
762
+ layouts: dict[str, DataTypeLayout] = {x.layout_name: x for x in dt_man.get_data_type_layout_list(data_type)}
763
+ layout_name: str = layout
764
+ layout: DataTypeLayout | None = layouts.get(layout_name)
765
+ # If a name was provided then the caller expects that name to exist. Throw an exception if it doesn't.
766
+ if not layout:
767
+ raise SapioException(f"The data type \"{data_type}\" does not have a layout by the name "
768
+ f"\"{layout_name}\" in the system.")
769
+
1248
770
  request = DataRecordDialogRequest(title, record, layout, minimized, access_level, plugin_path_list,
1249
771
  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.
772
+ try:
773
+ self.user.timeout_seconds = self.timeout_seconds
774
+ response: bool = self.callback.data_record_form_view_dialog(request)
775
+ except ReadTimeout:
776
+ raise SapioDialogTimeoutException()
777
+ finally:
778
+ self.user.timeout_seconds = self._original_timeout
1253
779
  if not response:
1254
780
  raise SapioUserCancelledException()
1255
-
1256
- # CR-47326: Allow the selection dialog functions to preselect rows/records in the table.
781
+
1257
782
  def selection_dialog(self,
1258
783
  msg: str,
1259
784
  fields: list[AbstractVeloxFieldDefinition],
1260
785
  values: list[FieldMap],
1261
786
  multi_select: bool = True,
1262
- preselected_rows: list[FieldMap | RecordIdentifier] | None = None,
1263
787
  *,
1264
- data_type: DataTypeIdentifier = "Default",
788
+ data_type: str = "Default",
1265
789
  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]:
790
+ plural_display_name: str | None = None) -> list[FieldMap]:
1270
791
  """
1271
792
  Create a selection dialog for a list of field maps for the user to choose from. Requires that the caller
1272
793
  provide the definitions of every field in the table.
1273
- The title of a selection dialog will always be "Select [plural display name]".
1274
794
 
1275
- :param msg: The message to display in the dialog. This can be formatted using HTML elements.
795
+ :param msg: The message to display in the dialog.
1276
796
  :param fields: The definitions of the fields to display as table columns. Fields will be displayed in the order
1277
797
  they are provided in this list.
1278
798
  :param values: The values to set for each row of the table.
1279
799
  :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
800
  :param data_type: The data type name for the temporary data type that will be created for this table.
1286
801
  :param display_name: The display name for the temporary data type. If not provided, defaults to the data type
1287
802
  name.
1288
803
  :param plural_display_name: The plural display name for the temporary data type. If not provided, defaults to
1289
804
  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
805
  :return: A list of field maps corresponding to the chosen input field maps.
1297
806
  """
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
807
+ if display_name is None:
808
+ display_name = data_type
809
+ if plural_display_name is None:
810
+ plural_display_name = display_name + "s"
1344
811
 
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]:
812
+ builder = FormBuilder(data_type, display_name, plural_display_name)
813
+ for field in fields:
814
+ builder.add_field(field)
815
+
816
+ request = TempTableSelectionRequest(builder.get_temporary_data_type(), msg, values,
817
+ multi_select=multi_select)
818
+ try:
819
+ self.user.timeout_seconds = self.timeout_seconds
820
+ response: list[FieldMap] | None = self.callback.show_temp_table_selection_dialog(request)
821
+ except ReadTimeout:
822
+ raise SapioDialogTimeoutException()
823
+ finally:
824
+ self.user.timeout_seconds = self._original_timeout
825
+ if response is None:
826
+ raise SapioUserCancelledException()
827
+ return response
828
+
829
+ def record_selection_dialog(self, msg: str, fields: list[str], records: list[SapioRecord],
830
+ multi_select: bool = True) -> list[SapioRecord]:
1356
831
  """
1357
832
  Create a record selection dialog for a list of records for the user to choose from. Provided field names must
1358
833
  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
834
 
1361
835
  Makes webservice calls to get the data type definition and fields of the given records if they weren't
1362
836
  previously cached.
1363
837
 
1364
- :param msg: The message to display in the dialog. This can be formatted using HTML elements.
838
+ :param msg: The message to display in the dialog.
1365
839
  :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.
840
+ they are provided in this list.
1369
841
  :param records: The records to display as rows in the table.
1370
842
  :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
843
  :return: A list of the selected records.
1381
844
  """
1382
- # Get the data type name and field values from the provided records.
1383
845
  if not records:
1384
846
  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_list(records, include_record_id=True)
847
+ data_types: set[str] = {x.data_type_name for x in records}
848
+ if len(data_types) > 1:
849
+ raise SapioException("Multiple data type names encountered in records list for record table popup.")
850
+ data_type: str = data_types.pop()
851
+ # Get the field maps from the records.
852
+ field_map_list: list[FieldMap] = AliasUtil.to_field_map_lists(records)
853
+ # Put the record ID of each record in its corresponding field map so that we can map the field maps back to
854
+ # the records when we return them to the caller.
855
+ for record, field_map in zip(records, field_map_list):
856
+ field_map.update({"RecId": record.record_id})
857
+ # Get the field definitions of the data type.
858
+ type_def: DataTypeDefinition = self.dt_cache.get_data_type(data_type)
859
+ field_defs: dict[str, AbstractVeloxFieldDefinition] = self.dt_cache.get_fields_for_type(data_type)
1387
860
 
1388
861
  # Key fields display their columns in order before all non-key fields.
1389
862
  # Unmark key fields so that the column order is respected exactly as the caller provides it.
1390
863
  # Also make everything visible, because presumably the caller give a field name because they want it to be seen.
1391
864
  modifier = FieldModifier(visible=True, key_field=False)
1392
865
 
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)
866
+ # Build the form using only those fields that are desired.
867
+ builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
868
+ for field_name in fields:
869
+ field_def = field_defs.get(field_name)
870
+ if field_def is None:
871
+ raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
872
+ builder.add_field(modifier.modify_field(field_def))
1419
873
 
874
+ request = TempTableSelectionRequest(builder.get_temporary_data_type(), msg, field_map_list,
875
+ multi_select=multi_select)
876
+ try:
877
+ self.user.timeout_seconds = self.timeout_seconds
878
+ response: list[FieldMap] | None = self.callback.show_temp_table_selection_dialog(request)
879
+ except ReadTimeout:
880
+ raise SapioDialogTimeoutException()
881
+ finally:
882
+ self.user.timeout_seconds = self._original_timeout
883
+ if response is None:
884
+ raise SapioUserCancelledException()
1420
885
  # Map the field maps in the response back to the record they come from, returning the chosen record instead of
1421
886
  # the chosen field map.
1422
887
  records_by_id: dict[int, SapioRecord] = RecordHandler.map_by_id(records)
1423
888
  ret_list: list[SapioRecord] = []
1424
889
  for field_map in response:
1425
- ret_list.append(records_by_id.get(field_map.get("RecordId")))
890
+ ret_list.append(records_by_id.get(field_map.get("RecId")))
1426
891
  return ret_list
1427
892
 
1428
- # CR-47377: Add allow_creation and default_creation_number to cover new parameters of this request type from 24.12.
1429
893
  def input_selection_dialog(self,
1430
894
  wrapper_type: type[WrappedType],
1431
895
  msg: str,
@@ -1436,21 +900,14 @@ class CallbackUtil:
1436
900
  custom_search: CustomReport | CustomReportCriteria | str | None = None,
1437
901
  preselected_records: list[RecordIdentifier] | None = None,
1438
902
  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]:
903
+ record_whitelist: list[RecordIdentifier] | None = None) -> list[WrappedType]:
1446
904
  """
1447
905
  Display a table of records that exist in the system matching the given data type and filter criteria and have
1448
906
  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
907
 
1451
908
  :param wrapper_type: The record model wrapper for the records to display in the dialog.
1452
909
  :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.
910
+ instruct the user on what is desired from the dialog.
1454
911
  :param multi_select: Whether the user may select multiple items at once in this dialog.
1455
912
  :param only_key_fields: Whether only key fields of the selected data type should be displayed in the table
1456
913
  of data in the dialog. If false, allows all possible fields to be displayed as columns in the table.
@@ -1474,18 +931,6 @@ class CallbackUtil:
1474
931
  :param record_blacklist: A list of records that should not be seen as possible options in the dialog.
1475
932
  :param record_whitelist: A list of records that will be seen as possible options in the dialog. Records not in
1476
933
  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
934
  :return: A list of the records selected by the user in the dialog, wrapped as record models using the provided
1490
935
  wrapper.
1491
936
  """
@@ -1507,61 +952,50 @@ class CallbackUtil:
1507
952
  if isinstance(custom_search, str):
1508
953
  custom_search: CustomReport = CustomReportUtil.get_system_report_criteria(self.user, custom_search)
1509
954
 
1510
- # Send the request to the user.
1511
955
  request = InputSelectionRequest(data_type, msg, search_types, only_key_fields, record_blacklist,
1512
956
  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:
957
+ multi_select)
958
+ try:
959
+ self.user.timeout_seconds = self.timeout_seconds
960
+ response: list[DataRecord] | None = self.callback.show_input_selection_dialog(request)
961
+ except ReadTimeout:
962
+ raise SapioDialogTimeoutException()
963
+ finally:
964
+ self.user.timeout_seconds = self._original_timeout
965
+ if response is None:
966
+ raise SapioUserCancelledException()
967
+ return RecordHandler(self.user).wrap_models(response, wrapper_type)
968
+
969
+ def esign_dialog(self, title: str, msg: str, show_comment: bool = True,
970
+ additional_fields: list[AbstractVeloxFieldDefinition] = None) -> ESigningResponsePojo:
1531
971
  """
1532
972
  Create an e-sign dialog for the user to interact with.
1533
-
973
+
1534
974
  :param title: The title of the dialog.
1535
- :param msg: The message to display in the dialog. This can be formatted using HTML elements.
975
+ :param msg: The message to display in the dialog.
1536
976
  :param show_comment: Whether the "Meaning of Action" field should appear in the e-sign dialog. If true, the
1537
977
  user is required to provide an action.
1538
978
  :param additional_fields: Field definitions for additional fields to display in the dialog, for if there is
1539
979
  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
980
  :return: An e-sign response object containing information about the e-sign attempt.
1543
981
  """
1544
- # Construct a temporary data type if any additional fields are provided.
1545
982
  temp_dt = None
1546
983
  if additional_fields:
1547
984
  builder = FormBuilder()
1548
985
  for field in additional_fields:
1549
986
  builder.add_field(field)
1550
987
  temp_dt = builder.get_temporary_data_type()
1551
-
1552
- # Send the request to the user.
1553
988
  request = ESigningRequestPojo(title, msg, show_comment, temp_dt,
1554
989
  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)
990
+ try:
991
+ self.user.timeout_seconds = self.timeout_seconds
992
+ response: ESigningResponsePojo | None = self.callback.show_esign_dialog(request)
993
+ except ReadTimeout:
994
+ raise SapioDialogTimeoutException()
995
+ finally:
996
+ self.user.timeout_seconds = self._original_timeout
997
+ if response is None:
998
+ raise SapioUserCancelledException()
1565
999
  return response
1566
1000
 
1567
1001
  def request_file(self, title: str, exts: list[str] | None = None,
@@ -1589,11 +1023,17 @@ class CallbackUtil:
1589
1023
  def do_consume(chunk: bytes) -> None:
1590
1024
  return sink.consume_data(chunk, io_obj)
1591
1025
 
1592
- # Send the request to the user.
1593
1026
  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)
1027
+ try:
1028
+ self.user.timeout_seconds = self.timeout_seconds
1029
+ file_path: str | None = self.callback.show_file_dialog(request, do_consume)
1030
+ except ReadTimeout:
1031
+ raise SapioDialogTimeoutException()
1032
+ finally:
1033
+ self.user.timeout_seconds = self._original_timeout
1034
+ if file_path is None:
1035
+ raise SapioUserCancelledException()
1595
1036
 
1596
- # Verify that each of the file given matches the expected extension(s).
1597
1037
  self.__verify_file(file_path, sink.data, exts)
1598
1038
  return file_path, sink.data
1599
1039
 
@@ -1614,11 +1054,17 @@ class CallbackUtil:
1614
1054
  if exts is None:
1615
1055
  exts: list[str] = []
1616
1056
 
1617
- # Send the request to the user.
1618
1057
  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)
1058
+ try:
1059
+ self.user.timeout_seconds = self.timeout_seconds
1060
+ file_paths: list[str] | None = self.callback.show_multi_file_dialog(request)
1061
+ except ReadTimeout:
1062
+ raise SapioDialogTimeoutException()
1063
+ finally:
1064
+ self.user.timeout_seconds = self._original_timeout
1065
+ if not file_paths:
1066
+ raise SapioUserCancelledException()
1620
1067
 
1621
- # Verify that each of the files given match the expected extension(s).
1622
1068
  ret_dict: dict[str, bytes] = {}
1623
1069
  for file_path in file_paths:
1624
1070
  sink = InMemoryRecordDataSink(self.user)
@@ -1670,236 +1116,6 @@ class CallbackUtil:
1670
1116
  data = io.BytesIO(FileUtil.zip_files(files))
1671
1117
  self.callback.send_file(zip_name, False, data)
1672
1118
 
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
1119
 
1904
1120
  class FieldModifier:
1905
1121
  """
@@ -1943,99 +1159,29 @@ class FieldModifier:
1943
1159
  Apply modifications to a given field.
1944
1160
 
1945
1161
  :param field: The field to modify.
1946
- :return: A copy of the input field with the modifications applied. The input field is unchanged.
1162
+ :return: A copy of the input field with the modifications applied.
1947
1163
  """
1948
- ret_val: AbstractVeloxFieldDefinition = copy(field)
1164
+ field = copy_field(field)
1949
1165
  if self.prepend_data_type is True:
1950
- ret_val._data_field_name = ret_val.data_type_name + "." + ret_val.data_field_name
1166
+ field._data_field_name = field.data_type_name + "." + field.data_field_name
1951
1167
  if self.display_name is not None:
1952
- ret_val.display_name = self.display_name
1168
+ field.display_name = self.display_name
1953
1169
  if self.required is not None:
1954
- ret_val.required = self.required
1170
+ field.required = self.required
1955
1171
  if self.editable is not None:
1956
- ret_val.editable = self.editable
1172
+ field.editable = self.editable
1957
1173
  if self.visible is not None:
1958
- ret_val.visible = self.visible
1174
+ field.visible = self.visible
1959
1175
  if self.key_field is not None:
1960
- ret_val.key_field = self.key_field
1176
+ field.key_field = self.key_field
1961
1177
  if self.column_width is not None:
1962
- ret_val.default_table_column_width = self.column_width
1963
- return ret_val
1178
+ field.default_table_column_width = self.column_width
1179
+ return field
1964
1180
 
1965
1181
 
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:
1182
+ def copy_field(field: AbstractVeloxFieldDefinition) -> AbstractVeloxFieldDefinition:
1970
1183
  """
1971
- A FieldFilterCriteria can be used to filter the fields that are displayed in certain record-backed client callbacks.
1184
+ Create a copy of a given field definition. This is used to modify field definitions from the server for existing
1185
+ data types without also modifying the field definition in the cache.
1972
1186
  """
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
1187
+ return FieldDefinitionParser.to_field_definition(field.to_json())